├── versions.properties ├── settings.gradle ├── ic_launcher.ai ├── app ├── lint.xml ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_record_white_24dp.png │ │ │ ├── values-small │ │ │ │ └── dimens.xml │ │ │ ├── values-large │ │ │ │ └── dimens.xml │ │ │ ├── values-v21 │ │ │ │ ├── dimens.xml │ │ │ │ └── styles.xml │ │ │ ├── values │ │ │ │ ├── attrs.xml │ │ │ │ ├── ids.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── colors.xml │ │ │ ├── drawable │ │ │ │ ├── partial_select_image_cropped.png │ │ │ │ ├── shadow.xml │ │ │ │ ├── ic_stop_white_24dp.xml │ │ │ │ ├── ic_play_arrow_white_24dp.xml │ │ │ │ ├── ic_add_white_24dp.xml │ │ │ │ ├── ic_send_white_24dp.xml │ │ │ │ ├── ic_pause_white_24dp.xml │ │ │ │ ├── ic_expand_less_white_24dp.xml │ │ │ │ ├── ic_expand_more_white_24dp.xml │ │ │ │ ├── ic_sort_white_24dp.xml │ │ │ │ ├── ic_filter_list_white_24dp.xml │ │ │ │ ├── ic_delete_white_24dp.xml │ │ │ │ ├── ic_folder_white_24dp.xml │ │ │ │ ├── ic_insert_drive_file_white_24dp.xml │ │ │ │ ├── ic_save_white_24dp.xml │ │ │ │ ├── ic_view_agenda_white_24dp.xml │ │ │ │ └── ic_settings_white_24dp.xml │ │ │ ├── values-xlarge │ │ │ │ └── dimens.xml │ │ │ ├── xml │ │ │ │ ├── recording_widget_info.xml │ │ │ │ └── settings.xml │ │ │ ├── layout │ │ │ │ ├── dialog_new_filter.xml │ │ │ │ ├── dialog_send_log.xml │ │ │ │ ├── toolbar_stub.xml │ │ │ │ ├── activity_settings.xml │ │ │ │ ├── dialog_delete_logfiles.xml │ │ │ │ ├── dialog_recording_filter.xml │ │ │ │ ├── dialog_partial_save_help.xml │ │ │ │ ├── list_header_add_filter.xml │ │ │ │ ├── list_item_dropdown.xml │ │ │ │ ├── list_item_filter.xml │ │ │ │ ├── widget_recording.xml │ │ │ │ ├── list_item_logfilename_multi.xml │ │ │ │ ├── list_item_logfilename_single.xml │ │ │ │ ├── activity_logcat.xml │ │ │ │ ├── dialog_searchby.xml │ │ │ │ └── list_item_logcat.xml │ │ │ ├── raw │ │ │ │ ├── about_css.css │ │ │ │ ├── about_body.htm │ │ │ │ ├── translations.htm │ │ │ │ └── changelog.htm │ │ │ └── menu │ │ │ │ └── menu_main.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── pluscubed │ │ │ │ └── logcat │ │ │ │ ├── helper │ │ │ │ ├── SaveFileHelper.java │ │ │ │ ├── PackageHelper.java │ │ │ │ ├── BitmapHelper.java │ │ │ │ ├── VersionHelper.java │ │ │ │ ├── RuntimeHelper.java │ │ │ │ ├── BuildHelper.java │ │ │ │ ├── LogcatHelper.java │ │ │ │ ├── ServiceHelper.java │ │ │ │ ├── WidgetHelper.java │ │ │ │ ├── UpdateHelper.java │ │ │ │ └── DialogHelper.java │ │ │ │ ├── util │ │ │ │ ├── Function.java │ │ │ │ ├── Callback.java │ │ │ │ ├── Util.java │ │ │ │ ├── StopWatch.java │ │ │ │ ├── ArrayUtil.java │ │ │ │ ├── UtilLogger.java │ │ │ │ ├── LogLineAdapterUtil.java │ │ │ │ └── StringUtil.java │ │ │ │ ├── intents │ │ │ │ └── Intents.java │ │ │ │ ├── reader │ │ │ │ ├── AbsLogcatReader.java │ │ │ │ ├── LogcatReader.java │ │ │ │ ├── LogcatReaderLoader.java │ │ │ │ ├── SingleLogcatReader.java │ │ │ │ └── MultipleLogcatReader.java │ │ │ │ ├── data │ │ │ │ ├── FilterQueryWithLevel.java │ │ │ │ ├── SavedLog.java │ │ │ │ ├── LogLineViewHolder.java │ │ │ │ ├── SendLogDetails.java │ │ │ │ ├── FilterAdapter.java │ │ │ │ ├── LogFileAdapter.java │ │ │ │ ├── SearchCriteria.java │ │ │ │ ├── LogLine.java │ │ │ │ └── ColorScheme.java │ │ │ │ ├── widget │ │ │ │ ├── NoPopupDialogPreference.java │ │ │ │ ├── NonnegativeIntegerEditTextPreference.java │ │ │ │ ├── ExceptionCatchingListView.java │ │ │ │ └── MultipleChoicePreference.java │ │ │ │ ├── db │ │ │ │ ├── FilterItem.java │ │ │ │ └── CatlogDBHelper.java │ │ │ │ ├── CrazyLoggerService.java │ │ │ │ ├── RecordingWidgetProvider.java │ │ │ │ └── ui │ │ │ │ ├── AppCompatPreferenceActivity.java │ │ │ │ ├── AboutDialogActivity.java │ │ │ │ └── RecordLogDialogActivity.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ └── java │ │ └── com │ │ └── pluscubed │ │ └── logcat │ │ └── test │ │ └── LogFilterTest.java ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── .gitattributes ├── README.md ├── gradlew.bat ├── .gitignore └── gradlew /versions.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ic_launcher.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/ic_launcher.ai -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/SaveFileHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | public class SaveFileHelper { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-small/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 55dp 3 | 70dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-large/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 100dp 3 | 115dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_record_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable-xxxhdpi/ic_record_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/partial_select_image_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/matlog/master/app/src/main/res/drawable/partial_select_image_cropped.png -------------------------------------------------------------------------------- /app/src/androidTest/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MatLog 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/Function.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | public interface Function { 4 | 5 | T apply(E input); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/Callback.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | public interface Callback { 4 | 5 | void onCallback(T object); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-xlarge/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 170dp 4 | 190dp 5 | 6 | 20dp 7 | 8 | 60dp 9 | 65dp 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/intents/Intents.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.intents; 2 | 3 | public class Intents { 4 | 5 | public static final String ACTION_LAUNCH = "com.pluscubed.logcat.intents.LAUNCH"; 6 | public static final String EXTRA_FILTER = "filter"; 7 | public static final String EXTRA_LEVEL = "level"; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/xml/recording_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/Util.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Util 7 | */ 8 | public class Util { 9 | public static int convertDpToPx(Context context, float dp) { 10 | return (int) (dp * context.getResources().getDisplayMetrics().density 11 | + 0.5f); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_less_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_list_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/reader/AbsLogcatReader.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.reader; 2 | 3 | 4 | public abstract class AbsLogcatReader implements LogcatReader { 5 | 6 | protected boolean recordingMode; 7 | 8 | public AbsLogcatReader(boolean recordingMode) { 9 | this.recordingMode = recordingMode; 10 | } 11 | 12 | public boolean isRecordingMode() { 13 | return recordingMode; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insert_drive_file_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_new_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/raw/about_css.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #424242; 3 | color: #fff; 4 | } 5 | 6 | table { 7 | margin : auto; 8 | border-collapse:separate; 9 | border-spacing:10px; 10 | } 11 | 12 | a:link { 13 | color: #0099dd; 14 | } 15 | a:visited : { 16 | color: #0099dd; 17 | } 18 | h1 { 19 | font-size : 1.2em; 20 | font-weight : bold; 21 | text-align : center; 22 | } 23 | h2 { 24 | font-size : 1.1em; 25 | font-weight : bold; 26 | text-align : center; 27 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/raw/about_body.htm: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |

Version %1$s

7 | 8 |
9 | 10 |

Credits

11 | 12 |

Developed by Daniel Ciao (plusCubed). 13 | 14 |

Based on CatLog by Nolan Lawson. 16 | 17 |

MatLog is open source on GitHub.

18 | 19 | %2$s -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_view_agenda_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/FilterQueryWithLevel.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | public class FilterQueryWithLevel { 4 | 5 | private String filterQuery; 6 | private String logLevel; 7 | 8 | public FilterQueryWithLevel(String filterQuery, String logLevel) { 9 | this.filterQuery = filterQuery; 10 | this.logLevel = logLevel; 11 | } 12 | 13 | public String getFilterQuery() { 14 | return filterQuery; 15 | } 16 | 17 | public String getLogLevel() { 18 | return logLevel; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/StopWatch.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | public class StopWatch { 4 | 5 | private long startTime; 6 | private String name; 7 | 8 | public StopWatch(String name) { 9 | if (UtilLogger.DEBUG_MODE) { 10 | this.name = name; 11 | this.startTime = System.currentTimeMillis(); 12 | } 13 | } 14 | 15 | public void log(UtilLogger log) { 16 | if (UtilLogger.DEBUG_MODE) { 17 | log.d("%s took %d ms", name, (System.currentTimeMillis() - startTime)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/SavedLog.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import java.util.List; 4 | 5 | public class SavedLog { 6 | private List logLines; 7 | private boolean truncated; 8 | 9 | public List getLogLines() { 10 | return logLines; 11 | } 12 | 13 | public void setLogLines(List logLines) { 14 | this.logLines = logLines; 15 | } 16 | 17 | public boolean isTruncated() { 18 | return truncated; 19 | } 20 | 21 | public void setTruncated(boolean truncated) { 22 | this.truncated = truncated; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 8px 3 | 10px 4 | 12px 5 | 14px 6 | 16px 7 | 8 | 15dp 9 | 70dp 10 | 85dp 11 | 12 | 47dp 13 | 52dp 14 | 15 | 0dp 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/reader/LogcatReader.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.reader; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | public interface LogcatReader { 7 | 8 | /** 9 | * Read a single log line, ala BufferedReader.readLine(). 10 | * 11 | * @return 12 | * @throws IOException 13 | */ 14 | String readLine() throws IOException; 15 | 16 | /** 17 | * Kill the reader and close all resources without throwing any exceptions. 18 | */ 19 | void killQuietly(); 20 | 21 | boolean readyToRecord(); 22 | 23 | List getProcesses(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_send_log.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar_stub.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/ArrayUtil.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | import java.lang.reflect.Array; 4 | import java.util.List; 5 | 6 | public class ArrayUtil { 7 | 8 | public static int indexOf(T[] array, T object) { 9 | for (int i = 0; i < array.length; i++) { 10 | if (object.equals(array[i])) { 11 | return i; 12 | } 13 | } 14 | return -1; 15 | } 16 | 17 | 18 | public static T[] toArray(List list, Class clazz) { 19 | @SuppressWarnings("unchecked") 20 | T[] result = (T[]) Array.newInstance(clazz, list.size()); 21 | for (int i = 0; i < list.size(); i++) { 22 | result[i] = list.get(i); 23 | } 24 | return result; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/PackageHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager.NameNotFoundException; 5 | 6 | import com.pluscubed.logcat.util.UtilLogger; 7 | 8 | public class PackageHelper { 9 | 10 | private static UtilLogger log = new UtilLogger(PackageHelper.class); 11 | 12 | public static boolean isCatlogDonateInstalled(Context context) { 13 | return true; 14 | } 15 | 16 | public static String getVersionName(Context context) { 17 | try { 18 | return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; 19 | } catch (NameNotFoundException e) { 20 | // should never happen 21 | log.d(e, "unexpected exception"); 22 | return ""; 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/widget/NoPopupDialogPreference.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.widget; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.preference.DialogPreference; 6 | import android.util.AttributeSet; 7 | 8 | /** 9 | * DialogPreference that shows the little "down" arrow but doesn't actually pop up a dialog window. Useful for when you 10 | * want to just attach a custom onPreferenceClick action to a Preference. 11 | * 12 | * @author nlawson 13 | */ 14 | public class NoPopupDialogPreference extends DialogPreference { 15 | 16 | public NoPopupDialogPreference(Context context, AttributeSet attrs, int defStyle) { 17 | super(context, attrs, defStyle); 18 | } 19 | 20 | public NoPopupDialogPreference(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | @Override 25 | protected void showDialog(Bundle state) { 26 | // do nothing 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following` 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keepattributes SourceFile,LineNumberTable 20 | -keep class com.crashlytics.** { *; } 21 | -dontwarn com.crashlytics.** 22 | 23 | -keep class android.support.v7.internal.view.menu.MenuBuilder {*;} 24 | -keep class android.support.v7.widget.SearchView {*;} 25 | -keep class com.pluscubed.logcat.RecordingWidgetProvider {*;} -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/LogLineViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | import com.pluscubed.logcat.R; 7 | 8 | /** 9 | * Improves performance of the ListView. Watch Romain Guy's video about ListView to learn more. 10 | * 11 | * @author nlawson 12 | */ 13 | public class LogLineViewHolder { 14 | 15 | View view; 16 | TextView levelTextView; 17 | TextView outputTextView; 18 | TextView tagTextView; 19 | TextView pidTextView; 20 | TextView timestampTextView; 21 | 22 | public LogLineViewHolder(View view) { 23 | this.view = view; 24 | pidTextView = (TextView) view.findViewById(R.id.pid_text); 25 | timestampTextView = (TextView) view.findViewById(R.id.timestamp_text); 26 | tagTextView = (TextView) view.findViewById(R.id.tag_text); 27 | levelTextView = (TextView) view.findViewById(R.id.log_level_text); 28 | outputTextView = (TextView) view.findViewById(R.id.log_output_text); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/BitmapHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Bitmap.Config; 6 | import android.graphics.Canvas; 7 | import android.graphics.Rect; 8 | import android.graphics.drawable.Drawable; 9 | 10 | public class BitmapHelper { 11 | 12 | private static int sIconSize = -1; 13 | 14 | public static Bitmap convertIconToBitmap(Context context, Drawable drawable) { 15 | 16 | if (sIconSize == -1) { 17 | sIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); 18 | } 19 | 20 | return toBitmap(drawable, sIconSize, sIconSize); 21 | } 22 | 23 | private static Bitmap toBitmap(Drawable drawable, int width, int height) { 24 | 25 | Bitmap bmp = Bitmap.createBitmap(width, height, Config.ARGB_8888); 26 | Canvas c = new Canvas(bmp); 27 | drawable.setBounds(new Rect(0, 0, width, height)); 28 | drawable.draw(c); 29 | 30 | return bmp; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_delete_logfiles.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/widget/NonnegativeIntegerEditTextPreference.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.widget; 2 | 3 | import android.content.Context; 4 | import android.preference.EditTextPreference; 5 | import android.text.method.DigitsKeyListener; 6 | import android.util.AttributeSet; 7 | 8 | /** 9 | * EditTextPreference that only allows inputting integer numbers. 10 | * 11 | * @author nlawson 12 | */ 13 | public class NonnegativeIntegerEditTextPreference extends EditTextPreference { 14 | 15 | public NonnegativeIntegerEditTextPreference(Context context, AttributeSet attrs, int defStyle) { 16 | super(context, attrs, defStyle); 17 | setUpEditText(); 18 | } 19 | 20 | public NonnegativeIntegerEditTextPreference(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | setUpEditText(); 23 | } 24 | 25 | public NonnegativeIntegerEditTextPreference(Context context) { 26 | super(context); 27 | setUpEditText(); 28 | } 29 | 30 | private void setUpEditText() { 31 | getEditText().setKeyListener(DigitsKeyListener.getInstance(false, false)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_recording_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 18 | 19 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_partial_save_help.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_header_add_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/raw/translations.htm: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Translations

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
DutchWim N (wimaen)
FrenchNolan Lawson
Germancalav3ra
JapaneseNolan Lawson
Tachiken
PortugueseGonçalo Matos
RussianМаксим Ногин
SwedishYraFyra
47 | 48 |
-------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/widget/ExceptionCatchingListView.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.widget; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | import android.widget.ListView; 8 | 9 | import com.pluscubed.logcat.util.UtilLogger; 10 | 11 | public class ExceptionCatchingListView extends ListView { 12 | 13 | private static UtilLogger log = new UtilLogger(ExceptionCatchingListView.class); 14 | 15 | public ExceptionCatchingListView(Context context, AttributeSet attrs, 16 | int defStyle) { 17 | super(context, attrs, defStyle); 18 | } 19 | 20 | public ExceptionCatchingListView(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public ExceptionCatchingListView(Context context) { 25 | super(context); 26 | } 27 | 28 | @Override 29 | public boolean onTouchEvent(@NonNull MotionEvent ev) { 30 | try { 31 | return super.onTouchEvent(ev); 32 | } catch (Exception e) { 33 | log.d(e, ""); 34 | return false; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/db/FilterItem.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.db; 2 | 3 | import java.util.Comparator; 4 | 5 | 6 | public class FilterItem implements Comparable { 7 | 8 | public static final Comparator DEFAULT_COMPARATOR = new Comparator() { 9 | 10 | @Override 11 | public int compare(FilterItem lhs, FilterItem rhs) { 12 | String leftText = lhs.text != null ? lhs.text : ""; 13 | String rightText = rhs.text != null ? rhs.text : ""; 14 | return leftText.compareToIgnoreCase(rightText); 15 | } 16 | }; 17 | private int id; 18 | private String text; 19 | 20 | private FilterItem() { 21 | } 22 | 23 | public static FilterItem create(int id, String text) { 24 | FilterItem filterItem = new FilterItem(); 25 | filterItem.id = id; 26 | filterItem.text = text; 27 | return filterItem; 28 | } 29 | 30 | public String getText() { 31 | return text; 32 | } 33 | 34 | public int getId() { 35 | return id; 36 | } 37 | 38 | @Override 39 | public int compareTo(FilterItem another) { 40 | return DEFAULT_COMPARATOR.compare(this, another); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_dropdown.xml: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/VersionHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.os.Build; 4 | 5 | import java.lang.reflect.Field; 6 | 7 | /** 8 | * Determine what version of Android the device is running. 9 | * 10 | * @author nolan 11 | */ 12 | public class VersionHelper { 13 | 14 | public static final int VERSION_CUPCAKE = 3; 15 | public static final int VERSION_DONUT = 4; 16 | public static final int VERSION_FROYO = 8; 17 | public static final int VERSION_JELLYBEAN = 16; 18 | 19 | private static Field sdkIntField = null; 20 | private static boolean fetchedSdkIntField = false; 21 | 22 | public static int getVersionSdkIntCompat() { 23 | try { 24 | Field field = getSdkIntField(); 25 | if (field != null) { 26 | return (Integer) field.get(null); 27 | } 28 | } catch (IllegalAccessException ignore) { 29 | // ignore 30 | } 31 | return VERSION_CUPCAKE; // cupcake 32 | } 33 | 34 | private static Field getSdkIntField() { 35 | if (!fetchedSdkIntField) { 36 | try { 37 | sdkIntField = Build.VERSION.class.getField("SDK_INT"); 38 | } catch (NoSuchFieldException ignore) { 39 | // ignore 40 | } 41 | fetchedSdkIntField = true; 42 | } 43 | return sdkIntField; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/SendLogDetails.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import java.io.File; 4 | 5 | public class SendLogDetails { 6 | 7 | private String subject; 8 | private String body; 9 | private File attachment; 10 | private SendLogDetails.AttachmentType attachmentType; 11 | 12 | public String getSubject() { 13 | return subject; 14 | } 15 | 16 | public void setSubject(String subject) { 17 | this.subject = subject; 18 | } 19 | 20 | public String getBody() { 21 | return body; 22 | } 23 | 24 | public void setBody(String body) { 25 | this.body = body; 26 | } 27 | 28 | public File getAttachment() { 29 | return attachment; 30 | } 31 | 32 | public void setAttachment(File attachment) { 33 | this.attachment = attachment; 34 | } 35 | 36 | public SendLogDetails.AttachmentType getAttachmentType() { 37 | return attachmentType; 38 | } 39 | 40 | public void setAttachmentType(SendLogDetails.AttachmentType attachmentType) { 41 | this.attachmentType = attachmentType; 42 | } 43 | 44 | public enum AttachmentType { 45 | None("text/plain"), 46 | Zip("application/zip"), 47 | Text("application/*"); 48 | 49 | private String mimeType; 50 | 51 | AttachmentType(String mimeType) { 52 | this.mimeType = mimeType; 53 | } 54 | 55 | public String getMimeType() { 56 | return this.mimeType; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/CrazyLoggerService.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat; 2 | 3 | import android.app.IntentService; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | 7 | import com.pluscubed.logcat.util.UtilLogger; 8 | 9 | import java.util.Date; 10 | 11 | /** 12 | * just writes a bunch of logs. to be used during debugging and testing. 13 | * 14 | * @author nolan 15 | */ 16 | public class CrazyLoggerService extends IntentService { 17 | 18 | private static final long INTERVAL = 300; 19 | 20 | private static UtilLogger log = new UtilLogger(CrazyLoggerService.class); 21 | 22 | private boolean kill = false; 23 | 24 | public CrazyLoggerService() { 25 | super("CrazyLoggerService"); 26 | } 27 | 28 | protected void onHandleIntent(Intent intent) { 29 | 30 | log.d("onHandleIntent()"); 31 | 32 | while (!kill) { 33 | 34 | try { 35 | Thread.sleep(INTERVAL); 36 | } catch (InterruptedException e) { 37 | log.e(e, "error"); 38 | } 39 | Date date = new Date(); 40 | log.i("Log message " + date + " " + (date.getTime() % 1000)); 41 | 42 | } 43 | 44 | } 45 | 46 | @Override 47 | public void onStart(Intent intent, int startId) { 48 | super.onStart(intent, startId); 49 | } 50 | 51 | @Override 52 | public IBinder onBind(Intent intent) { 53 | return null; 54 | } 55 | 56 | @Override 57 | public void onDestroy() { 58 | super.onDestroy(); 59 | kill = true; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 26 | 27 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_recording.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 18 | 19 | 24 | 25 | 34 | 35 | 36 | 37 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/UtilLogger.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | import android.util.Log; 4 | 5 | import com.pluscubed.logcat.BuildConfig; 6 | 7 | import java.util.Arrays; 8 | 9 | /** 10 | * Easier way to interact with logcat. 11 | * 12 | * @author nolan 13 | */ 14 | public class UtilLogger { 15 | 16 | public static final boolean DEBUG_MODE = BuildConfig.DEBUG; 17 | 18 | private String tag; 19 | 20 | public UtilLogger(String tag) { 21 | this.tag = tag; 22 | } 23 | 24 | public UtilLogger(Class clazz) { 25 | this.tag = clazz.getSimpleName(); 26 | } 27 | 28 | public void i(String format, Object... more) { 29 | Log.i(tag, String.format(format, more)); 30 | } 31 | 32 | public void i(Exception e, String format, Object... more) { 33 | Log.i(tag, String.format(format, more), e); 34 | } 35 | 36 | public void w(Exception e, String format, Object... more) { 37 | Log.w(tag, String.format(format, more), e); 38 | } 39 | 40 | public void w(String format, Object... more) { 41 | Log.w(tag, String.format(format, more)); 42 | } 43 | 44 | public void e(String format, Object... more) { 45 | Log.e(tag, String.format(format, more)); 46 | } 47 | 48 | public void e(Exception e, String format, Object... more) { 49 | Log.e(tag, String.format(format, more), e); 50 | } 51 | 52 | public void d(String format, Object... more) { 53 | if (DEBUG_MODE) { 54 | for (int i = 0; i < more.length; i++) { 55 | if (more[i] instanceof int[]) { 56 | more[i] = Arrays.toString((int[]) more[i]); 57 | } 58 | } 59 | Log.d(tag, String.format(format, more)); 60 | } 61 | } 62 | 63 | public void d(Exception e, String format, Object... more) { 64 | if (DEBUG_MODE) { 65 | for (int i = 0; i < more.length; i++) { 66 | if (more[i] instanceof int[]) { 67 | more[i] = Arrays.toString((int[]) more[i]); 68 | } 69 | } 70 | Log.d(tag, String.format(format, more), e); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/RuntimeHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.pluscubed.logcat.util.ArrayUtil; 6 | 7 | import java.io.BufferedOutputStream; 8 | import java.io.IOException; 9 | import java.io.PrintStream; 10 | import java.util.List; 11 | 12 | /** 13 | * Helper functions for running processes. 14 | * 15 | * @author nolan 16 | */ 17 | public class RuntimeHelper { 18 | 19 | /** 20 | * Exec the arguments, using root if necessary. 21 | * 22 | * @param args 23 | */ 24 | public static Process exec(List args) throws IOException { 25 | // since JellyBean, sudo is required to read other apps' logs 26 | if (VersionHelper.getVersionSdkIntCompat() >= VersionHelper.VERSION_JELLYBEAN 27 | && !SuperUserHelper.isFailedToObtainRoot()) { 28 | Process process = Runtime.getRuntime().exec("su"); 29 | 30 | PrintStream outputStream = null; 31 | try { 32 | outputStream = new PrintStream(new BufferedOutputStream(process.getOutputStream(), 8192)); 33 | outputStream.println(TextUtils.join(" ", args)); 34 | outputStream.flush(); 35 | } finally { 36 | if (outputStream != null) { 37 | outputStream.close(); 38 | } 39 | } 40 | 41 | return process; 42 | } 43 | return Runtime.getRuntime().exec(ArrayUtil.toArray(args, String.class)); 44 | } 45 | 46 | public static void destroy(Process process) { 47 | // if we're in JellyBean, then we need to kill the process as root, which requires all this 48 | // extra UnixProcess logic 49 | if (VersionHelper.getVersionSdkIntCompat() >= VersionHelper.VERSION_JELLYBEAN 50 | && !SuperUserHelper.isFailedToObtainRoot()) { 51 | SuperUserHelper.destroy(process); 52 | } else { 53 | process.destroy(); 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_logfilename_multi.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_logfilename_single.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/FilterAdapter.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.View.OnClickListener; 7 | import android.view.ViewGroup; 8 | import android.widget.ArrayAdapter; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.pluscubed.logcat.R; 13 | import com.pluscubed.logcat.db.CatlogDBHelper; 14 | import com.pluscubed.logcat.db.FilterItem; 15 | 16 | import java.util.List; 17 | 18 | public class FilterAdapter extends ArrayAdapter { 19 | 20 | public FilterAdapter(Context context, List items) { 21 | super(context, R.layout.list_item_filter, items); 22 | } 23 | 24 | @Override 25 | public View getView(int position, View convertView, ViewGroup parent) { 26 | if (convertView == null) { 27 | LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 28 | convertView = inflater.inflate(R.layout.list_item_filter, parent, false); 29 | } 30 | 31 | final FilterItem filterItem = getItem(position); 32 | 33 | TextView textView = (TextView) convertView.findViewById(android.R.id.text1); 34 | textView.setText(filterItem.getText()); 35 | // add listener to the delete button 36 | ImageView button = (ImageView) convertView.findViewById(android.R.id.button1); 37 | button.setOnClickListener(new OnClickListener() { 38 | @Override 39 | public void onClick(View v) { 40 | //delete 41 | CatlogDBHelper dbHelper = null; 42 | try { 43 | dbHelper = new CatlogDBHelper(getContext()); 44 | dbHelper.deleteFilter(filterItem.getId()); 45 | } finally { 46 | if (dbHelper != null) { 47 | dbHelper.close(); 48 | } 49 | } 50 | remove(filterItem); 51 | notifyDataSetChanged(); 52 | } 53 | }); 54 | 55 | return convertView; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/BuildHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.os.Build; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Map.Entry; 9 | import java.util.SortedMap; 10 | import java.util.TreeMap; 11 | 12 | public class BuildHelper { 13 | 14 | // public static final Strings of android.os.Build 15 | private static final List BUILD_FIELDS = Arrays.asList( 16 | "BOARD", "BOOTLOADER", "BRAND", "CPU_ABI", "CPU_ABI2", 17 | "DEVICE", "DISPLAY", "FINGERPRINT", "HARDWARE", "HOST", 18 | "ID", "MANUFACTURER", "MODEL", "PRODUCT", "RADIO", 19 | "SERIAL", "TAGS", "TIME", "TYPE", "USER"); 20 | 21 | // public static final Strings of android.os.Build.Version 22 | private static final List BUILD_VERSION_FIELDS = Arrays.asList( 23 | "CODENAME", "INCREMENTAL", "RELEASE", "SDK_INT"); 24 | 25 | public static String getBuildInformationAsString() { 26 | SortedMap keysToValues = new TreeMap(); 27 | 28 | for (String buildField : BUILD_FIELDS) { 29 | putKeyValue(Build.class, buildField, keysToValues); 30 | } 31 | for (String buildVersionField : BUILD_VERSION_FIELDS) { 32 | putKeyValue(Build.VERSION.class, buildVersionField, keysToValues); 33 | } 34 | 35 | StringBuilder stringBuilder = new StringBuilder(); 36 | for (Entry entry : keysToValues.entrySet()) { 37 | stringBuilder.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); 38 | } 39 | return stringBuilder.toString(); 40 | } 41 | 42 | private static void putKeyValue(Class clazz, String buildField, SortedMap keysToValues) { 43 | try { 44 | Field field = clazz.getField(buildField); 45 | Object value = field.get(null); 46 | String key = clazz.getSimpleName().toLowerCase() + "." + buildField.toLowerCase(); 47 | keysToValues.put(key, String.valueOf(value)); 48 | } catch (SecurityException | NoSuchFieldException | IllegalAccessException e) { 49 | // ignore 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Icon](./app/src/main/res/drawable-hdpi/ic_launcher.png)MatLog 2 | ========= 3 | It's CatLog, but with material goodness. 4 | 5 | Graphical log reader for Android. 6 | 7 | [![Get it on Google Play](http://i.imgur.com/MIXbzVC.png)](https://play.google.com/store/apps/details?id=com.pluscubed.matlog) 8 | 9 | Based on Nolan Lawson's CatLog: [Google Play][1], [GitHub][2] 10 | 11 | Discussions: [Google+ Community][3] 12 | 13 | Overview 14 | --------- 15 | MatLog is a free and open-source material-style log reader for Android based on CatLog. 16 | 17 | It shows a scrolling (tailed) view of the Android "logcat" system log, 18 | hence the goofy name. It also allows you to record logs in real time, send logs via email, 19 | and filter using a variety of criteria. 20 | 21 | FAQs 22 | ------------- 23 | Taken from CatLog's FAQ: 24 | 25 | #### Where are the logs saved? 26 | 27 | On the SD card, under ```/sdcard/catlog/saved_logs/```. 28 | 29 | #### I can't see any logs! 30 | 31 | This problem typically shows up on custom ROMs. First off, try an alternative logging app, to verify that 32 | the problem is with your ROM and not MatLog. 33 | 34 | Next, see if your ROM offers system-wide settings to disable logging. Be sure to reboot after you change anything. 35 | 36 | If that still doesn't work, you can contact the creator of your ROM to file a bug/RFE. 37 | 38 | License 39 | --------- 40 | ``` 41 | Copyright (C) 2016 Daniel Ciao 42 | 43 | This program is free software: you can redistribute it and/or modify 44 | it under the terms of the GNU General Public License as published by 45 | the Free Software Foundation, either version 3 of the License, or 46 | (at your option) any later version. 47 | 48 | This program is distributed in the hope that it will be useful, 49 | but WITHOUT ANY WARRANTY; without even the implied warranty of 50 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 51 | GNU General Public License for more details. 52 | 53 | You should have received a copy of the GNU General Public License 54 | along with this program. If not, see . 55 | 56 | ``` 57 | 58 | [1]: https://play.google.com/store/apps/details?id=com.nolanlawson.logcat 59 | [2]: https://github.com/nolanlawson/Catlog 60 | [3]: https://plus.google.com/u/0/communities/108705871773878445106 61 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'https://maven.fabric.io/public' } 4 | } 5 | 6 | dependencies { 7 | classpath 'io.fabric.tools:gradle:1.21.4' 8 | } 9 | } 10 | apply plugin: 'com.android.application' 11 | apply plugin: 'io.fabric' 12 | 13 | repositories { 14 | maven { url 'https://maven.fabric.io/public' } 15 | maven { url "https://jitpack.io" } 16 | } 17 | 18 | android { 19 | compileSdkVersion 23 20 | buildToolsVersion "23.0.2" 21 | 22 | defaultConfig { 23 | applicationId "com.pluscubed.matlog" 24 | minSdkVersion 16 25 | targetSdkVersion 23 26 | versionName '1.0.2' 27 | versionCode 4 28 | 29 | testApplicationId "com.pluscubed.matlog.test" 30 | testInstrumentationRunner "android.test.InstrumentationTestRunner" 31 | 32 | manifestPlaceholders = [appName: "@string/app_name"] 33 | 34 | vectorDrawables.useSupportLibrary = true 35 | } 36 | 37 | if (project.hasProperty("RELEASE_STORE_FILE")) { 38 | signingConfigs { 39 | release { 40 | storeFile file(RELEASE_STORE_FILE) 41 | storePassword RELEASE_STORE_PASSWORD 42 | keyAlias RELEASE_KEY_ALIAS_MATLOG 43 | keyPassword RELEASE_KEY_PASSWORD_MATLOG 44 | } 45 | } 46 | } 47 | 48 | buildTypes { 49 | release { 50 | minifyEnabled false 51 | shrinkResources true 52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 53 | if (project.hasProperty("RELEASE_STORE_FILE")) { 54 | signingConfig signingConfigs.release 55 | } else { 56 | signingConfig signingConfigs.debug 57 | } 58 | } 59 | debug { 60 | applicationIdSuffix '.debug' 61 | versionNameSuffix '-DEBUG' 62 | manifestPlaceholders = [appName: "MatLog DEBUG"] 63 | ext.enableCrashlytics = false 64 | } 65 | } 66 | } 67 | 68 | dependencies { 69 | compile 'com.android.support:appcompat-v7:23.2.1' 70 | compile 'com.android.support:support-annotations:23.2.1' 71 | compile 'com.android.support:design:23.2.1' 72 | compile 'com.afollestad.material-dialogs:core:0.8.5.6' 73 | compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') { 74 | transitive = true; 75 | } 76 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_logcat.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 15 | 16 | 24 | 25 | 33 | 34 | 44 | 45 | 46 | 58 | 59 | 63 | 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | 32 | ### OSX ### 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Icon must end with two \r 38 | Icon 39 | 40 | 41 | # Thumbnails 42 | ._* 43 | 44 | # Files that might appear on external disk 45 | .Spotlight-V100 46 | .Trashes 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | 56 | ### Java ### 57 | *.class 58 | 59 | # Mobile Tools for Java (J2ME) 60 | .mtj.tmp/ 61 | 62 | # Package Files # 63 | *.jar 64 | *.war 65 | *.ear 66 | 67 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 68 | hs_err_pid* 69 | 70 | 71 | ### Intellij ### 72 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 73 | 74 | *.iml 75 | 76 | ## Directory-based project format: 77 | .idea/ 78 | # if you remove the above rule, at least ignore the following: 79 | 80 | # User-specific stuff: 81 | # .idea/workspace.xml 82 | # .idea/tasks.xml 83 | # .idea/dictionaries 84 | 85 | # Sensitive or high-churn files: 86 | # .idea/dataSources.ids 87 | # .idea/dataSources.xml 88 | # .idea/sqlDataSources.xml 89 | # .idea/dynamic.xml 90 | # .idea/uiDesigner.xml 91 | 92 | # Gradle: 93 | # .idea/gradle.xml 94 | # .idea/libraries 95 | 96 | # Mongo Explorer plugin: 97 | # .idea/mongoSettings.xml 98 | 99 | ## File-based project format: 100 | *.ipr 101 | *.iws 102 | 103 | ## Plugin-specific files: 104 | 105 | # IntelliJ 106 | out/ 107 | 108 | # mpeltonen/sbt-idea plugin 109 | .idea_modules/ 110 | 111 | # JIRA plugin 112 | atlassian-ide-plugin.xml 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | 119 | 120 | ### Crashlytics ### 121 | #Crashlytics 122 | crashlytics-build.properties 123 | com_crashlytics_export_strings.xml 124 | fabric_apikey.xml 125 | 126 | ### Windows ### 127 | # Windows image file caches 128 | Thumbs.db 129 | ehthumbs.db 130 | 131 | # Folder config file 132 | Desktop.ini 133 | 134 | # Recycle Bin used on file shares 135 | $RECYCLE.BIN/ 136 | 137 | # Windows Installer files 138 | *.cab 139 | *.msi 140 | *.msm 141 | *.msp 142 | 143 | # Windows shortcuts 144 | *.lnk -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/LogFileAdapter.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.CheckBox; 9 | import android.widget.RadioButton; 10 | import android.widget.TextView; 11 | 12 | import com.pluscubed.logcat.R; 13 | import com.pluscubed.logcat.helper.SaveLogHelper; 14 | 15 | import java.text.DateFormat; 16 | import java.util.Date; 17 | import java.util.List; 18 | 19 | public class LogFileAdapter extends ArrayAdapter { 20 | private List objects; 21 | private int checked; 22 | private boolean multiMode; 23 | private boolean[] checkedItems; 24 | private int resId; 25 | 26 | public LogFileAdapter(Context context, List objects, int checked, boolean multiMode) { 27 | 28 | super(context, -1, objects); 29 | this.objects = objects; 30 | this.checked = checked; 31 | this.multiMode = multiMode; 32 | if (multiMode) { 33 | checkedItems = new boolean[objects.size()]; 34 | } 35 | resId = multiMode ? R.layout.list_item_logfilename_multi : R.layout.list_item_logfilename_single; 36 | } 37 | 38 | @Override 39 | public View getView(int position, View view, ViewGroup parent) { 40 | 41 | Context context = parent.getContext(); 42 | 43 | if (view == null) { 44 | LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 45 | view = inflater.inflate(resId, parent, false); 46 | } 47 | 48 | CheckBox box = (CheckBox) view.findViewById(android.R.id.checkbox); 49 | RadioButton button = (RadioButton) view.findViewById(android.R.id.button1); 50 | TextView text1 = (TextView) view.findViewById(android.R.id.text1); 51 | TextView text2 = (TextView) view.findViewById(android.R.id.text2); 52 | 53 | CharSequence filename = objects.get(position); 54 | 55 | text1.setText(filename); 56 | 57 | 58 | if (multiMode) { 59 | box.setChecked(checkedItems[position]); 60 | } else { 61 | button.setChecked(checked == position); 62 | } 63 | 64 | Date lastModified = SaveLogHelper.getLastModifiedDate(filename.toString()); 65 | DateFormat dateFormat = DateFormat.getDateTimeInstance(); 66 | 67 | text2.setText(dateFormat.format(lastModified)); 68 | 69 | return view; 70 | } 71 | 72 | public void checkOrUncheck(int position) { 73 | checkedItems[position] = !checkedItems[position]; 74 | notifyDataSetChanged(); 75 | } 76 | 77 | public boolean[] getCheckedItems() { 78 | return checkedItems; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_searchby.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 28 | 29 | 40 | 41 | 42 | 43 | 51 | 52 | 60 | 61 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/RecordingWidgetProvider.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.appwidget.AppWidgetProvider; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.support.annotation.NonNull; 8 | 9 | import com.pluscubed.logcat.helper.DialogHelper; 10 | import com.pluscubed.logcat.helper.PreferenceHelper; 11 | import com.pluscubed.logcat.helper.ServiceHelper; 12 | import com.pluscubed.logcat.helper.WidgetHelper; 13 | import com.pluscubed.logcat.ui.RecordLogDialogActivity; 14 | import com.pluscubed.logcat.util.UtilLogger; 15 | 16 | import java.util.Arrays; 17 | 18 | public class RecordingWidgetProvider extends AppWidgetProvider { 19 | 20 | public static final String ACTION_RECORD_OR_STOP = BuildConfig.APPLICATION_ID + ".action.RECORD_OR_STOP"; 21 | 22 | public static final String URI_SCHEME = "catlog_widget"; 23 | 24 | private static UtilLogger log = new UtilLogger(RecordingWidgetProvider.class); 25 | 26 | @Override 27 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 28 | super.onUpdate(context, appWidgetManager, appWidgetIds); 29 | log.d("onUpdate() for appWidgetIds %s", Arrays.toString(appWidgetIds)); 30 | log.d("appWidgetIds are %s", Arrays.toString(appWidgetIds)); 31 | 32 | // track which widgets were created, since there's a bug in the android system that lets 33 | // stale app widget ids stick around 34 | PreferenceHelper.setWidgetExistsPreference(context, appWidgetIds); 35 | 36 | WidgetHelper.updateWidgets(context, appWidgetIds); 37 | } 38 | 39 | @Override 40 | public void onReceive(@NonNull final Context context, @NonNull Intent intent) { 41 | super.onReceive(context, intent); 42 | log.d("onReceive(); intent is: %s", intent); 43 | 44 | if (ACTION_RECORD_OR_STOP.equals(intent.getAction())) { 45 | 46 | // start or stop recording as necessary 47 | synchronized (RecordingWidgetProvider.class) { 48 | 49 | boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); 50 | 51 | if (alreadyRunning) { 52 | // stop the current recording process 53 | DialogHelper.stopRecordingLog(context); 54 | } else { 55 | // start a new recording process 56 | Intent targetIntent = new Intent(); 57 | targetIntent.setClass(context, RecordLogDialogActivity.class); 58 | targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 59 | | Intent.FLAG_ACTIVITY_MULTIPLE_TASK 60 | | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 61 | 62 | context.startActivity(targetIntent); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/LogcatHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import com.pluscubed.logcat.util.UtilLogger; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | public class LogcatHelper { 13 | 14 | public static final String BUFFER_MAIN = "main"; 15 | public static final String BUFFER_EVENTS = "events"; 16 | public static final String BUFFER_RADIO = "radio"; 17 | 18 | private static UtilLogger log = new UtilLogger(LogcatHelper.class); 19 | 20 | public static Process getLogcatProcess(String buffer) throws IOException { 21 | 22 | List args = getLogcatArgs(buffer); 23 | Process process = RuntimeHelper.exec(args); 24 | 25 | return process; 26 | } 27 | 28 | private static List getLogcatArgs(String buffer) { 29 | List args = new ArrayList(Arrays.asList("logcat", "-v", "time")); 30 | 31 | // for some reason, adding -b main excludes log output from AndroidRuntime runtime exceptions, 32 | // whereas just leaving it blank keeps them in. So do not specify the buffer if it is "main" 33 | if (!buffer.equals(BUFFER_MAIN)) { 34 | args.add("-b"); 35 | args.add(buffer); 36 | } 37 | 38 | return args; 39 | } 40 | 41 | public static String getLastLogLine(String buffer) { 42 | Process dumpLogcatProcess = null; 43 | BufferedReader reader = null; 44 | String result = null; 45 | try { 46 | 47 | List args = getLogcatArgs(buffer); 48 | args.add("-d"); // -d just dumps the whole thing 49 | 50 | dumpLogcatProcess = RuntimeHelper.exec(args); 51 | reader = new BufferedReader(new InputStreamReader(dumpLogcatProcess 52 | .getInputStream()), 8192); 53 | 54 | String line; 55 | while ((line = reader.readLine()) != null) { 56 | result = line; 57 | } 58 | } catch (IOException e) { 59 | log.e(e, "unexpected exception"); 60 | } finally { 61 | if (dumpLogcatProcess != null) { 62 | RuntimeHelper.destroy(dumpLogcatProcess); 63 | log.d("destroyed 1 dump logcat process"); 64 | } 65 | // post-jellybean, we just kill the process, so there's no need 66 | // to close the bufferedReader. Anyway, it just hangs. 67 | if (VersionHelper.getVersionSdkIntCompat() < VersionHelper.VERSION_JELLYBEAN 68 | && reader != null) { 69 | try { 70 | reader.close(); 71 | } catch (IOException e) { 72 | log.e(e, "unexpected exception"); 73 | } 74 | } 75 | } 76 | 77 | return result; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/pluscubed/logcat/test/LogFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.test; 2 | 3 | import android.test.ActivityInstrumentationTestCase2; 4 | 5 | import com.pluscubed.logcat.data.LogLine; 6 | import com.pluscubed.logcat.data.SearchCriteria; 7 | import com.pluscubed.logcat.ui.LogcatActivity; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | public class LogFilterTest extends ActivityInstrumentationTestCase2 { 14 | 15 | private static final List TEST_LOG_LINES = Arrays 16 | .asList("07-21 21:34:09.900 D/dalvikvm( 257): GC_CONCURRENT freed 385K, 50% free 3190K/6279K, external 0K/0K, paused 2ms+2ms", 17 | "07-21 21:34:10.050 D/Vold ( 114): USB connected", 18 | "07-21 21:34:10.050 D/Vold ( 114): Share method ums now available", 19 | "07-21 21:34:10.050 I/StorageNotification( 244): UMS connection changed to true (media state mounted)", 20 | "07-21 21:34:10.080 D/Vold ( 114): USB connected", 21 | "07-21 21:34:10.090 D/Tethering( 167): sendTetherStateChangedBroadcast 1, 0, 0", 22 | "07-21 21:34:10.090 D/Tethering( 167): interfaceAdded :usb0", 23 | "07-21 21:34:10.100 D/BluetoothNetworkService( 167): updating tether state", 24 | "07-21 21:34:10.100 D/BluetoothNetworkService( 167): interface usb0", 25 | "07-21 21:34:10.110 W/Tethering( 167): active iface (usb0) reported as added, ignoring", 26 | "07-21 21:34:10.110 W/Changelog Droid( 123): blah blah blah"); 27 | 28 | 29 | public LogFilterTest() { 30 | super("com.pluscubed.logcat", LogcatActivity.class); 31 | } 32 | 33 | public void testFilterBasic() { 34 | testFilter("dalvik", 1); 35 | testFilter("tethering", 3); 36 | testFilter("114", 3); 37 | testFilter("usb connected", 2); 38 | testFilter("connected usb", 0); 39 | testFilter("pid:167", 5); 40 | testFilter("tag:Vold", 3); 41 | } 42 | 43 | public void testFilterTagWithSpaces() { 44 | testFilter("changelog droid", 1); 45 | testFilter("changelog", 1); 46 | testFilter("tag:changelog", 1); 47 | testFilter("tag:\"changelog\"", 1); 48 | testFilter("tag:\"changelog droid\"", 1); 49 | 50 | testFilter("tag:changelog foobar", 0); 51 | testFilter("tag:changelog droid", 1); 52 | 53 | } 54 | 55 | private void testFilter(String text, int expectedLogLines) { 56 | SearchCriteria criteria = new SearchCriteria(text); 57 | List matches = new ArrayList(); 58 | for (String line : TEST_LOG_LINES) { 59 | if (criteria.matches(LogLine.newLogLine(line, false))) { 60 | matches.add(line); 61 | } 62 | } 63 | assertEquals(expectedLogLines, matches.size()); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_logcat.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 38 | 39 | 53 | 54 | 66 | 67 | 68 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/reader/LogcatReaderLoader.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.reader; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import com.pluscubed.logcat.helper.LogcatHelper; 9 | import com.pluscubed.logcat.helper.PreferenceHelper; 10 | 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Map.Entry; 16 | 17 | public class LogcatReaderLoader implements Parcelable { 18 | 19 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 20 | public LogcatReaderLoader createFromParcel(Parcel in) { 21 | return new LogcatReaderLoader(in); 22 | } 23 | 24 | public LogcatReaderLoader[] newArray(int size) { 25 | return new LogcatReaderLoader[size]; 26 | } 27 | }; 28 | private Map lastLines = new HashMap<>(); 29 | private boolean recordingMode; 30 | private boolean multiple; 31 | 32 | private LogcatReaderLoader(Parcel in) { 33 | this.recordingMode = in.readInt() == 1; 34 | this.multiple = in.readInt() == 1; 35 | Bundle bundle = in.readBundle(); 36 | for (String key : bundle.keySet()) { 37 | lastLines.put(key, bundle.getString(key)); 38 | } 39 | } 40 | 41 | private LogcatReaderLoader(List buffers, boolean recordingMode) { 42 | this.recordingMode = recordingMode; 43 | this.multiple = buffers.size() > 1; 44 | for (String buffer : buffers) { 45 | // no need to grab the last line if this isn't recording mode 46 | String lastLine = recordingMode ? LogcatHelper.getLastLogLine(buffer) : null; 47 | lastLines.put(buffer, lastLine); 48 | } 49 | } 50 | 51 | public static LogcatReaderLoader create(Context context, boolean recordingMode) { 52 | List buffers = PreferenceHelper.getBuffers(context); 53 | return new LogcatReaderLoader(buffers, recordingMode); 54 | } 55 | 56 | public LogcatReader loadReader() throws IOException { 57 | LogcatReader reader; 58 | if (!multiple) { 59 | // single reader 60 | String buffer = lastLines.keySet().iterator().next(); 61 | String lastLine = lastLines.values().iterator().next(); 62 | reader = new SingleLogcatReader(recordingMode, buffer, lastLine); 63 | } else { 64 | // multiple reader 65 | reader = new MultipleLogcatReader(recordingMode, lastLines); 66 | } 67 | 68 | return reader; 69 | } 70 | 71 | @Override 72 | public int describeContents() { 73 | return 0; 74 | } 75 | 76 | @Override 77 | public void writeToParcel(Parcel dest, int flags) { 78 | dest.writeInt(recordingMode ? 1 : 0); 79 | dest.writeInt(multiple ? 1 : 0); 80 | Bundle bundle = new Bundle(); 81 | for (Entry entry : lastLines.entrySet()) { 82 | bundle.putString(entry.getKey(), entry.getValue()); 83 | } 84 | dest.writeBundle(bundle); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/widget/MultipleChoicePreference.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.widget; 2 | 3 | import android.app.AlertDialog.Builder; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.preference.ListPreference; 7 | import android.util.AttributeSet; 8 | 9 | import com.pluscubed.logcat.util.StringUtil; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | /** 16 | * Similar to a ListPreference, but uses a multi-choice list and saves the value as a comma-separated string. 17 | * 18 | * @author nlawson 19 | */ 20 | public class MultipleChoicePreference extends ListPreference { 21 | 22 | public static final String DELIMITER = ","; 23 | boolean[] checkedDialogEntryIndexes; 24 | 25 | public MultipleChoicePreference(Context context, AttributeSet attrs) { 26 | super(context, attrs); 27 | } 28 | 29 | public MultipleChoicePreference(Context context) { 30 | super(context); 31 | } 32 | 33 | @Override 34 | protected void onPrepareDialogBuilder(Builder builder) { 35 | 36 | // convert comma-separated list to boolean array 37 | 38 | String value = getValue(); 39 | Set commaSeparated = new HashSet(Arrays.asList(StringUtil.split(value, DELIMITER))); 40 | 41 | CharSequence[] entryValues = getEntryValues(); 42 | final boolean[] checked = new boolean[entryValues.length]; 43 | for (int i = 0; i < entryValues.length; i++) { 44 | checked[i] = commaSeparated.contains(entryValues[i]); 45 | } 46 | 47 | builder.setMultiChoiceItems(getEntries(), checked, new DialogInterface.OnMultiChoiceClickListener() { 48 | 49 | @Override 50 | public void onClick(DialogInterface dialog, int which, boolean isChecked) { 51 | checked[which] = isChecked; 52 | } 53 | }); 54 | builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 55 | 56 | @Override 57 | public void onClick(DialogInterface dialog, int which) { 58 | 59 | checkedDialogEntryIndexes = checked; 60 | 61 | /* 62 | * Clicking on an item simulates the positive button 63 | * click, and dismisses the dialog. 64 | */ 65 | MultipleChoicePreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); 66 | dialog.dismiss(); 67 | 68 | } 69 | }); 70 | } 71 | 72 | @Override 73 | protected void onDialogClosed(boolean positiveResult) { 74 | 75 | if (positiveResult && checkedDialogEntryIndexes != null) { 76 | String value = createValueAsString(checkedDialogEntryIndexes); 77 | if (callChangeListener(value)) { 78 | setValue(value); 79 | } 80 | } 81 | } 82 | 83 | private String createValueAsString(boolean[] checked) { 84 | StringBuilder sb = new StringBuilder(); 85 | 86 | for (int i = 0; i < checked.length; i++) { 87 | if (checked[i]) { 88 | sb.append(getEntryValues()[i]).append(DELIMITER); 89 | } 90 | } 91 | return sb.length() == 0 ? "" : sb.substring(0, sb.length() - 1); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/SearchCriteria.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.pluscubed.logcat.util.StringUtil; 6 | 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class SearchCriteria { 11 | 12 | public static final String PID_KEYWORD = "pid:"; 13 | public static final String TAG_KEYWORD = "tag:"; 14 | 15 | private static final Pattern PID_PATTERN = Pattern.compile("pid:(\\d+)", Pattern.CASE_INSENSITIVE); 16 | private static final Pattern TAG_PATTERN = Pattern.compile("tag:(\"[^\"]+\"|\\S+)", Pattern.CASE_INSENSITIVE); 17 | 18 | private int pid = -1; 19 | private String tag; 20 | private String searchText; 21 | private int searchTextAsInt = -1; 22 | 23 | public SearchCriteria(CharSequence inputQuery) { 24 | 25 | // check for the "pid" keyword 26 | 27 | StringBuilder query = new StringBuilder(StringUtil.nullToEmpty(inputQuery)); 28 | Matcher pidMatcher = PID_PATTERN.matcher(query); 29 | if (pidMatcher.find()) { 30 | try { 31 | pid = Integer.parseInt(pidMatcher.group(1)); 32 | query.replace(pidMatcher.start(), pidMatcher.end(), ""); // remove 33 | // from 34 | // search 35 | // string 36 | } catch (NumberFormatException ignore) { 37 | } 38 | } 39 | 40 | // check for the "tag" keyword 41 | 42 | Matcher tagMatcher = TAG_PATTERN.matcher(query); 43 | if (tagMatcher.find()) { 44 | tag = tagMatcher.group(1); 45 | if (tag.startsWith("\"") && tag.endsWith("\"")) { 46 | tag = tag.substring(1, tag.length() - 1); // remove quotes 47 | } 48 | query.replace(tagMatcher.start(), tagMatcher.end(), ""); // remove 49 | // from 50 | // search 51 | // string 52 | } 53 | 54 | // everything else becomes a search term 55 | searchText = query.toString().trim(); 56 | 57 | try { 58 | searchTextAsInt = Integer.parseInt(searchText); 59 | } catch (NumberFormatException ignore) { 60 | } 61 | 62 | } 63 | 64 | public boolean isEmpty() { 65 | return pid == -1 && TextUtils.isEmpty(tag) && TextUtils.isEmpty(searchText); 66 | } 67 | 68 | public boolean matches(LogLine logLine) { 69 | 70 | // consider the criteria to be ANDed 71 | if (!checkFoundPid(logLine)) { 72 | return false; 73 | } 74 | if (!checkFoundTag(logLine)) { 75 | return false; 76 | } 77 | return checkFoundText(logLine); 78 | } 79 | 80 | private boolean checkFoundText(LogLine logLine) { 81 | return TextUtils.isEmpty(searchText) 82 | || (searchTextAsInt != -1 && searchTextAsInt == logLine.getProcessId()) 83 | || (logLine.getTag() != null && StringUtil.containsIgnoreCase(logLine.getTag(), searchText)) 84 | || (logLine.getLogOutput() != null && StringUtil.containsIgnoreCase(logLine.getLogOutput(), searchText)); 85 | } 86 | 87 | private boolean checkFoundTag(LogLine logLine) { 88 | return TextUtils.isEmpty(tag) 89 | || (logLine.getTag() != null && StringUtil.containsIgnoreCase(logLine.getTag(), tag)); 90 | } 91 | 92 | private boolean checkFoundPid(LogLine logLine) { 93 | return pid == -1 || logLine.getProcessId() == pid; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/ServiceHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import com.pluscubed.logcat.CrazyLoggerService; 9 | import com.pluscubed.logcat.LogcatRecordingService; 10 | import com.pluscubed.logcat.reader.LogcatReaderLoader; 11 | import com.pluscubed.logcat.util.UtilLogger; 12 | 13 | import java.util.List; 14 | 15 | public class ServiceHelper { 16 | 17 | private static UtilLogger log = new UtilLogger(ServiceHelper.class); 18 | 19 | public static void startOrStopCrazyLogger(Context context) { 20 | 21 | boolean alreadyRunning = checkIfServiceIsRunning(context, CrazyLoggerService.class); 22 | Intent intent = new Intent(context, CrazyLoggerService.class); 23 | 24 | if (!alreadyRunning) { 25 | context.startService(intent); 26 | } else { 27 | context.stopService(intent); 28 | } 29 | 30 | } 31 | 32 | public static synchronized void stopBackgroundServiceIfRunning(Context context) { 33 | boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); 34 | 35 | log.d("Is CatlogService running: %s", alreadyRunning); 36 | 37 | if (alreadyRunning) { 38 | Intent intent = new Intent(context, LogcatRecordingService.class); 39 | context.stopService(intent); 40 | } 41 | 42 | } 43 | 44 | public static synchronized void startBackgroundServiceIfNotAlreadyRunning( 45 | Context context, String filename, String queryFilter, String level) { 46 | 47 | boolean alreadyRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); 48 | 49 | log.d("Is CatlogService already running: %s", alreadyRunning); 50 | 51 | if (!alreadyRunning) { 52 | 53 | Intent intent = new Intent(context, LogcatRecordingService.class); 54 | intent.putExtra(LogcatRecordingService.EXTRA_FILENAME, filename); 55 | 56 | // load "lastLine" in the background 57 | LogcatReaderLoader loader = LogcatReaderLoader.create(context, true); 58 | intent.putExtra(LogcatRecordingService.EXTRA_LOADER, loader); 59 | 60 | // add query text and log level 61 | intent.putExtra(LogcatRecordingService.EXTRA_QUERY_FILTER, queryFilter); 62 | intent.putExtra(LogcatRecordingService.EXTRA_LEVEL, level); 63 | 64 | context.startService(intent); 65 | } 66 | } 67 | 68 | public static boolean checkIfServiceIsRunning(Context context, Class service) { 69 | 70 | String serviceName = service.getName(); 71 | 72 | ComponentName componentName = new ComponentName(context.getPackageName(), serviceName); 73 | 74 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 75 | 76 | List procList = activityManager.getRunningServices(Integer.MAX_VALUE); 77 | 78 | if (procList != null) { 79 | 80 | for (ActivityManager.RunningServiceInfo appProcInfo : procList) { 81 | if (appProcInfo != null && componentName.equals(appProcInfo.service)) { 82 | log.d("%s is already running", serviceName); 83 | return true; 84 | } 85 | } 86 | } 87 | log.d("%s is not running", serviceName); 88 | return false; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/reader/SingleLogcatReader.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.reader; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.pluscubed.logcat.helper.LogcatHelper; 6 | import com.pluscubed.logcat.helper.RuntimeHelper; 7 | import com.pluscubed.logcat.helper.VersionHelper; 8 | import com.pluscubed.logcat.util.UtilLogger; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | public class SingleLogcatReader extends AbsLogcatReader { 17 | 18 | private static UtilLogger log = new UtilLogger(SingleLogcatReader.class); 19 | 20 | private Process logcatProcess; 21 | private BufferedReader bufferedReader; 22 | private String logBuffer; 23 | private String lastLine; 24 | 25 | public SingleLogcatReader(boolean recordingMode, String logBuffer, String lastLine) throws IOException { 26 | super(recordingMode); 27 | this.logBuffer = logBuffer; 28 | this.lastLine = lastLine; 29 | init(); 30 | } 31 | 32 | private void init() throws IOException { 33 | // use the "time" log so we can see what time the logs were logged at 34 | logcatProcess = LogcatHelper.getLogcatProcess(logBuffer); 35 | 36 | bufferedReader = new BufferedReader(new InputStreamReader(logcatProcess 37 | .getInputStream()), 8192); 38 | } 39 | 40 | 41 | public String getLogBuffer() { 42 | return logBuffer; 43 | } 44 | 45 | 46 | @Override 47 | public void killQuietly() { 48 | if (logcatProcess != null) { 49 | RuntimeHelper.destroy(logcatProcess); 50 | log.d("killed 1 logcat process"); 51 | } 52 | 53 | // post-jellybean, we just kill the process, so there's no need 54 | // to close the bufferedReader. Anyway, it just hangs. 55 | if (VersionHelper.getVersionSdkIntCompat() < VersionHelper.VERSION_JELLYBEAN 56 | && bufferedReader != null) { 57 | try { 58 | bufferedReader.close(); 59 | } catch (IOException e) { 60 | log.e(e, "unexpected exception"); 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public String readLine() throws IOException { 67 | String line = bufferedReader.readLine(); 68 | 69 | if (recordingMode && lastLine != null) { // still skipping past the 'last line' 70 | if (lastLine.equals(line) || isAfterLastTime(line)) { 71 | lastLine = null; // indicates we've passed the last line 72 | } 73 | } 74 | 75 | return line; 76 | 77 | } 78 | 79 | private boolean isAfterLastTime(String line) { 80 | // doing a string comparison is sufficient to determine whether this line is chronologically 81 | // after the last line, because the format they use is exactly the same and 82 | // lists larger time period before smaller ones 83 | return isDatedLogLine(lastLine) && isDatedLogLine(line) && line.compareTo(lastLine) > 0; 84 | 85 | } 86 | 87 | private boolean isDatedLogLine(String line) { 88 | // 18 is the size of the logcat timestamp 89 | return (!TextUtils.isEmpty(line) && line.length() >= 18 && Character.isDigit(line.charAt(0))); 90 | } 91 | 92 | 93 | @Override 94 | public boolean readyToRecord() { 95 | return recordingMode && lastLine == null; 96 | } 97 | 98 | @Override 99 | public List getProcesses() { 100 | return Collections.singletonList(logcatProcess); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 22 | 23 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 51 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/LogLineAdapterUtil.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.pluscubed.logcat.R; 7 | import com.pluscubed.logcat.data.ColorScheme; 8 | import com.pluscubed.logcat.helper.PreferenceHelper; 9 | 10 | public class LogLineAdapterUtil { 11 | 12 | public static final int LOG_WTF = 100; // arbitrary int to signify 'wtf' log level 13 | 14 | private static final int NUM_COLORS = 17; 15 | 16 | public static int getBackgroundColorForLogLevel(Context context, int logLevel) { 17 | int result = android.R.color.black; 18 | switch (logLevel) { 19 | case Log.DEBUG: 20 | result = R.color.background_debug; 21 | break; 22 | case Log.ERROR: 23 | result = R.color.background_error; 24 | break; 25 | case Log.INFO: 26 | result = R.color.background_info; 27 | break; 28 | case Log.VERBOSE: 29 | result = R.color.background_verbose; 30 | break; 31 | case Log.WARN: 32 | result = R.color.background_warn; 33 | break; 34 | case LOG_WTF: 35 | result = R.color.background_wtf; 36 | break; 37 | } 38 | 39 | return context.getResources().getColor(result); 40 | } 41 | 42 | public static int getForegroundColorForLogLevel(Context context, int logLevel) { 43 | int result = android.R.color.primary_text_dark; 44 | switch (logLevel) { 45 | case Log.DEBUG: 46 | result = R.color.foreground_debug; 47 | break; 48 | case Log.ERROR: 49 | result = R.color.foreground_error; 50 | break; 51 | case Log.INFO: 52 | result = R.color.foreground_info; 53 | break; 54 | case Log.VERBOSE: 55 | result = R.color.foreground_verbose; 56 | break; 57 | case Log.WARN: 58 | result = R.color.foreground_warn; 59 | break; 60 | case LOG_WTF: 61 | result = R.color.foreground_wtf; 62 | break; 63 | } 64 | return context.getResources().getColor(result); 65 | } 66 | 67 | public static synchronized int getOrCreateTagColor(Context context, String tag) { 68 | 69 | int hashCode = (tag == null) ? 0 : tag.hashCode(); 70 | 71 | int smear = Math.abs(hashCode) % NUM_COLORS; 72 | 73 | return getColorAt(smear, context); 74 | 75 | } 76 | 77 | private static int getColorAt(int i, Context context) { 78 | 79 | ColorScheme colorScheme = PreferenceHelper.getColorScheme(context); 80 | 81 | int[] colorArray = colorScheme.getTagColors(context); 82 | 83 | return colorArray[i]; 84 | 85 | } 86 | 87 | public static boolean logLevelIsAcceptableGivenLogLevelLimit(int logLevel, int logLevelLimit) { 88 | 89 | int minVal = 0; 90 | switch (logLevel) { 91 | 92 | case Log.VERBOSE: 93 | minVal = 0; 94 | break; 95 | case Log.DEBUG: 96 | minVal = 1; 97 | break; 98 | case Log.INFO: 99 | minVal = 2; 100 | break; 101 | case Log.WARN: 102 | minVal = 3; 103 | break; 104 | case Log.ERROR: 105 | minVal = 4; 106 | break; 107 | case LOG_WTF: 108 | minVal = 5; 109 | break; 110 | default: // e.g. the starting line that says "output of log such-and-such" 111 | return true; 112 | } 113 | 114 | return minVal >= logLevelLimit; 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/db/CatlogDBHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.db; 2 | 3 | import android.content.ContentValues; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.database.sqlite.SQLiteDatabase; 7 | import android.database.sqlite.SQLiteOpenHelper; 8 | 9 | import com.pluscubed.logcat.util.UtilLogger; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class CatlogDBHelper extends SQLiteOpenHelper { 15 | 16 | private static final String DB_NAME = "catlog.db"; 17 | private static final int DB_VERSION = 1; 18 | private static final String TABLE_NAME = "Filters"; 19 | private static final String COLUMN_ID = "_id"; 20 | private static final String COLUMN_TEXT = "filterText"; 21 | private static UtilLogger log = new UtilLogger(CatlogDBHelper.class); 22 | private SQLiteDatabase db; 23 | 24 | public CatlogDBHelper(Context context) { 25 | super(context, DB_NAME, null, DB_VERSION); 26 | db = getWritableDatabase(); 27 | } 28 | 29 | @Override 30 | public void onCreate(SQLiteDatabase db) { 31 | String createSql = "create table if not exists " + TABLE_NAME + " (" 32 | + COLUMN_ID + " integer not null primary key autoincrement, " 33 | + COLUMN_TEXT + " text);"; 34 | 35 | String indexSql = "create unique index if not exists index_game_id on " + TABLE_NAME 36 | + " (" + COLUMN_TEXT + ");"; 37 | 38 | db.execSQL(createSql); 39 | db.execSQL(indexSql); 40 | } 41 | 42 | @Override 43 | public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) { 44 | // do nothing 45 | } 46 | 47 | public List findFilterItems() { 48 | 49 | synchronized (CatlogDBHelper.class) { 50 | 51 | List filters = new ArrayList(); 52 | 53 | Cursor cursor = null; 54 | try { 55 | cursor = db.query(TABLE_NAME, new String[]{COLUMN_ID, COLUMN_TEXT}, null, null, null, null, null); 56 | 57 | while (cursor.moveToNext()) { 58 | FilterItem filterItem = FilterItem.create(cursor.getInt(0), cursor.getString(1)); 59 | filters.add(filterItem); 60 | } 61 | } finally { 62 | if (cursor != null) { 63 | cursor.close(); 64 | } 65 | } 66 | 67 | log.d("fetched %d filters", filters.size()); 68 | 69 | return filters; 70 | } 71 | } 72 | 73 | public void deleteFilter(int id) { 74 | synchronized (CatlogDBHelper.class) { 75 | int rows = db.delete(TABLE_NAME, COLUMN_ID + "=" + id, null); 76 | log.d("deleted %d filters with id %d", rows, id); 77 | } 78 | } 79 | 80 | public FilterItem addFilter(String text) { 81 | synchronized (CatlogDBHelper.class) { 82 | 83 | ContentValues contentValues = new ContentValues(); 84 | contentValues.put(COLUMN_TEXT, text); 85 | 86 | long result = db.insert(TABLE_NAME, null, contentValues); 87 | 88 | log.d("inserted filter with text %s: %g", text, result); 89 | 90 | if (result == -1) { 91 | log.d("attempted to insert duplicate filter"); 92 | return null; 93 | } 94 | 95 | Cursor cursor = null; 96 | try { 97 | String selection = COLUMN_TEXT + "=?"; 98 | String[] selectionArgs = {text}; 99 | cursor = db.query(TABLE_NAME, new String[]{COLUMN_ID, COLUMN_TEXT}, selection, selectionArgs, null, null, null); 100 | cursor.moveToNext(); 101 | return FilterItem.create(cursor.getInt(0), cursor.getString(1)); 102 | } finally { 103 | if (cursor != null) { 104 | cursor.close(); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/reader/MultipleLogcatReader.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.reader; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import com.pluscubed.logcat.util.UtilLogger; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Map.Entry; 13 | import java.util.concurrent.ArrayBlockingQueue; 14 | import java.util.concurrent.BlockingQueue; 15 | 16 | /** 17 | * Combines multipe buffered readers into a single reader that merges all input synchronously. 18 | * 19 | * @author nolan 20 | */ 21 | public class MultipleLogcatReader extends AbsLogcatReader { 22 | 23 | private static final String DUMMY_NULL = ""; 24 | private static UtilLogger log = new UtilLogger(MultipleLogcatReader.class); 25 | private List readerThreads = new LinkedList(); 26 | private BlockingQueue queue = new ArrayBlockingQueue(1); 27 | 28 | public MultipleLogcatReader(boolean recordingMode, 29 | Map lastLines) throws IOException { 30 | super(recordingMode); 31 | // read from all three buffers at once 32 | for (Entry entry : lastLines.entrySet()) { 33 | String logBuffer = entry.getKey(); 34 | String lastLine = entry.getValue(); 35 | ReaderThread readerThread = new ReaderThread(logBuffer, lastLine); 36 | readerThread.start(); 37 | readerThreads.add(readerThread); 38 | } 39 | } 40 | 41 | public String readLine() throws IOException { 42 | 43 | try { 44 | String value = queue.take(); 45 | if (!value.equals(DUMMY_NULL)) { 46 | return value; 47 | } 48 | } catch (InterruptedException e) { 49 | log.d(e, ""); 50 | } 51 | return null; 52 | } 53 | 54 | 55 | @Override 56 | public boolean readyToRecord() { 57 | for (ReaderThread thread : readerThreads) { 58 | if (!thread.reader.readyToRecord()) { 59 | return false; 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | @Override 66 | public void killQuietly() { 67 | for (ReaderThread thread : readerThreads) { 68 | thread.killed = true; 69 | } 70 | 71 | // do in background, because otherwise we might hang 72 | new AsyncTask() { 73 | 74 | @Override 75 | protected Void doInBackground(Void... params) { 76 | for (ReaderThread thread : readerThreads) { 77 | thread.reader.killQuietly(); 78 | } 79 | queue.offer(DUMMY_NULL); 80 | return null; 81 | } 82 | }.execute((Void) null); 83 | } 84 | 85 | 86 | @Override 87 | public List getProcesses() { 88 | List result = new ArrayList(); 89 | for (ReaderThread thread : readerThreads) { 90 | result.addAll(thread.reader.getProcesses()); 91 | } 92 | return result; 93 | } 94 | 95 | private class ReaderThread extends Thread { 96 | 97 | SingleLogcatReader reader; 98 | 99 | private boolean killed; 100 | 101 | public ReaderThread(String logBuffer, String lastLine) throws IOException { 102 | this.reader = new SingleLogcatReader(recordingMode, logBuffer, lastLine); 103 | } 104 | 105 | @Override 106 | public void run() { 107 | String line; 108 | 109 | try { 110 | while (!killed && (line = reader.readLine()) != null && !killed) { 111 | queue.put(line); 112 | } 113 | } catch (IOException | InterruptedException e) { 114 | log.d(e, "exception"); 115 | } 116 | log.d("thread died"); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 17 | 18 | 23 | 24 | 29 | 30 | 34 | 35 | 36 | 41 | 42 | 47 | 48 | 53 | 54 | 59 | 60 | 65 | 66 | 71 | 72 | 73 | 74 | 79 | 80 | 85 | 86 | 91 | 92 | 97 | 98 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/WidgetHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.app.PendingIntent; 4 | import android.appwidget.AppWidgetManager; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.view.View; 10 | import android.widget.RemoteViews; 11 | 12 | import com.pluscubed.logcat.LogcatRecordingService; 13 | import com.pluscubed.logcat.R; 14 | import com.pluscubed.logcat.RecordingWidgetProvider; 15 | import com.pluscubed.logcat.util.UtilLogger; 16 | 17 | public class WidgetHelper { 18 | 19 | private static UtilLogger log = new UtilLogger(WidgetHelper.class); 20 | 21 | public static void updateWidgets(Context context) { 22 | 23 | int[] appWidgetIds = findAppWidgetIds(context); 24 | 25 | updateWidgets(context, appWidgetIds); 26 | 27 | } 28 | 29 | 30 | /** 31 | * manually tell us if the service is running or not 32 | */ 33 | public static void updateWidgets(Context context, boolean serviceRunning) { 34 | 35 | int[] appWidgetIds = findAppWidgetIds(context); 36 | 37 | updateWidgets(context, appWidgetIds, serviceRunning); 38 | 39 | } 40 | 41 | public static void updateWidgets(Context context, int[] appWidgetIds) { 42 | 43 | boolean serviceRunning = ServiceHelper.checkIfServiceIsRunning(context, LogcatRecordingService.class); 44 | 45 | updateWidgets(context, appWidgetIds, serviceRunning); 46 | 47 | } 48 | 49 | 50 | public static void updateWidgets(Context context, int[] appWidgetIds, boolean serviceRunning) { 51 | 52 | AppWidgetManager manager = AppWidgetManager.getInstance(context); 53 | 54 | for (int appWidgetId : appWidgetIds) { 55 | 56 | if (!PreferenceHelper.getWidgetExistsPreference(context, appWidgetId)) { 57 | // android has a bug that sometimes keeps stale app widget ids around 58 | log.d("Found stale app widget id %d; skipping...", appWidgetId); 59 | continue; 60 | } 61 | 62 | updateWidget(context, manager, appWidgetId, serviceRunning); 63 | 64 | } 65 | 66 | } 67 | 68 | private static void updateWidget(Context context, AppWidgetManager manager, int appWidgetId, boolean serviceRunning) { 69 | 70 | 71 | RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_recording); 72 | 73 | // change the subtext depending on whether the service is running or not 74 | CharSequence subtext = context.getText( 75 | serviceRunning ? R.string.widget_recording_in_progress : R.string.widget_start_recording); 76 | updateViews.setTextViewText(R.id.widget_subtext, subtext); 77 | 78 | // if service not running, don't show the "recording" icon 79 | updateViews.setViewVisibility(R.id.record_badge_image_view, serviceRunning ? View.VISIBLE : View.INVISIBLE); 80 | 81 | PendingIntent pendingIntent = getPendingIntent(context, appWidgetId); 82 | 83 | updateViews.setOnClickPendingIntent(R.id.clickable_linear_layout, pendingIntent); 84 | 85 | manager.updateAppWidget(appWidgetId, updateViews); 86 | 87 | } 88 | 89 | private static PendingIntent getPendingIntent(Context context, int appWidgetId) { 90 | 91 | Intent intent = new Intent(); 92 | intent.setAction(RecordingWidgetProvider.ACTION_RECORD_OR_STOP); 93 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 94 | // gotta make this unique for this appwidgetid - otherwise, the PendingIntents conflict 95 | // it seems to be a quasi-bug in Android 96 | Uri data = Uri.withAppendedPath(Uri.parse(RecordingWidgetProvider.URI_SCHEME + "://widget/id/#"), String.valueOf(appWidgetId)); 97 | intent.setData(data); 98 | 99 | return PendingIntent.getBroadcast(context, 100 | 0 /* no requestCode */, intent, PendingIntent.FLAG_ONE_SHOT); 101 | } 102 | 103 | private static int[] findAppWidgetIds(Context context) { 104 | AppWidgetManager manager = AppWidgetManager.getInstance(context); 105 | ComponentName widget = new ComponentName(context, RecordingWidgetProvider.class); 106 | return manager.getAppWidgetIds(widget); 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/ui/AppCompatPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.pluscubed.logcat.ui; 18 | 19 | import android.content.res.Configuration; 20 | import android.os.Bundle; 21 | import android.preference.PreferenceActivity; 22 | import android.support.annotation.LayoutRes; 23 | import android.support.annotation.NonNull; 24 | import android.support.annotation.Nullable; 25 | import android.support.v7.app.ActionBar; 26 | import android.support.v7.app.AppCompatDelegate; 27 | import android.support.v7.widget.Toolbar; 28 | import android.view.MenuInflater; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | 32 | /** 33 | * A {@link PreferenceActivity} which implements and proxies the necessary calls 34 | * to be used with AppCompat. 35 | *

36 | * This technique can be used with an {@link android.app.Activity} class, not just 37 | * {@link PreferenceActivity}. 38 | */ 39 | public abstract class AppCompatPreferenceActivity extends PreferenceActivity { 40 | 41 | private AppCompatDelegate mDelegate; 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | getDelegate().installViewFactory(); 46 | getDelegate().onCreate(savedInstanceState); 47 | super.onCreate(savedInstanceState); 48 | } 49 | 50 | @Override 51 | protected void onPostCreate(Bundle savedInstanceState) { 52 | super.onPostCreate(savedInstanceState); 53 | getDelegate().onPostCreate(savedInstanceState); 54 | } 55 | 56 | public ActionBar getSupportActionBar() { 57 | return getDelegate().getSupportActionBar(); 58 | } 59 | 60 | public void setSupportActionBar(@Nullable Toolbar toolbar) { 61 | getDelegate().setSupportActionBar(toolbar); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public MenuInflater getMenuInflater() { 67 | return getDelegate().getMenuInflater(); 68 | } 69 | 70 | @Override 71 | public void setContentView(@LayoutRes int layoutResID) { 72 | getDelegate().setContentView(layoutResID); 73 | } 74 | 75 | @Override 76 | public void setContentView(View view) { 77 | getDelegate().setContentView(view); 78 | } 79 | 80 | @Override 81 | public void setContentView(View view, ViewGroup.LayoutParams params) { 82 | getDelegate().setContentView(view, params); 83 | } 84 | 85 | @Override 86 | public void addContentView(View view, ViewGroup.LayoutParams params) { 87 | getDelegate().addContentView(view, params); 88 | } 89 | 90 | @Override 91 | protected void onPostResume() { 92 | super.onPostResume(); 93 | getDelegate().onPostResume(); 94 | } 95 | 96 | @Override 97 | protected void onTitleChanged(CharSequence title, int color) { 98 | super.onTitleChanged(title, color); 99 | getDelegate().setTitle(title); 100 | } 101 | 102 | @Override 103 | public void onConfigurationChanged(Configuration newConfig) { 104 | super.onConfigurationChanged(newConfig); 105 | getDelegate().onConfigurationChanged(newConfig); 106 | } 107 | 108 | @Override 109 | protected void onStop() { 110 | super.onStop(); 111 | getDelegate().onStop(); 112 | } 113 | 114 | @Override 115 | protected void onDestroy() { 116 | super.onDestroy(); 117 | getDelegate().onDestroy(); 118 | } 119 | 120 | public void invalidateOptionsMenu() { 121 | getDelegate().invalidateOptionsMenu(); 122 | } 123 | 124 | private AppCompatDelegate getDelegate() { 125 | if (mDelegate == null) { 126 | mDelegate = AppCompatDelegate.create(this, null); 127 | } 128 | return mDelegate; 129 | } 130 | } -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/UpdateHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.SharedPreferences.Editor; 6 | import android.preference.PreferenceManager; 7 | 8 | import com.pluscubed.logcat.R; 9 | import com.pluscubed.logcat.util.Callback; 10 | import com.pluscubed.logcat.util.Function; 11 | 12 | /** 13 | * Helper for applying app-wide updates of persistent data. 14 | * 15 | * @author nlawson 16 | */ 17 | public class UpdateHelper { 18 | 19 | public static boolean areUpdatesNecessary(Context context) { 20 | for (Update update : Update.values()) { 21 | if (update.getIsNecessary().apply(context)) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | } 27 | 28 | public static void runUpdatesIfNecessary(Context context) { 29 | for (Update update : Update.values()) { 30 | if (update.getIsNecessary().apply(context)) { 31 | update.getRunUpdate().onCallback(context); 32 | } 33 | } 34 | } 35 | 36 | private enum Update { 37 | 38 | // update to change "all_combined" to a comma-separation of all three buffers 39 | Update1(new Function() { 40 | 41 | @Override 42 | public Boolean apply(Context context) { 43 | SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); 44 | String bufferPref = sharedPrefs.getString( 45 | context.getString(R.string.pref_buffer), context.getString(R.string.pref_buffer_choice_main)); 46 | 47 | return bufferPref.equals("all_combined"); 48 | } 49 | }, new Callback() { 50 | 51 | @Override 52 | public void onCallback(Context context) { 53 | SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); 54 | String bufferPref = sharedPrefs.getString( 55 | context.getString(R.string.pref_buffer), context.getString(R.string.pref_buffer_choice_main)); 56 | 57 | if (bufferPref.equals("all_combined")) { 58 | Editor editor = sharedPrefs.edit(); 59 | editor.putString(context.getString(R.string.pref_buffer), "main,events,radio"); 60 | editor.apply(); 61 | } 62 | } 63 | }), 64 | 65 | // update to move saved logs from /sdcard/catlog_saved_logs to /sdcard/catlog/saved_logs 66 | Update2(new Function() { 67 | 68 | @Override 69 | public Boolean apply(Context context) { 70 | return SaveLogHelper.checkIfSdCardExists() && SaveLogHelper.legacySavedLogsDirExists(); 71 | } 72 | }, new Callback() { 73 | 74 | @Override 75 | public void onCallback(Context context) { 76 | if (SaveLogHelper.checkIfSdCardExists()) { 77 | SaveLogHelper.moveLogsFromLegacyDirIfNecessary(); 78 | } 79 | } 80 | }), 81 | 82 | // update to request superuser READ_LOGS permission on JellyBean 83 | Update3(new Function() { 84 | 85 | @Override 86 | public Boolean apply(Context context) { 87 | 88 | boolean isJellyBean = VersionHelper.getVersionSdkIntCompat() >= VersionHelper.VERSION_JELLYBEAN; 89 | 90 | return isJellyBean && !PreferenceHelper.getJellybeanRootRan(context); 91 | } 92 | }, new Callback() { 93 | 94 | @Override 95 | public void onCallback(Context context) { 96 | SuperUserHelper.requestRoot(context); 97 | } 98 | }),; 99 | 100 | private Function isNecessary; 101 | private Callback runUpdate; 102 | 103 | Update(Function isNecessary, Callback runUpdate) { 104 | this.isNecessary = isNecessary; 105 | this.runUpdate = runUpdate; 106 | } 107 | 108 | public Function getIsNecessary() { 109 | return isNecessary; 110 | } 111 | 112 | public Callback getRunUpdate() { 113 | return runUpdate; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/ui/AboutDialogActivity.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.ui; 2 | 3 | import android.app.Dialog; 4 | import android.app.DialogFragment; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.os.Handler; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | 12 | import com.afollestad.materialdialogs.MaterialDialog; 13 | import com.pluscubed.logcat.R; 14 | import com.pluscubed.logcat.helper.PackageHelper; 15 | import com.pluscubed.logcat.util.UtilLogger; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.InputStreamReader; 21 | 22 | public class AboutDialogActivity extends AppCompatActivity { 23 | 24 | private static UtilLogger log = new UtilLogger(AboutDialogActivity.class); 25 | 26 | 27 | public void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | 30 | DialogFragment fragment = new AboutDialog(); 31 | fragment.show(getFragmentManager(), "aboutDialog"); 32 | 33 | } 34 | 35 | public static class AboutDialog extends DialogFragment { 36 | 37 | private Handler handler = new Handler(); 38 | 39 | @Override 40 | public void onDismiss(DialogInterface dialog) { 41 | super.onDismiss(dialog); 42 | getActivity().finish(); 43 | } 44 | 45 | 46 | public void initializeWebView(WebView view) { 47 | 48 | String text = loadTextFile(R.raw.about_body); 49 | String version = PackageHelper.getVersionName(getActivity()); 50 | String changelog = loadTextFile(R.raw.changelog); 51 | String css = loadTextFile(R.raw.about_css); 52 | text = String.format(text, version, changelog, css); 53 | 54 | WebSettings settings = view.getSettings(); 55 | settings.setDefaultTextEncodingName("utf-8"); 56 | 57 | view.loadDataWithBaseURL(null, text, "text/html", "UTF-8", null); 58 | } 59 | 60 | private String loadTextFile(int resourceId) { 61 | 62 | InputStream is = getResources().openRawResource(resourceId); 63 | 64 | BufferedReader buff = new BufferedReader(new InputStreamReader(is)); 65 | StringBuilder sb = new StringBuilder(); 66 | 67 | try { 68 | while (buff.ready()) { 69 | sb.append(buff.readLine()).append("\n"); 70 | } 71 | } catch (IOException e) { 72 | log.e(e, "This should not happen"); 73 | } 74 | 75 | return sb.toString(); 76 | 77 | } 78 | 79 | @Override 80 | public Dialog onCreateDialog(Bundle savedInstanceState) { 81 | WebView view = new WebView(getActivity()); 82 | /* 83 | view.setWebViewClient(new AboutWebClient());*/ 84 | initializeWebView(view); 85 | 86 | return new MaterialDialog.Builder(getActivity()) 87 | .customView(view, false) 88 | .title(R.string.about_matlog) 89 | .iconRes(R.drawable.ic_launcher) 90 | .positiveText(android.R.string.ok) 91 | .build(); 92 | } 93 | 94 | 95 | /*private void loadExternalUrl(String url) { 96 | Intent intent = new Intent(); 97 | intent.setAction("android.intent.action.VIEW"); 98 | intent.setData(Uri.parse(url)); 99 | 100 | startActivity(intent); 101 | }*/ 102 | 103 | /*private class AboutWebClient extends WebViewClient { 104 | 105 | @Override 106 | public boolean shouldOverrideUrlLoading(WebView view, final String url) { 107 | log.d("shouldOverrideUrlLoading"); 108 | 109 | // XXX hack to make the webview go to an external url if the hyperlink is 110 | // in my own HTML file - otherwise it says "Page not available" because I'm not calling 111 | // loadDataWithBaseURL. But if I call loadDataWithBaseUrl using a fake URL, then 112 | // the links within the page itself don't work!! Arggggh!!! 113 | 114 | if (url.startsWith("http") || url.startsWith("mailto") || url.startsWith("market")) { 115 | handler.post(new Runnable() { 116 | @Override 117 | public void run() { 118 | loadExternalUrl(url); 119 | } 120 | }); 121 | return true; 122 | } 123 | return false; 124 | } 125 | }*/ 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/pref_buffer_choice_main 5 | @string/pref_buffer_choice_events 6 | @string/pref_buffer_choice_radio 7 | 8 | 9 | 10 | @string/pref_buffer_choice_main_value 11 | @string/pref_buffer_choice_events_value 12 | @string/pref_buffer_choice_radio_value 13 | 14 | 15 | 16 | @string/pref_theme_choice_dark_value 17 | @string/pref_theme_choice_light_value 18 | @string/pref_theme_choice_android_value 19 | @string/pref_theme_choice_verizon_value 20 | @string/pref_theme_choice_att_value 21 | @string/pref_theme_choice_sprint_value 22 | @string/pref_theme_choice_tmobile_value 23 | 24 | 25 | 26 | @string/pref_theme_choice_dark_name 27 | @string/pref_theme_choice_light_name 28 | @string/pref_theme_choice_android_name 29 | @string/pref_theme_choice_verizon_name 30 | @string/pref_theme_choice_att_name 31 | @string/pref_theme_choice_sprint_name 32 | @string/pref_theme_choice_tmobile_name 33 | 34 | 35 | 36 | @string/filter_choice_tag 37 | @string/filter_choice_pid 38 | 39 | 40 | 41 | @string/log_level_verbose 42 | @string/log_level_debug 43 | @string/log_level_info 44 | @string/log_level_warn 45 | @string/log_level_error 46 | @string/log_level_wtf 47 | 48 | 49 | 50 | @string/log_level_value_verbose 51 | @string/log_level_value_debug 52 | @string/log_level_value_info 53 | @string/log_level_value_warn 54 | @string/log_level_value_error 55 | @string/log_level_value_wtf 56 | 57 | 58 | 59 | @string/text_size_xsmall_value 60 | @string/text_size_small_value 61 | @string/text_size_medium_value 62 | @string/text_size_large_value 63 | @string/text_size_xlarge_value 64 | 65 | 66 | 67 | @string/text_size_xsmall_name 68 | @string/text_size_small_name 69 | @string/text_size_medium_name 70 | @string/text_size_large_name 71 | @string/text_size_xlarge_name 72 | 73 | 74 | 75 | 76 | @color/tag_color_01 77 | @color/tag_color_02 78 | @color/tag_color_03 79 | @color/tag_color_04 80 | @color/tag_color_05 81 | @color/tag_color_06 82 | @color/tag_color_07 83 | @color/tag_color_08 84 | @color/tag_color_09 85 | @color/tag_color_10 86 | @color/tag_color_11 87 | @color/tag_color_12 88 | @color/tag_color_13 89 | @color/tag_color_14 90 | @color/tag_color_15 91 | @color/tag_color_16 92 | @color/tag_color_17 93 | 94 | 95 | 96 | @color/light_tag_color_01 97 | @color/light_tag_color_02 98 | @color/light_tag_color_03 99 | @color/light_tag_color_04 100 | @color/light_tag_color_05 101 | @color/light_tag_color_06 102 | @color/light_tag_color_07 103 | @color/light_tag_color_08 104 | @color/light_tag_color_09 105 | @color/light_tag_color_10 106 | @color/light_tag_color_11 107 | @color/light_tag_color_12 108 | @color/light_tag_color_13 109 | @color/light_tag_color_14 110 | @color/light_tag_color_15 111 | @color/light_tag_color_16 112 | @color/light_tag_color_17 113 | 114 | 115 | 116 | @color/android_theme_tag_color_01 117 | @color/android_theme_tag_color_02 118 | @color/android_theme_tag_color_03 119 | @color/android_theme_tag_color_04 120 | @color/android_theme_tag_color_05 121 | @color/android_theme_tag_color_06 122 | @color/android_theme_tag_color_07 123 | @color/android_theme_tag_color_08 124 | @color/android_theme_tag_color_09 125 | @color/android_theme_tag_color_10 126 | @color/android_theme_tag_color_11 127 | @color/android_theme_tag_color_12 128 | @color/android_theme_tag_color_13 129 | @color/android_theme_tag_color_14 130 | @color/android_theme_tag_color_15 131 | @color/android_theme_tag_color_16 132 | @color/android_theme_tag_color_17 133 | 134 | 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/LogLine.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import com.pluscubed.logcat.util.LogLineAdapterUtil; 7 | import com.pluscubed.logcat.util.UtilLogger; 8 | 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | 13 | public class LogLine { 14 | 15 | public static final String LOGCAT_DATE_FORMAT = "MM-dd HH:mm:ss.SSS"; 16 | 17 | private static final int TIMESTAMP_LENGTH = 19; 18 | 19 | private static Pattern logPattern = Pattern.compile( 20 | // log level 21 | "(\\w)" + 22 | "/" + 23 | // tag 24 | "([^(]+)" + 25 | "\\(\\s*" + 26 | // pid 27 | "(\\d+)" + 28 | // optional weird number that only occurs on ZTE blade 29 | "(?:\\*\\s*\\d+)?" + 30 | "\\): "); 31 | 32 | private static UtilLogger log = new UtilLogger(LogLine.class); 33 | 34 | private int logLevel; 35 | private String tag; 36 | private String logOutput; 37 | private int processId = -1; 38 | private String timestamp; 39 | private boolean expanded = false; 40 | private boolean highlighted = false; 41 | 42 | public static LogLine newLogLine(String originalLine, boolean expanded) { 43 | 44 | LogLine logLine = new LogLine(); 45 | logLine.setExpanded(expanded); 46 | 47 | int startIdx = 0; 48 | 49 | // if the first char is a digit, then this starts out with a timestamp 50 | // otherwise, it's a legacy log or the beginning of the log output or something 51 | if (!TextUtils.isEmpty(originalLine) 52 | && Character.isDigit(originalLine.charAt(0)) 53 | && originalLine.length() >= TIMESTAMP_LENGTH) { 54 | String timestamp = originalLine.substring(0, TIMESTAMP_LENGTH - 1); 55 | logLine.setTimestamp(timestamp); 56 | startIdx = TIMESTAMP_LENGTH; // cut off timestamp 57 | } 58 | 59 | Matcher matcher = logPattern.matcher(originalLine); 60 | 61 | if (matcher.find(startIdx)) { 62 | char logLevelChar = matcher.group(1).charAt(0); 63 | 64 | logLine.setLogLevel(convertCharToLogLevel(logLevelChar)); 65 | logLine.setTag(matcher.group(2)); 66 | logLine.setProcessId(Integer.parseInt(matcher.group(3))); 67 | 68 | logLine.setLogOutput(originalLine.substring(matcher.end())); 69 | 70 | } else { 71 | log.d("Line doesn't match pattern: %s", originalLine); 72 | logLine.setLogOutput(originalLine); 73 | logLine.setLogLevel(-1); 74 | } 75 | 76 | return logLine; 77 | 78 | } 79 | 80 | public static int convertCharToLogLevel(char logLevelChar) { 81 | 82 | switch (logLevelChar) { 83 | case 'D': 84 | return Log.DEBUG; 85 | case 'E': 86 | return Log.ERROR; 87 | case 'I': 88 | return Log.INFO; 89 | case 'V': 90 | return Log.VERBOSE; 91 | case 'W': 92 | return Log.WARN; 93 | case 'F': 94 | return LogLineAdapterUtil.LOG_WTF; // 'F' actually stands for 'WTF', which is a real Android log level in 2.2 95 | } 96 | return -1; 97 | } 98 | 99 | public static char convertLogLevelToChar(int logLevel) { 100 | 101 | switch (logLevel) { 102 | case Log.DEBUG: 103 | return 'D'; 104 | case Log.ERROR: 105 | return 'E'; 106 | case Log.INFO: 107 | return 'I'; 108 | case Log.VERBOSE: 109 | return 'V'; 110 | case Log.WARN: 111 | return 'W'; 112 | case LogLineAdapterUtil.LOG_WTF: 113 | return 'F'; 114 | } 115 | return ' '; 116 | } 117 | 118 | public String getOriginalLine() { 119 | 120 | if (logLevel == -1) { // starter line like "begin of log etc. etc." 121 | return logOutput; 122 | } 123 | 124 | StringBuilder stringBuilder = new StringBuilder(); 125 | 126 | if (timestamp != null) { 127 | stringBuilder.append(timestamp).append(' '); 128 | } 129 | 130 | stringBuilder.append(convertLogLevelToChar(logLevel)) 131 | .append('/') 132 | .append(tag) 133 | .append('(') 134 | .append(processId) 135 | .append("): ") 136 | .append(logOutput); 137 | 138 | return stringBuilder.toString(); 139 | } 140 | 141 | public int getLogLevel() { 142 | return logLevel; 143 | } 144 | 145 | public void setLogLevel(int logLevel) { 146 | this.logLevel = logLevel; 147 | } 148 | 149 | public String getTag() { 150 | return tag; 151 | } 152 | 153 | public void setTag(String tag) { 154 | this.tag = tag; 155 | } 156 | 157 | public String getLogOutput() { 158 | return logOutput; 159 | } 160 | 161 | public void setLogOutput(String logOutput) { 162 | this.logOutput = logOutput; 163 | } 164 | 165 | public int getProcessId() { 166 | return processId; 167 | } 168 | 169 | public void setProcessId(int processId) { 170 | this.processId = processId; 171 | } 172 | 173 | public String getTimestamp() { 174 | return timestamp; 175 | } 176 | 177 | public void setTimestamp(String timestamp) { 178 | this.timestamp = timestamp; 179 | } 180 | 181 | public boolean isExpanded() { 182 | return expanded; 183 | } 184 | 185 | public void setExpanded(boolean expanded) { 186 | this.expanded = expanded; 187 | } 188 | 189 | public boolean isHighlighted() { 190 | return highlighted; 191 | } 192 | 193 | public void setHighlighted(boolean highlighted) { 194 | this.highlighted = highlighted; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #DDFFFFFF 4 | #FF333333 5 | 6 | #FFf26700 7 | #FFff9000 8 | #FFfbad00 9 | #FFffc600 10 | 11 | #FFFBFBFB 12 | #FFCCCCCC 13 | 14 | #0099dd 15 | 16 | #33cccccc 17 | 18 | #FF999900 19 | #FFCC0000 20 | #FF666666 21 | #FF0066CC 22 | #FF00CC00 23 | #FFFFFF00 24 | 25 | #44FFFFFF 26 | #55BBBBFF 27 | #33000000 28 | #330000BB 29 | #33333333 30 | #FFEEEEEE 31 | 32 | #FF000000 33 | #FFFFFFEE 34 | #FFA4C639 35 | #FF000000 36 | #FFFFFFFF 37 | #FF000000 38 | #FFeaeff3 39 | 40 | @android:color/darker_gray 41 | @android:color/black 42 | @android:color/black 43 | #FFed1f24 44 | #FF0c7db5 45 | #FFffde00 46 | #FFe9098d 47 | 48 | #FF333333 49 | #00000000 50 | #FFA4C639 51 | #FF333333 52 | #00000000 53 | #FF333333 54 | #00000000 55 | 56 | #FFFFFFFF 57 | #FFFFFFFF 58 | #FFCCCCCC 59 | #FFCCCCCC 60 | #FF333333 61 | #FF333333 62 | 63 | 64 | #FFA4C639 65 | #FF3366FF 66 | #FFCC9900 67 | #FFCC0000 68 | #FFCCCCCC 69 | #FF990099 70 | #FF00CC00 71 | #FF66CCFF 72 | #FF00CC99 73 | #FFFF6633 74 | #FFFFFFFF 75 | #FFCCCCFF 76 | #FFFFFF00 77 | #FF0099CC 78 | #FF999900 79 | #FFCC3399 80 | #FFFFCC99 81 | 82 | #FF5b39c6 83 | #FFcc9900 84 | #FF3366ff 85 | #FF006600 86 | #FF990000 87 | #FF000066 88 | #FFff33ff 89 | #FF993300 90 | #FFff3366 91 | #FF0099cc 92 | #FF000000 93 | #FF333300 94 | #FF0000ff 95 | #FFff6633 96 | #FF6666ff 97 | #FF33cc66 98 | #FF003366 99 | 100 | #FF222222 101 | #FF996666 102 | #FFFFCC99 103 | #FFCC0000 104 | #FF666633 105 | #FF990099 106 | #FF006600 107 | #FF660066 108 | #FF003399 109 | #FFFF6633 110 | #FFFFFFFF 111 | #FFCC3333 112 | #FFFFFF00 113 | #FF0099CC 114 | #FF009933 115 | #FFCC3399 116 | #FFFFCC00 117 | 118 | #F44336 119 | #D32F2F 120 | #448AFF 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/data/ColorScheme.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.data; 2 | 3 | import android.content.Context; 4 | 5 | import com.pluscubed.logcat.R; 6 | import com.pluscubed.logcat.util.ArrayUtil; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | 12 | public enum ColorScheme { 13 | Dark(R.string.pref_theme_choice_dark_value, R.color.main_background_dark, 14 | R.color.primary_text_default_material_dark, R.array.dark_theme_colors, R.color.spinner_droptown_dark, 15 | R.color.main_bubble_background_dark_2, false, R.color.accent), 16 | Light(R.string.pref_theme_choice_light_value, R.color.main_background_light, 17 | R.color.main_foreground_light, R.array.light_theme_colors, R.color.spinner_droptown_light, 18 | R.color.main_bubble_background_light_2, true, R.color.main_bubble_background_light_2), 19 | Android(R.string.pref_theme_choice_android_value, R.color.main_background_android, 20 | R.color.main_foreground_android, R.array.android_theme_colors, R.color.spinner_droptown_android, 21 | R.color.main_bubble_background_light, true, R.color.yellow1), 22 | Verizon(R.string.pref_theme_choice_verizon_value, R.color.main_background_verizon, 23 | R.color.main_foreground_verizon, R.array.dark_theme_colors, R.color.spinner_droptown_verizon, 24 | R.color.main_bubble_background_verizon, false, R.color.yellow1), 25 | Att(R.string.pref_theme_choice_att_value, R.color.main_background_att, 26 | R.color.main_foreground_att, R.array.light_theme_colors, R.color.spinner_droptown_att, 27 | R.color.main_bubble_background_light, true, R.color.main_bubble_background_light_2), 28 | Sprint(R.string.pref_theme_choice_sprint_value, R.color.main_background_sprint, 29 | R.color.main_foreground_sprint, R.array.dark_theme_colors, R.color.spinner_droptown_sprint, 30 | R.color.main_bubble_background_dark, false, R.color.yellow1), 31 | Tmobile(R.string.pref_theme_choice_tmobile_value, R.color.main_background_tmobile, 32 | R.color.main_foreground_tmobile, R.array.light_theme_colors, R.color.spinner_droptown_tmobile, 33 | R.color.main_bubble_background_tmobile, true, R.color.main_bubble_background_light_2),; 34 | 35 | private static Map preferenceNameToColorScheme = new HashMap(); 36 | private int nameResource; 37 | private int backgroundColorResource; 38 | private int foregroundColorResource; 39 | private int spinnerColorResource; 40 | private int bubbleBackgroundColorResource; 41 | private int tagColorsResource; 42 | private boolean useLightProgressBar; 43 | private int selectedColorResource; 44 | private int backgroundColor = -1; 45 | private int foregroundColor = -1; 46 | private int spinnerColor = -1; 47 | private int bubbleBackgroundColor = -1; 48 | private int selectedColor = -1; 49 | private int[] tagColors; 50 | 51 | ColorScheme(int nameResource, int backgroundColorResource, int foregroundColorResource, 52 | int tagColorsResource, int spinnerColorResource, int bubbleBackgroundColorResource, 53 | boolean useLightProgressBar, int selectedColorResource) { 54 | this.nameResource = nameResource; 55 | this.backgroundColorResource = backgroundColorResource; 56 | this.foregroundColorResource = foregroundColorResource; 57 | this.tagColorsResource = tagColorsResource; 58 | this.spinnerColorResource = spinnerColorResource; 59 | this.bubbleBackgroundColorResource = bubbleBackgroundColorResource; 60 | this.useLightProgressBar = useLightProgressBar; 61 | this.selectedColorResource = selectedColorResource; 62 | } 63 | 64 | public static ColorScheme findByPreferenceName(String name, Context context) { 65 | if (preferenceNameToColorScheme.isEmpty()) { 66 | // initialize map 67 | for (ColorScheme colorScheme : values()) { 68 | preferenceNameToColorScheme.put(context.getText(colorScheme.getNameResource()).toString(), colorScheme); 69 | } 70 | } 71 | return preferenceNameToColorScheme.get(name); 72 | } 73 | 74 | public String getDisplayableName(Context context) { 75 | 76 | CharSequence[] themeChoiceValues = context.getResources().getStringArray(R.array.pref_theme_choices_values); 77 | int idx = ArrayUtil.indexOf(themeChoiceValues, context.getString(nameResource)); 78 | return context.getResources().getStringArray(R.array.pref_theme_choices_names)[idx]; 79 | 80 | } 81 | 82 | public int getNameResource() { 83 | return nameResource; 84 | } 85 | 86 | public int getSelectedColor(Context context) { 87 | if (selectedColor == -1) { 88 | selectedColor = context.getResources().getColor(selectedColorResource); 89 | } 90 | return selectedColor; 91 | } 92 | 93 | public int getBackgroundColor(Context context) { 94 | if (backgroundColor == -1) { 95 | backgroundColor = context.getResources().getColor(backgroundColorResource); 96 | } 97 | return backgroundColor; 98 | } 99 | 100 | public int getForegroundColor(Context context) { 101 | if (foregroundColor == -1) { 102 | foregroundColor = context.getResources().getColor(foregroundColorResource); 103 | } 104 | return foregroundColor; 105 | } 106 | 107 | public int[] getTagColors(Context context) { 108 | if (tagColors == null) { 109 | tagColors = context.getResources().getIntArray(tagColorsResource); 110 | } 111 | return tagColors; 112 | } 113 | 114 | public int getSpinnerColor(Context context) { 115 | if (spinnerColor == -1) { 116 | spinnerColor = context.getResources().getColor(spinnerColorResource); 117 | } 118 | return spinnerColor; 119 | } 120 | 121 | public int getBubbleBackgroundColor(Context context) { 122 | if (bubbleBackgroundColor == -1) { 123 | bubbleBackgroundColor = context.getResources().getColor(bubbleBackgroundColorResource); 124 | } 125 | return bubbleBackgroundColor; 126 | } 127 | 128 | public boolean isUseLightProgressBar() { 129 | return useLightProgressBar; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/ui/RecordLogDialogActivity.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.ui; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.Dialog; 6 | import android.app.DialogFragment; 7 | import android.content.pm.PackageManager; 8 | import android.os.Bundle; 9 | import android.support.annotation.NonNull; 10 | import android.support.v4.app.ActivityCompat; 11 | import android.support.v4.content.ContextCompat; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.widget.Toast; 14 | 15 | import com.afollestad.materialdialogs.MaterialDialog; 16 | import com.pluscubed.logcat.R; 17 | import com.pluscubed.logcat.data.FilterQueryWithLevel; 18 | import com.pluscubed.logcat.helper.DialogHelper; 19 | import com.pluscubed.logcat.helper.PreferenceHelper; 20 | import com.pluscubed.logcat.helper.WidgetHelper; 21 | import com.pluscubed.logcat.util.Callback; 22 | 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | public class RecordLogDialogActivity extends AppCompatActivity { 27 | 28 | public static final String EXTRA_QUERY_SUGGESTIONS = "suggestions"; 29 | public static final int REQUEST_STORAGE_PERMISSIONS = 101; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | 35 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 36 | != PackageManager.PERMISSION_GRANTED) { 37 | ActivityCompat.requestPermissions(this, 38 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 39 | 101); 40 | } else { 41 | showDialog(); 42 | } 43 | } 44 | 45 | private void showDialog() { 46 | final String[] suggestions = (getIntent() != null && getIntent().hasExtra(EXTRA_QUERY_SUGGESTIONS)) 47 | ? getIntent().getStringArrayExtra(EXTRA_QUERY_SUGGESTIONS) : new String[]{}; 48 | 49 | DialogFragment fragment = ShowRecordLogDialog.newInstance(suggestions); 50 | fragment.show(getFragmentManager(), "showRecordLogDialog"); 51 | } 52 | 53 | @Override 54 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 55 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 56 | 57 | if (requestCode == REQUEST_STORAGE_PERMISSIONS) { 58 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 59 | showDialog(); 60 | } else { 61 | finish(); 62 | } 63 | } 64 | } 65 | 66 | public static class ShowRecordLogDialog extends DialogFragment { 67 | 68 | public static final String QUERY_SUGGESTIONS = "suggestions"; 69 | 70 | public static ShowRecordLogDialog newInstance(String[] suggestions) { 71 | ShowRecordLogDialog dialog = new ShowRecordLogDialog(); 72 | Bundle args = new Bundle(); 73 | args.putStringArray(QUERY_SUGGESTIONS, suggestions); 74 | dialog.setArguments(args); 75 | return dialog; 76 | } 77 | 78 | @Override 79 | public void onResume() { 80 | super.onResume(); 81 | getDialog().setCancelable(false); 82 | getDialog().setCanceledOnTouchOutside(false); 83 | } 84 | 85 | @Override 86 | public Dialog onCreateDialog(Bundle savedInstanceState) { 87 | //noinspection ConstantConditions 88 | final List suggestions = Arrays.asList(getArguments().getStringArray(QUERY_SUGGESTIONS)); 89 | 90 | String logFilename = DialogHelper.createLogFilename(); 91 | 92 | String defaultLogLevel = Character.toString(PreferenceHelper.getDefaultLogLevelPreference(getActivity())); 93 | final StringBuilder queryFilterText = new StringBuilder(); 94 | final StringBuilder logLevelText = new StringBuilder(defaultLogLevel); 95 | final Activity activity = getActivity(); 96 | MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) 97 | .title(R.string.record_log) 98 | .content(R.string.enter_filename) 99 | .input("", logFilename, new MaterialDialog.InputCallback() { 100 | @Override 101 | public void onInput(MaterialDialog materialDialog, CharSequence charSequence) { 102 | if (DialogHelper.isInvalidFilename(charSequence)) { 103 | Toast.makeText(getActivity(), R.string.enter_good_filename, Toast.LENGTH_SHORT).show(); 104 | } else { 105 | materialDialog.dismiss(); 106 | String filename = charSequence.toString(); 107 | Runnable runnable = new Runnable() { 108 | @Override 109 | public void run() { 110 | activity.finish(); 111 | } 112 | }; 113 | DialogHelper.startRecordingWithProgressDialog(filename, 114 | queryFilterText.toString(), logLevelText.toString(), runnable, getActivity()); 115 | } 116 | } 117 | }) 118 | .neutralText(R.string.text_filter_ellipsis) 119 | .negativeText(android.R.string.cancel) 120 | .autoDismiss(false) 121 | .callback(new MaterialDialog.ButtonCallback() { 122 | @Override 123 | public void onAny(MaterialDialog dialog) { 124 | super.onAny(dialog); 125 | WidgetHelper.updateWidgets(getActivity()); 126 | } 127 | 128 | @Override 129 | public void onNeutral(MaterialDialog dialog) { 130 | super.onNeutral(dialog); 131 | DialogHelper.showFilterDialogForRecording(getActivity(), queryFilterText.toString(), 132 | logLevelText.toString(), suggestions, 133 | new Callback() { 134 | @Override 135 | public void onCallback(FilterQueryWithLevel result) { 136 | queryFilterText.replace(0, queryFilterText.length(), result.getFilterQuery()); 137 | logLevelText.replace(0, logLevelText.length(), result.getLogLevel()); 138 | } 139 | }); 140 | } 141 | 142 | @Override 143 | public void onNegative(MaterialDialog dialog) { 144 | super.onNegative(dialog); 145 | dialog.dismiss(); 146 | getActivity().finish(); 147 | } 148 | }).build(); 149 | //noinspection ConstantConditions 150 | DialogHelper.initFilenameInputDialog(dialog); 151 | 152 | return dialog; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/res/raw/changelog.htm: -------------------------------------------------------------------------------- 1 |


2 | 3 |

Changelog

4 | 1.0.2 5 |
    6 |
  • Properly request storage permissions on Marshmallow+
  • 7 |
8 | 9 | 1.0.1 10 |
    11 |
  • Temporary "fix" for rotation
  • 12 |
  • Instruction dialog for granting log-reading permission on non-rooted device
  • 13 |
  • Internal library updates
  • 14 |
15 | 16 | 1.0.0 17 |
    18 |
  • Initial release!
  • 19 |
  • Material goodness.
  • 20 |
21 | 22 |
23 | 24 |

Original CatLog Changelog

25 | 1.6.0 26 | 31 | 32 | 1.5.0 33 |
    34 |
  • Polish translations by Paweł Radtke.
  • 35 |
  • Added "Copy to Clipboard" into context menu of log entry. (issue #36). 37 |
  • 38 |
39 | 40 | 1.4.4 41 | 52 | 53 | 1.4.3 54 |
    55 |
  • Improved Japanese translations, thanks to GitHub user 56 | Tachiken. 57 |
  • 58 |
59 | 60 | 1.4.2 61 |
    62 |
  • Allow for spaces in tag names, using e.g. tag:"My Tag Name". 63 |
  • 64 |
  • Add Swedish translations, thanks to GitHub user 65 | YraFyra. 66 |
  • 67 |
68 | 69 | 1.4.1 70 | 74 | 75 | 1.4 76 |
    77 |
  • Request super user access on Jelly Bean devices.
  • 78 |
  • Open-sourced the app.
  • 79 |
80 | 81 | 82 | 1.3.6 83 |
    84 |
  • Fix a potential UI bug introduced by 1.3.5.
  • 85 |
86 | 87 | 1.3.5 88 |
    89 |
  • UI performance boost. CatLog should load more quickly at startup now.
  • 90 |
  • Fixed a potential OutOfMemoryError bug.
  • 91 |
92 | 93 | 1.3.4 94 | 95 |
    96 |
  • Filters can be applied when recording.
  • 97 |
  • Support "pid:" and "tag:" keywords in searches.
  • 98 |
  • UI beautification - used faux "action bar" instead of buttons.
  • 99 |
100 | 101 | 1.3.3 102 | 103 |
    104 |
  • Device information can be included in log reports.
  • 105 |
  • New "log display limit" setting.
  • 106 |
  • Got rid of extraneous "kill failed" message in ICS.
  • 107 |
  • Performance improvements.
  • 108 |
109 | 110 | 111 | 1.3.2 112 | 113 |
    114 |
  • CatLog can be invoked from an external Intent.
  • 115 |
116 | 117 | 1.3.1 118 | 119 |
    120 |
  • New "default log level" setting.
  • 121 |
122 | 123 | 1.3.0 124 | 125 |
    126 |
  • Small graphical fix.
  • 127 |
128 | 129 | 1.2.9 130 | 131 |
    132 |
  • Added filters.
  • 133 |
134 | 135 | 1.2.8 136 | 137 |
    138 |
  • Reduced APK file size by about 30%.
  • 139 |
140 | 141 | 1.2.7 142 | 143 |
    144 |
  • The search box now autosuggests tag names.
  • 145 |
146 | 147 | 1.2.6 148 | 149 |
    150 |
  • Fix a bug that was causing some of the Logcat processes to not be properly destroyed.
  • 151 |
152 | 153 | 1.2.5 154 | 155 |
    156 |
  • Any number of buffers can now be shown simultaneously.
  • 157 |
158 | 159 | 1.2.4 160 | 161 |
    162 |
  • Added pause/unpause button.
  • 163 |
164 | 165 | 1.2.3 166 | 167 |
    168 |
  • Added "All Combined" buffer. For performance reasons, I make 169 | no guarantee that the logs will be perfectly interleaved 170 | in chronological order. 171 |
  • 172 |
  • Added fast scroll thumb.
  • 173 |
  • Fixed bug for nonstandard logs on ZTE Blade. Thanks, Andrew!
  • 174 |
175 | 176 | 1.2.1/1.2.2 177 | 178 |
    179 |
  • Japanese and French localizations added (to everything but the changelog).
  • 180 |
181 | 182 | 1.2 183 | 184 |
    185 |
  • Minor bugfix.
  • 186 |
187 | 188 | 1.1.9 189 | 190 |
    191 |
  • Minor bugfixes and graphical tweaks.
  • 192 |
193 | 194 | 1.1.8 195 | 196 |
    197 |
  • Added log buffer option (main, events, and radio) to the settings.
  • 198 |
199 | 200 | 1.1.7 201 | 202 |
    203 |
  • Graphical tweaks to support different screen sizes.
  • 204 |
  • Bugfixes.
  • 205 |
206 | 207 | 1.1.6 208 | 209 |
    210 |
  • Significant performance improvements.
  • 211 |
  • Bugfixes.
  • 212 |
213 | 214 | 1.1.5 215 | 216 |
    217 |
  • Added "Select Partial" mode.
  • 218 |
  • UI tweaks.
  • 219 |
220 | 221 | 1.1.4 222 | 223 |
    224 |
  • Small bugfix.
  • 225 |
226 | 227 | 1.1.3 228 | 229 |
    230 |
  • Modified the interface to make it more intuitive.
  • 231 |
  • Added four new color schemes: Big Red, Ma Bell, Big Yellow, and T-Mo.
  • 232 |
233 | 234 | 1.1.1-2 235 | 236 |
    237 |
  • Small bugfixes.
  • 238 |
239 | 240 | 1.1 241 | 242 |
    243 |
  • Added a donate-only "color scheme" setting, just as a little thank-you to the folks who 244 | donated. :) 245 |
  • 246 |
247 | 248 | 1.0.8 249 | 250 |
    251 |
  • Added "periodic write" setting.
  • 252 |
  • Bugfixes and performance improvements.
  • 253 |
254 | 255 | 1.0.7 256 | 257 |
    258 |
  • Added long-press to search by tag/pid.
  • 259 |
  • Fixed NPE in recording widget.
  • 260 |
261 | 262 | 1.0.6 263 | 264 |
    265 |
  • Fixed an out-of-memory bug. Now you can leave the CatLog recorder on all day, if you want! 266 |
  • 267 |
  • Added search by process id.
  • 268 |
269 | 270 | 1.0.5 271 | 272 |
    273 |
  • More bugfixes. Thanks to the folks who report these bugs!
  • 274 |
275 | 276 | 1.0.4 277 | 278 |
    279 |
  • Fixed bug that was causing it to freeze.
  • 280 |
281 | 282 | 1.0.3 283 | 284 |
    285 |
  • Added optional timestamps and process ids when in "expanded" mode.
  • 286 |
287 | 288 | 1.0.2 289 | 290 |
    291 |
  • Added a widget to start/stop recording logs. Please note that Android widgets do not 292 | work 293 | if you've ever moved the app to the SD card. 294 |
  • 295 |
  • Fixed bug with text sizes on mdpi devices.
  • 296 |
297 | 298 | 299 | 1.0.1 300 | 301 |
    302 |
  • Logs can now be sent as either text or attachments.
  • 303 |
  • Added text size and "expand by default" settings.
  • 304 |
-------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/util/StringUtil.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.util; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * @author nolan 10 | */ 11 | public class StringUtil { 12 | 13 | /** 14 | * Pad the specified number of spaces to the input string to make it that length 15 | * 16 | * @param input 17 | * @param size 18 | * @return 19 | */ 20 | public static String padLeft(String input, int size) { 21 | 22 | if (input.length() > size) { 23 | throw new IllegalArgumentException("input must be shorter than or equal to the number of spaces: " + size); 24 | } 25 | 26 | StringBuilder sb = new StringBuilder(); 27 | for (int i = input.length(); i < size; i++) { 28 | sb.append(" "); 29 | } 30 | return sb.append(input).toString(); 31 | } 32 | 33 | /** 34 | * same as the String.split(), except it doesn't use regexes, so it's faster. 35 | * 36 | * @param str - the string to split up 37 | * @param delimiter the delimiter 38 | * @return the split string 39 | */ 40 | public static String[] split(String str, String delimiter) { 41 | List result = new ArrayList(); 42 | int lastIndex = 0; 43 | int index = str.indexOf(delimiter); 44 | while (index != -1) { 45 | result.add(str.substring(lastIndex, index)); 46 | lastIndex = index + delimiter.length(); 47 | index = str.indexOf(delimiter, index + delimiter.length()); 48 | } 49 | result.add(str.substring(lastIndex, str.length())); 50 | 51 | return ArrayUtil.toArray(result, String.class); 52 | } 53 | 54 | /* 55 | * Replace all occurances of the searchString in the originalString with the replaceString. Faster than the 56 | * String.replace() method. Does not use regexes. 57 | *

58 | * If your searchString is empty, this will spin forever. 59 | * 60 | * 61 | * @param originalString 62 | * @param searchString 63 | * @param replaceString 64 | * @return 65 | */ 66 | public static String replace(String originalString, String searchString, String replaceString) { 67 | StringBuilder sb = new StringBuilder(originalString); 68 | int index = sb.indexOf(searchString); 69 | while (index != -1) { 70 | sb.replace(index, index + searchString.length(), replaceString); 71 | index += replaceString.length(); 72 | index = sb.indexOf(searchString, index); 73 | } 74 | return sb.toString(); 75 | } 76 | 77 | public static String join(String delimiter, String[] strings) { 78 | 79 | if (strings.length == 0) { 80 | return ""; 81 | } 82 | 83 | StringBuilder stringBuilder = new StringBuilder(); 84 | for (String str : strings) { 85 | stringBuilder.append(" ").append(str); 86 | } 87 | 88 | return stringBuilder.substring(1); 89 | } 90 | 91 | 92 | public static int computeLevenshteinDistance(CharSequence str1, CharSequence str2) { 93 | 94 | 95 | int commonPrefixLength = findCommonPrefixLength(str1, str2); 96 | 97 | if (commonPrefixLength == str1.length() && commonPrefixLength == str2.length()) { 98 | return 0; // same exact string 99 | } 100 | 101 | int commonSuffixLength = findCommonSuffixLength(str1, str2, commonPrefixLength); 102 | 103 | str1 = str1.subSequence(commonPrefixLength, str1.length() - commonSuffixLength); 104 | str2 = str2.subSequence(commonPrefixLength, str2.length() - commonSuffixLength); 105 | 106 | int[][] distance = new int[str1.length() + 1][str2.length() + 1]; 107 | 108 | for (int i = 0; i <= str1.length(); i++) { 109 | distance[i][0] = i; 110 | } 111 | for (int j = 0; j <= str2.length(); j++) { 112 | distance[0][j] = j; 113 | } 114 | 115 | for (int i = 1; i <= str1.length(); i++) { 116 | for (int j = 1; j <= str2.length(); j++) { 117 | distance[i][j] = minimum( 118 | distance[i - 1][j] + 1, 119 | distance[i][j - 1] + 1, 120 | distance[i - 1][j - 1] + ((str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 121 | : 1)); 122 | } 123 | } 124 | 125 | int dist = distance[str1.length()][str2.length()]; 126 | 127 | 128 | return dist; 129 | } 130 | 131 | private static int findCommonPrefixLength(CharSequence str1, CharSequence str2) { 132 | 133 | int length = (Math.min(str1.length(), str2.length())); 134 | for (int i = 0; i < length; i++) { 135 | if (str1.charAt(i) != str2.charAt(i)) { 136 | return i; 137 | } 138 | } 139 | 140 | return 0; 141 | 142 | } 143 | 144 | private static int findCommonSuffixLength(CharSequence str1, CharSequence str2, int commonPrefixLength) { 145 | int length = (Math.min(str1.length(), str2.length())); 146 | for (int i = 0; i < length - commonPrefixLength; i++) { 147 | if (str1.charAt(str1.length() - i - 1) != str2.charAt(str2.length() - i - 1)) { 148 | return i; 149 | } 150 | } 151 | 152 | return 0; 153 | } 154 | 155 | private static int minimum(int a, int b, int c) { 156 | return Math.min(Math.min(a, b), c); 157 | } 158 | 159 | public static String join(int[] arr, String delimiter) { 160 | 161 | if (arr.length == 0) { 162 | return ""; 163 | } 164 | 165 | StringBuilder sb = new StringBuilder(); 166 | 167 | for (int i : arr) { 168 | sb.append(delimiter).append(Integer.toString(i)); 169 | } 170 | 171 | return sb.substring(delimiter.length()); 172 | 173 | } 174 | 175 | public static String capitalize(String str) { 176 | 177 | StringBuilder sb = new StringBuilder(str); 178 | 179 | for (int i = 0; i < sb.length(); i++) { 180 | if (i == 0 || Character.isWhitespace(sb.charAt(i - 1))) { 181 | sb.replace(i, i + 1, Character.toString(Character.toUpperCase(sb.charAt(i)))); 182 | } 183 | } 184 | 185 | return sb.toString(); 186 | } 187 | 188 | public static String nullToEmpty(CharSequence str) { 189 | return str == null ? "" : str.toString(); 190 | } 191 | 192 | public static boolean isEmptyOrWhitespaceOnly(String str) { 193 | if (TextUtils.isEmpty(str)) { 194 | return true; 195 | } 196 | for (int i = 0; i < str.length(); i++) { 197 | if (!Character.isWhitespace(str.charAt(i))) { 198 | return false; 199 | } 200 | } 201 | return true; 202 | } 203 | 204 | /** 205 | * same as String.contains, but ignores case. 206 | * 207 | * @param str 208 | * @param query 209 | * @return 210 | */ 211 | public static boolean containsIgnoreCase(String str, String query) { 212 | if (str != null && query != null) { 213 | int limit = str.length() - query.length() + 1; 214 | for (int i = 0; i < limit; i++) { 215 | if (matchesIgnoreCase(str, query, i)) { 216 | return true; 217 | } 218 | } 219 | } 220 | return false; 221 | } 222 | 223 | private static boolean matchesIgnoreCase(String str, String query, int startingAt) { 224 | int len = query.length(); 225 | for (int i = 0; i < len; i++) { 226 | if (Character.toUpperCase(query.charAt(i)) != Character.toUpperCase(str.charAt(startingAt + i))) { 227 | return false; 228 | } 229 | } 230 | return true; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /app/src/main/java/com/pluscubed/logcat/helper/DialogHelper.java: -------------------------------------------------------------------------------- 1 | package com.pluscubed.logcat.helper; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.text.InputType; 8 | import android.text.TextUtils; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.inputmethod.EditorInfo; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.AutoCompleteTextView; 14 | import android.widget.EditText; 15 | import android.widget.Spinner; 16 | 17 | import com.afollestad.materialdialogs.MaterialDialog; 18 | import com.pluscubed.logcat.R; 19 | import com.pluscubed.logcat.data.FilterQueryWithLevel; 20 | import com.pluscubed.logcat.data.SortedFilterArrayAdapter; 21 | import com.pluscubed.logcat.util.ArrayUtil; 22 | import com.pluscubed.logcat.util.Callback; 23 | 24 | import java.text.DecimalFormat; 25 | import java.util.Calendar; 26 | import java.util.Date; 27 | import java.util.GregorianCalendar; 28 | import java.util.List; 29 | 30 | public class DialogHelper { 31 | 32 | public static void startRecordingWithProgressDialog(final String filename, 33 | final String filterQuery, final String logLevel, final Runnable onPostExecute, final Context context) { 34 | 35 | final MaterialDialog progressDialog = new MaterialDialog.Builder(context) 36 | .title(R.string.dialog_please_wait) 37 | .content(R.string.dialog_initializing_recorder) 38 | .progress(true, -1) 39 | .build(); 40 | 41 | progressDialog.setCancelable(false); 42 | progressDialog.setCanceledOnTouchOutside(false); 43 | 44 | final Handler handler = new Handler(Looper.getMainLooper()); 45 | progressDialog.show(); 46 | new Thread(new Runnable() { 47 | @Override 48 | public void run() { 49 | ServiceHelper.startBackgroundServiceIfNotAlreadyRunning(context, filename, filterQuery, logLevel); 50 | handler.post(new Runnable() { 51 | @Override 52 | public void run() { 53 | if (progressDialog.isShowing()) { 54 | progressDialog.dismiss(); 55 | } 56 | if (onPostExecute != null) { 57 | onPostExecute.run(); 58 | } 59 | } 60 | }); 61 | } 62 | }).start(); 63 | 64 | } 65 | 66 | public static boolean isInvalidFilename(CharSequence filename) { 67 | 68 | String filenameAsString; 69 | 70 | return TextUtils.isEmpty(filename) 71 | || (filenameAsString = filename.toString()).contains("/") 72 | || filenameAsString.contains(":") 73 | || filenameAsString.contains(" ") 74 | || !filenameAsString.endsWith(".txt"); 75 | 76 | } 77 | 78 | public static void showFilterDialogForRecording(final Context context, final String queryFilterText, 79 | final String logLevelText, final List filterQuerySuggestions, 80 | final Callback callback) { 81 | 82 | LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 83 | @SuppressLint("InflateParams") View filterView = inflater.inflate(R.layout.dialog_recording_filter, null, false); 84 | 85 | // add suggestions to autocompletetextview 86 | final AutoCompleteTextView autoCompleteTextView = (AutoCompleteTextView) filterView.findViewById(android.R.id.text1); 87 | autoCompleteTextView.setText(queryFilterText); 88 | 89 | SortedFilterArrayAdapter suggestionAdapter = new SortedFilterArrayAdapter( 90 | context, R.layout.list_item_dropdown, filterQuerySuggestions); 91 | autoCompleteTextView.setAdapter(suggestionAdapter); 92 | 93 | // set values on spinner to be the log levels 94 | final Spinner spinner = (Spinner) filterView.findViewById(R.id.spinner); 95 | 96 | // put the word "default" after whatever the default log level is 97 | CharSequence[] logLevels = context.getResources().getStringArray(R.array.log_levels); 98 | String defaultLogLevel = Character.toString(PreferenceHelper.getDefaultLogLevelPreference(context)); 99 | int index = ArrayUtil.indexOf(context.getResources().getStringArray(R.array.log_levels_values), defaultLogLevel); 100 | logLevels[index] = logLevels[index].toString() + " " + context.getString(R.string.default_in_parens); 101 | 102 | ArrayAdapter adapter = new ArrayAdapter<>( 103 | context, android.R.layout.simple_spinner_item, logLevels); 104 | adapter.setDropDownViewResource(R.layout.list_item_dropdown); 105 | spinner.setAdapter(adapter); 106 | 107 | // in case the user has changed it, choose the pre-selected log level 108 | spinner.setSelection(ArrayUtil.indexOf(context.getResources().getStringArray(R.array.log_levels_values), 109 | logLevelText)); 110 | 111 | // create alertdialog for the "Filter..." button 112 | new MaterialDialog.Builder(context) 113 | .title(R.string.title_filter) 114 | .customView(filterView, true) 115 | .negativeText(android.R.string.cancel) 116 | .positiveText(android.R.string.ok) 117 | .callback(new MaterialDialog.ButtonCallback() { 118 | @Override 119 | public void onPositive(MaterialDialog dialog) { 120 | super.onPositive(dialog); 121 | // get the true log level value, as opposed to the display string 122 | int logLevelIdx = spinner.getSelectedItemPosition(); 123 | String[] logLevelValues = context.getResources().getStringArray(R.array.log_levels_values); 124 | String logLevelValue = logLevelValues[logLevelIdx]; 125 | 126 | String filterQuery = autoCompleteTextView.getText().toString(); 127 | 128 | callback.onCallback(new FilterQueryWithLevel(filterQuery, logLevelValue)); 129 | } 130 | }).show(); 131 | 132 | } 133 | 134 | public static void stopRecordingLog(Context context) { 135 | ServiceHelper.stopBackgroundServiceIfRunning(context); 136 | } 137 | 138 | 139 | public static void showFilenameSuggestingDialog(final Context context, 140 | final MaterialDialog.ButtonCallback callback, final MaterialDialog.InputCallback inputCallback, int titleResId) { 141 | 142 | 143 | MaterialDialog.Builder builder = new MaterialDialog.Builder(context); 144 | builder.title(titleResId) 145 | .negativeText(android.R.string.cancel) 146 | .positiveText(android.R.string.ok) 147 | .content(R.string.enter_filename) 148 | .input("", "", inputCallback) 149 | .callback(callback); 150 | 151 | MaterialDialog show = builder.show(); 152 | initFilenameInputDialog(show); 153 | } 154 | 155 | public static void initFilenameInputDialog(MaterialDialog show) { 156 | final EditText editText = show.getInputEditText(); 157 | editText.setSingleLine(); 158 | editText.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); 159 | editText.setImeOptions(EditorInfo.IME_ACTION_DONE); 160 | 161 | // create an initial filename to suggest to the user 162 | String filename = createLogFilename(); 163 | editText.setText(filename); 164 | 165 | // highlight everything but the .txt at the end 166 | editText.setSelection(0, filename.length() - 4); 167 | } 168 | 169 | public static String createLogFilename() { 170 | Date date = new Date(); 171 | GregorianCalendar calendar = new GregorianCalendar(); 172 | calendar.setTime(date); 173 | 174 | DecimalFormat twoDigitDecimalFormat = new DecimalFormat("00"); 175 | DecimalFormat fourDigitDecimalFormat = new DecimalFormat("0000"); 176 | 177 | String year = fourDigitDecimalFormat.format(calendar.get(Calendar.YEAR)); 178 | String month = twoDigitDecimalFormat.format(calendar.get(Calendar.MONTH) + 1); 179 | String day = twoDigitDecimalFormat.format(calendar.get(Calendar.DAY_OF_MONTH)); 180 | String hour = twoDigitDecimalFormat.format(calendar.get(Calendar.HOUR_OF_DAY)); 181 | String minute = twoDigitDecimalFormat.format(calendar.get(Calendar.MINUTE)); 182 | String second = twoDigitDecimalFormat.format(calendar.get(Calendar.SECOND)); 183 | 184 | return year + "-" + month + "-" + day + "-" + hour + "-" + minute + "-" + second + ".txt"; 185 | } 186 | 187 | } 188 | --------------------------------------------------------------------------------