package com.nolanlawson.logcat;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.ClipboardManager;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnActionExpandListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.Filter;
import android.widget.Filter.FilterListener;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import com.nolanlawson.logcat.data.ColorScheme;
import com.nolanlawson.logcat.data.FilterAdapter;
import com.nolanlawson.logcat.data.LogFileAdapter;
import com.nolanlawson.logcat.data.LogLine;
import com.nolanlawson.logcat.data.LogLineAdapter;
import com.nolanlawson.logcat.data.SavedLog;
import com.nolanlawson.logcat.data.SearchCriteria;
import com.nolanlawson.logcat.data.SendLogDetails;
import com.nolanlawson.logcat.data.SenderAppAdapter;
import com.nolanlawson.logcat.data.SortedFilterArrayAdapter;
import com.nolanlawson.logcat.data.TagAndProcessIdAdapter;
import com.nolanlawson.logcat.db.CatlogDBHelper;
import com.nolanlawson.logcat.db.FilterItem;
import com.nolanlawson.logcat.helper.BuildHelper;
import com.nolanlawson.logcat.helper.DialogHelper;
import com.nolanlawson.logcat.helper.PreferenceHelper;
import com.nolanlawson.logcat.helper.SaveLogHelper;
import com.nolanlawson.logcat.helper.ServiceHelper;
import com.nolanlawson.logcat.helper.UpdateHelper;
import com.nolanlawson.logcat.intents.Intents;
import com.nolanlawson.logcat.reader.LogcatReader;
import com.nolanlawson.logcat.reader.LogcatReaderLoader;
import com.nolanlawson.logcat.util.ArrayUtil;
import com.nolanlawson.logcat.util.LogLineAdapterUtil;
import com.nolanlawson.logcat.util.StringUtil;
import com.nolanlawson.logcat.util.UtilLogger;
import java.util.Arrays;
@SuppressWarnings("deprecation")
public class LogcatActivity extends ListActivity implements TextWatcher,
OnScrollListener, FilterListener, OnEditorActionListener,
OnClickListener, OnLongClickListener {
private static final int REQUEST_CODE_SETTINGS = 1;
// how often to check to see if we've gone over the max size
private static final int UPDATE_CHECK_INTERVAL = 200;
// how many suggestions to keep in the autosuggestions text
private static final int MAX_NUM_SUGGESTIONS = 1000;
// id for context menu entry
private static final int CONTEXT_MENU_FILTER_ID = 0;
private static final int CONTEXT_MENU_COPY_ID = 1;
private static UtilLogger log = new UtilLogger(LogcatActivity.class);
private View backgroundLayout, mainFilenameLayout, clearButton,
expandButton, pauseButton;
private AutoCompleteTextView searchEditText;
private ProgressBar darkProgressBar, lightProgressBar;
private LogLineAdapter adapter;
private LogReaderAsyncTask task;
private ImageView expandButtonImage, pauseButtonImage;
private TextView filenameTextView;
private View borderView1, borderView2, borderView3, borderView4;
private int firstVisibleItem = -1;
private boolean autoscrollToBottom = true;
private boolean collapsedMode;
private boolean partialSelectMode;
private List<LogLine> partiallySelectedLogLines = new ArrayList<LogLine>(2);
private Set<String> searchSuggestionsSet = new HashSet<String>();
private SortedFilterArrayAdapter<String> searchSuggestionsAdapter;
private String currentlyOpenLog = null;
private Handler handler = new Handler(Looper.getMainLooper());
private MenuItem menuPauseButton;
private MenuItem menuExpandButton;
private AutoCompleteTextView searchView;
protected FilterAdapter filterAdapter;
private MenuItem searchViewItem;
protected boolean isAutoOpen = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
collapsedMode = !PreferenceHelper.getExpandedByDefaultPreference(this);
log.d("initial collapsed mode is %s", collapsedMode);
registerForContextMenu(getListView());
setUpWidgets();
setUpAdapter();
updateBackgroundColor();
runUpdatesIfNecessaryAndShowInitialMessage();
}
private void runUpdatesIfNecessaryAndShowInitialMessage() {
if (UpdateHelper.areUpdatesNecessary(this)) {
// show progress dialog while updates are running
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage(getString(R.string.dialog_loading_updates));
dialog.show();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
UpdateHelper.runUpdatesIfNecessary(LogcatActivity.this);
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
showInitialMessageAndStartupLog();
}
}.execute((Void) null);
} else {
showInitialMessageAndStartupLog();
}
}
private void addFiltersToSuggestions() {
CatlogDBHelper dbHelper = null;
try {
dbHelper = new CatlogDBHelper(this);
for (FilterItem filterItem : dbHelper.findFilterItems()) {
addToAutocompleteSuggestions(filterItem.getText());
}
} finally {
if (dbHelper != null) {
dbHelper.close();
}
}
}
private void showInitialMessageAndStartupLog() {
Intent intent = getIntent();
if (intent == null || !intent.hasExtra("filename")) {
startUpMainLog();
} else {
String filename = intent.getStringExtra("filename");
openLog(filename);
}
boolean isFirstRun = PreferenceHelper
.getFirstRunPreference(getApplicationContext());
if (isFirstRun) {
View view = View.inflate(this, R.layout.intro_dialog, null);
TextView textView = (TextView) view
.findViewById(R.id.first_run_text_view_2);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setLinkTextColor(ColorStateList.valueOf(getResources()
.getColor(R.color.linkColorBlue)));
new AlertDialog.Builder(this)
.setTitle(R.string.first_run_title)
.setView(view)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
PreferenceHelper.setFirstRunPreference(
getApplicationContext(), false);
dialog.dismiss();
doAfterInitialMessage(getIntent());
}
}).setCancelable(false).setIcon(R.drawable.icon)
.show();
} else {
doAfterInitialMessage(getIntent());
}
}
private void doAfterInitialMessage(Intent intent) {
// handle an intent that was sent from an external application
if (intent != null && Intents.ACTION_LAUNCH.equals(intent.getAction())) {
String filter = intent.getStringExtra(Intents.EXTRA_FILTER);
String level = intent.getStringExtra(Intents.EXTRA_LEVEL);
if (!TextUtils.isEmpty(filter)) {
silentlySetSearchText(filter);
}
if (!TextUtils.isEmpty(level)) {
CharSequence[] logLevels = getResources().getStringArray(
R.array.log_levels_values);
int logLevelLimit = ArrayUtil.indexOf(logLevels,
level.toUpperCase(Locale.US));
if (logLevelLimit == -1) {
String invalidLevel = String.format(
getString(R.string.toast_invalid_level), level);
Toast.makeText(this, invalidLevel, Toast.LENGTH_LONG)
.show();
} else {
adapter.setLogLevelLimit(logLevelLimit);
logLevelChanged();
}
}
}
}
@Override
public void onResume() {
super.onResume();
if (getListView().getCount() > 0) {
// scroll to bottom, since for some reason it always scrolls to the
// top, which is annoying
getListView().setSelection(getListView().getCount() - 1);
}
}
private void restartMainLog() {
adapter.clear();
startUpMainLog();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
doAfterInitialMessage(intent);
// launched from the widget or notification
if (intent != null && !Intents.ACTION_LAUNCH.equals(intent.getAction())
&& intent.hasExtra("filename")) {
String filename = intent.getStringExtra("filename");
openLog(filename);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
log.d("onActivityResult()");
// preferences may have changed
PreferenceHelper.clearCache();
collapsedMode = !PreferenceHelper
.getExpandedByDefaultPreference(getApplicationContext());
if (requestCode == REQUEST_CODE_SETTINGS && resultCode == RESULT_OK) {
onSettingsActivityResult(data);
}
adapter.notifyDataSetChanged();
updateBackgroundColor();
updateDisplayedFilename();
}
private void onSettingsActivityResult(final Intent data) {
handler.post(new Runnable() {
@Override
public void run() {
updateBackgroundColor();
if (data.hasExtra("bufferChanged")
&& data.getBooleanExtra("bufferChanged", false)
&& currentlyOpenLog == null) {
// log buffer changed, so update list
restartMainLog();
} else {
// settings activity returned - text size might have
// changed, so update list
expandOrCollapseAll(false);
adapter.notifyDataSetChanged();
}
}
});
}
private void startUpMainLog() {
Runnable mainLogRunnable = new Runnable() {
@Override
public void run() {
if (adapter != null) {
adapter.clear();
}
task = new LogReaderAsyncTask();
task.execute((Void) null);
}
};
if (task != null) {
// do only after current log is depleted, to avoid splicing the
// streams together
// (Don't cross the streams!)
task.unpause();
task.setOnFinished(mainLogRunnable);
task.killReader();
task = null;
} else {
// no main log currently running; just start up the main log now
mainLogRunnable.run();
}
}
@Override
public void onPause() {
super.onPause();
log.d("onPause() called");
cancelPartialSelect();
}
@Override
public void onDestroy() {
super.onDestroy();
log.d("onDestroy() called");
if (task != null) {
task.unpause();
task.killReader();
task = null;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_reload:
startUpMainLog();
return true;
case R.id.menu_log_level:
showLogLevelDialog();
return true;
case R.id.menu_open_log:
showOpenLogDialog();
return true;
case R.id.menu_save_log:
case R.id.menu_save_as_log:
showSaveLogDialog();
return true;
case R.id.menu_record_log:
showRecordLogDialog();
return true;
case R.id.menu_stop_recording_log:
DialogHelper.stopRecordingLog(this);
return true;
case R.id.menu_send_log:
showSendLogDialog();
return true;
case R.id.menu_main_log:
startUpMainLog();
return true;
case R.id.menu_delete_saved_log:
startDeleteSavedLogsDialog();
return true;
case R.id.menu_settings:
startSettingsActivity();
return true;
case R.id.menu_crazy_logger_service:
ServiceHelper.startOrStopCrazyLogger(this);
return true;
case R.id.menu_partial_select:
startPartialSelectMode();
return true;
case R.id.menu_filters:
showFiltersDialog();
return true;
case R.id.menu_clear:
unfocusEditText();
if (adapter != null) {
adapter.clear();
}
if (searchEditText != null) {
searchEditText.setText("");
}
Toast.makeText(this, R.string.log_cleared, Toast.LENGTH_LONG)
.show();
return true;
case R.id.menu_pause:
pauseUnpause();
return true;
case R.id.menu_expand:
boolean open;
if (menuExpandButton.getIcon().equals(
getResources().getDrawable(
R.drawable.ic_action_navigation_expand))) {
open = false;
} else {
open = true;
}
expandOrCollapseAll(open);
return true;
}
return false;
}
@SuppressLint("NewApi")
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean showingMainLog = (task != null);
searchViewItem = menu.findItem(R.id.menu_search);
searchView = (AutoCompleteTextView) searchViewItem.getActionView();
MenuItem mainLogMenuItem = menu.findItem(R.id.menu_main_log);
MenuItem saveLogMenuItem = menu.findItem(R.id.menu_save_log);
MenuItem saveAsLogMenuItem = menu.findItem(R.id.menu_save_as_log);
menuPauseButton = menu.findItem(R.id.menu_pause);
menuExpandButton = menu.findItem(R.id.menu_expand);
menu.findItem(R.id.menu_clear);
final ActionBar bar = getActionBar();
bar.setDisplayOptions(
ActionBar.DISPLAY_SHOW_TITLE |
ActionBar.DISPLAY_USE_LOGO |
ActionBar.DISPLAY_SHOW_HOME
);
mainLogMenuItem.setEnabled(!showingMainLog);
mainLogMenuItem.setVisible(!showingMainLog);
List<String> bufferNames = PreferenceHelper.getBufferNames(this);
searchEditText = searchView;// (AutoCompleteTextView)
// findViewById(R.id.main_edit_text);
if (android.os.Build.VERSION.SDK_INT >= 14) {
searchViewItem
.setOnActionExpandListener(new OnActionExpandListener() {
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
// TODO Auto-generated method stub
searchEditText.setText("");
handleNewFilterText(searchEditText.getText()
.toString(), filterAdapter);
return true;
}
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
// TODO Auto-generated method stub
getActionBar().setIcon(R.drawable.icon);
if(!isAutoOpen){
searchEditText.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
}
isAutoOpen=false;
return true;
}
});
}
searchEditText.addTextChangedListener(this);
searchEditText.setOnEditorActionListener(this);
searchEditText.setOnClickListener(this);
searchEditText
.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(
searchEditText.getWindowToken(), 0);
}
});
searchSuggestionsAdapter = new SortedFilterArrayAdapter<String>(this,
R.layout.simple_dropdown_small, new ArrayList<String>());
searchEditText.setAdapter(searchSuggestionsAdapter);
// change the displayed menu name depending on how many logs are to be
// shown
String mainLogTitle;
switch (bufferNames.size()) {
case 1:
mainLogTitle = String.format(getString(R.string.play_x1_log),
bufferNames.get(0));
break;
case 2:
mainLogTitle = String.format(getString(R.string.play_x2_log),
bufferNames.get(0), bufferNames.get(1));
break;
default: // 3
mainLogTitle = getString(R.string.play_x3_log);
break;
}
mainLogMenuItem.setTitle(mainLogTitle);
saveLogMenuItem.setEnabled(showingMainLog);
saveLogMenuItem.setVisible(showingMainLog);
saveAsLogMenuItem.setEnabled(!showingMainLog);
saveAsLogMenuItem.setVisible(!showingMainLog);
boolean recordingInProgress = ServiceHelper.checkIfServiceIsRunning(
getApplicationContext(), LogcatRecordingService.class);
MenuItem recordMenuItem = menu.findItem(R.id.menu_record_log);
MenuItem stopRecordingMenuItem = menu
.findItem(R.id.menu_stop_recording_log);
recordMenuItem.setEnabled(!recordingInProgress);
recordMenuItem.setVisible(!recordingInProgress);
stopRecordingMenuItem.setEnabled(recordingInProgress);
stopRecordingMenuItem.setVisible(recordingInProgress);
MenuItem crazyLoggerMenuItem = menu
.findItem(R.id.menu_crazy_logger_service);
crazyLoggerMenuItem.setEnabled(UtilLogger.DEBUG_MODE);
crazyLoggerMenuItem.setVisible(UtilLogger.DEBUG_MODE);
MenuItem partialSelectMenuItem = menu
.findItem(R.id.menu_partial_select);
partialSelectMenuItem.setEnabled(!partialSelectMode);
partialSelectMenuItem.setVisible(!partialSelectMode);
return super.onPrepareOptionsMenu(menu);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenu.ContextMenuInfo menuInfo) {
menu.add(0, CONTEXT_MENU_FILTER_ID, 0, R.string.filter_choice);
menu.add(0, CONTEXT_MENU_COPY_ID, 0, R.string.copy_to_clipboard);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item
.getMenuInfo();
LogLine logLine = adapter.getItem(info.position);
if (logLine != null) {
switch (item.getItemId()) {
case CONTEXT_MENU_COPY_ID:
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(logLine.getOriginalLine());
Toast.makeText(this, R.string.copied_to_clipboard,
Toast.LENGTH_SHORT).show();
return true;
case CONTEXT_MENU_FILTER_ID:
if (logLine.getProcessId() == -1) {
// invalid line
return false;
}
showSearchByDialog(logLine);
return true;
}
}
return false;
}
private void showSearchByDialog(final LogLine logLine) {
List<CharSequence> choices = Arrays
.<CharSequence> asList(getResources().getStringArray(
R.array.filter_choices));
List<CharSequence> choicesSubtexts = Arrays.<CharSequence> asList(
logLine.getTag(), Integer.toString(logLine.getProcessId()));
int tagColor = LogLineAdapterUtil.getOrCreateTagColor(this,
logLine.getTag());
TagAndProcessIdAdapter textAndSubtextAdapter = new TagAndProcessIdAdapter(
this, choices, choicesSubtexts, tagColor, -1);
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.filter_choice)
.setSingleChoiceItems(textAndSubtextAdapter, -1,
new DialogInterface.OnClickListener() {
@SuppressLint("NewApi")
@Override
public void onClick(DialogInterface dialog,
int which) {
if (which == 0) { // tag
// determine the right way to phrase this
// tag query - e.g.
// tag:myTag or tag:"my tag"
String tagQuery = (logLine.getTag()
.contains(" ")) ? ('"' + logLine
.getTag() + '"') : logLine.getTag();
silentlySetSearchText(SearchCriteria.TAG_KEYWORD
+ tagQuery);
} else { // which == 1, i.e. process id
silentlySetSearchText(SearchCriteria.PID_KEYWORD
+ logLine.getProcessId());
}
// put the cursor at the end
if (android.os.Build.VERSION.SDK_INT >= 14){
isAutoOpen=true;
searchViewItem.expandActionView();
}
searchEditText.setSelection(searchEditText
.length());
dialog.dismiss();
}
}).show();
}
private void showRecordLogDialog() {
// start up the dialog-like activity
String[] suggestions = ArrayUtil.toArray(new ArrayList<String>(
searchSuggestionsSet), String.class);
Intent intent = new Intent(LogcatActivity.this,
ShowRecordLogDialogActivity.class);
intent.putExtra(ShowRecordLogDialogActivity.EXTRA_QUERY_SUGGESTIONS,
suggestions);
startActivity(intent);
}
private void showFiltersDialog() {
new AsyncTask<Void, Void, List<FilterItem>>() {
@Override
protected List<FilterItem> doInBackground(Void... params) {
List<FilterItem> filters = new ArrayList<FilterItem>();
filters.add(FilterItem.create(-1, null)); // dummy for the
// "add filter"
// option
CatlogDBHelper dbHelper = null;
try {
dbHelper = new CatlogDBHelper(LogcatActivity.this);
filters.addAll(dbHelper.findFilterItems());
} finally {
if (dbHelper != null) {
dbHelper.close();
}
}
Collections.sort(filters);
return filters;
}
@Override
protected void onPostExecute(List<FilterItem> filters) {
super.onPostExecute(filters);
filterAdapter = new FilterAdapter(LogcatActivity.this, filters);
new AlertDialog.Builder(LogcatActivity.this)
.setCancelable(true)
.setTitle(R.string.title_filters)
.setNegativeButton(android.R.string.cancel, null)
.setSingleChoiceItems(filterAdapter, 0,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
if (which == 0) { // dummy 'add filter'
// item
showAddFilterDialog(filterAdapter);
} else {
// load filter
String text = filterAdapter
.getItem(which).getText();
silentlySetSearchText(text);
dialog.dismiss();
}
}
}).show();
}
}.execute((Void) null);
}
private void showAddFilterDialog(final FilterAdapter filterAdapter) {
// show a popup to add a new filter text
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final AutoCompleteTextView editText = (AutoCompleteTextView) inflater
.inflate(R.layout.new_filter_text_view, null, false);
// show suggestions as the user types
List<String> suggestions = new ArrayList<String>(searchSuggestionsSet);
SortedFilterArrayAdapter<String> suggestionAdapter = new SortedFilterArrayAdapter<String>(
this, R.layout.simple_dropdown_small, suggestions);
editText.setAdapter(suggestionAdapter);
final AlertDialog alertDialog = new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.add_filter)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// dismiss soft keyboard
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(
editText.getWindowToken(), 0);
handleNewFilterText(editText.getText()
.toString(), filterAdapter);
dialog.dismiss();
}
}).setNegativeButton(android.R.string.cancel, null)
.setView(editText).create();
// ensures that the soft keyboard doesn't weirdly pop up at startup
// alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
// when 'Done' is clicked (i.e. enter button), do the same as when "OK"
// is clicked
editText.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
// dismiss soft keyboard
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
handleNewFilterText(editText.getText().toString(),
filterAdapter);
alertDialog.dismiss();
return true;
}
return false;
}
});
alertDialog.show();
}
protected void handleNewFilterText(String text,
final FilterAdapter filterAdapter) {
final String trimmed = text.trim();
if (!TextUtils.isEmpty(trimmed)) {
new AsyncTask<Void, Void, FilterItem>() {
@Override
protected FilterItem doInBackground(Void... params) {
CatlogDBHelper dbHelper = null;
try {
dbHelper = new CatlogDBHelper(LogcatActivity.this);
return dbHelper.addFilter(trimmed);
} finally {
if (dbHelper != null) {
dbHelper.close();
}
}
}
@Override
protected void onPostExecute(FilterItem filterItem) {
super.onPostExecute(filterItem);
if (filterItem != null) { // null indicates duplicate
filterAdapter.add(filterItem);
filterAdapter.sort(FilterItem.DEFAULT_COMPARATOR);
filterAdapter.notifyDataSetChanged();
addToAutocompleteSuggestions(trimmed);
}
}
}.execute((Void) null);
}
}
private void startPartialSelectMode() {
boolean hideHelp = PreferenceHelper
.getHidePartialSelectHelpPreference(this);
if (hideHelp) {
partialSelectMode = true;
partiallySelectedLogLines.clear();
Toast.makeText(this, R.string.toast_started_select_partial,
Toast.LENGTH_SHORT).show();
} else {
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View helpView = inflater
.inflate(R.layout.partial_select_help, null);
// don't show the scroll bar
helpView.setVerticalScrollBarEnabled(false);
helpView.setHorizontalScrollBarEnabled(false);
final CheckBox checkBox = (CheckBox) helpView
.findViewById(android.R.id.checkbox);
new AlertDialog.Builder(this)
.setTitle(R.string.menu_title_partial_select)
.setCancelable(true)
.setView(helpView)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
partialSelectMode = true;
partiallySelectedLogLines.clear();
Toast.makeText(
LogcatActivity.this,
R.string.toast_started_select_partial,
Toast.LENGTH_SHORT).show();
if (checkBox.isChecked()) {
// hide this help dialog in the future
PreferenceHelper
.setHidePartialSelectHelpPreference(
LogcatActivity.this,
true);
}
dialog.dismiss();
}
}).show();
}
}
private void startSettingsActivity() {
Intent intent = new Intent(this, SettingsActivity.class);
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
}
private void expandOrCollapseAll(boolean change) {
collapsedMode = change ? !collapsedMode : collapsedMode;
int oldFirstVisibleItem = firstVisibleItem;
for (LogLine logLine : adapter.getTrueValues()) {
if (logLine != null) {
logLine.setExpanded(!collapsedMode);
}
}
// expandButtonImage.setImageResource(
menuExpandButton.setIcon(collapsedMode ? R.drawable.ic_action_navigation_expand
: R.drawable.ic_action_navigation_collapse);
menuExpandButton.setTitle(collapsedMode ? R.string.expand
: R.string.collapse);
adapter.notifyDataSetChanged();
// ensure that we either stay autoscrolling at the bottom of the list...
if (autoscrollToBottom) {
getListView().setSelection(getListView().getCount() - 1);
// ... or that whatever was the previous first visible item is still
// the current first
// visible item after expanding/collapsing
} else if (oldFirstVisibleItem != -1) {
getListView().setSelection(oldFirstVisibleItem);
}
}
private void startDeleteSavedLogsDialog() {
if (!SaveLogHelper.checkSdCard(this)) {
return;
}
List<CharSequence> filenames = new ArrayList<CharSequence>(
SaveLogHelper.getLogFilenames());
if (filenames.isEmpty()) {
Toast.makeText(this, R.string.no_saved_logs, Toast.LENGTH_SHORT)
.show();
return;
}
final CharSequence[] filenameArray = ArrayUtil.toArray(filenames,
CharSequence.class);
final LogFileAdapter dropdownAdapter = new LogFileAdapter(this,
filenames, -1, true);
final TextView messageTextView = new TextView(LogcatActivity.this);
messageTextView.setText(R.string.select_logs_to_delete);
messageTextView.setPadding(3, 3, 3, 3);
Builder builder = new Builder(this);
builder.setTitle(R.string.manage_saved_logs)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.delete_all,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
boolean[] allChecked = new boolean[dropdownAdapter
.getCount()];
for (int i = 0; i < allChecked.length; i++) {
allChecked[i] = true;
}
verifyDelete(filenameArray, allChecked, dialog);
}
})
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
verifyDelete(filenameArray,
dropdownAdapter.getCheckedItems(),
dialog);
}
})
.setView(messageTextView)
.setSingleChoiceItems(dropdownAdapter, 0,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dropdownAdapter.checkOrUncheck(which);
}
});
builder.show();
}
protected void verifyDelete(final CharSequence[] filenameArray,
final boolean[] checkedItems, final DialogInterface parentDialog) {
Builder builder = new Builder(this);
int deleteCount = 0;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
deleteCount++;
}
}
final int finalDeleteCount = deleteCount;
if (finalDeleteCount > 0) {
builder.setTitle(R.string.delete_saved_log)
.setCancelable(true)
.setMessage(
String.format(getText(R.string.are_you_sure)
.toString(), finalDeleteCount))
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// ok, delete
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
SaveLogHelper
.deleteLogIfExists(filenameArray[i]
.toString());
}
}
String toastText = String.format(
getText(R.string.files_deleted)
.toString(),
finalDeleteCount);
Toast.makeText(LogcatActivity.this,
toastText, Toast.LENGTH_SHORT)
.show();
dialog.dismiss();
parentDialog.dismiss();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
}
private void showSendLogDialog() {
CharSequence[] items = new CharSequence[] {
getText(R.string.as_attachment), getText(R.string.as_text) };
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View includeDeviceInfoView = inflater.inflate(
R.layout.include_device_info, null, false);
final CheckBox includeDeviceInfoCheckBox = (CheckBox) includeDeviceInfoView
.findViewById(android.R.id.checkbox);
// allow user to choose whether or not to include device info in report,
// use preferences for persistence
includeDeviceInfoCheckBox.setChecked(PreferenceHelper
.getIncludeDeviceInfoPreference(this));
includeDeviceInfoCheckBox
.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
PreferenceHelper.setIncludeDeviceInfoPreference(
LogcatActivity.this, isChecked);
}
});
new AlertDialog.Builder(this)
.setTitle(R.string.send_log_title)
.setView(includeDeviceInfoView)
.setSingleChoiceItems(items, -1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
showSendLogToWhichAppDialogue(which == 1,
includeDeviceInfoCheckBox.isChecked());
dialog.dismiss();
}
}).show();
}
private void showSendLogToWhichAppDialogue(final boolean asText,
final boolean includeDeviceInfo) {
if (!(currentlyOpenLog == null && asText)
&& !SaveLogHelper.checkSdCard(this)) {
// if asText is false, then we need to check to make sure we can
// access the sdcard
return;
}
String title = getString(asText ? R.string.send_as_text
: R.string.send_as_attachment);
// determine the attachment type
SendLogDetails.AttachmentType attachmentType = asText ? SendLogDetails.AttachmentType.None
: (includeDeviceInfo ? SendLogDetails.AttachmentType.Zip
: SendLogDetails.AttachmentType.Text);
final SenderAppAdapter senderAppAdapter = new SenderAppAdapter(this,
asText, attachmentType);
new AlertDialog.Builder(LogcatActivity.this)
.setTitle(title)
.setCancelable(true)
.setSingleChoiceItems(senderAppAdapter, -1,
new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog,
final int which) {
dialog.dismiss();
sendLogToTargetApp(asText, includeDeviceInfo,
senderAppAdapter, which);
}
}).show();
}
protected void sendLogToTargetApp(final boolean asText,
final boolean includeDeviceInfo,
final SenderAppAdapter senderAppAdapter, final int which) {
final ProgressDialog getBodyProgressDialog = new ProgressDialog(
LogcatActivity.this);
getBodyProgressDialog.setCancelable(false);
// do in the background to avoid jank
AsyncTask<Void, Void, SendLogDetails> getBodyTask = new AsyncTask<Void, Void, SendLogDetails>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
if (asText || currentlyOpenLog == null || includeDeviceInfo) {
getBodyProgressDialog.setTitle(R.string.dialog_please_wait);
getBodyProgressDialog
.setMessage(getString(R.string.dialog_compiling_log));
getBodyProgressDialog.show();
}
}
@Override
protected SendLogDetails doInBackground(Void... params) {
return getSendLogDetailsInBackground(asText, includeDeviceInfo);
}
@Override
protected void onPostExecute(SendLogDetails sendLogDetails) {
super.onPostExecute(sendLogDetails);
senderAppAdapter.respondToClick(which,
sendLogDetails.getSubject(), sendLogDetails.getBody(),
sendLogDetails.getAttachmentType(),
sendLogDetails.getAttachment());
if (getBodyProgressDialog != null
&& getBodyProgressDialog.isShowing()) {
getBodyProgressDialog.dismiss();
}
}
};
getBodyTask.execute((Void) null);
}
private SendLogDetails getSendLogDetailsInBackground(boolean asText,
boolean includeDeviceInfo) {
SendLogDetails sendLogDetails = new SendLogDetails();
StringBuilder body = new StringBuilder();
List<File> files = new ArrayList<File>();
if (!asText) {
if (currentlyOpenLog != null) { // use saved log file
files.add(SaveLogHelper.getFile(currentlyOpenLog));
} else { // create a temp file to hold the current, unsaved log
File tempLogFile = SaveLogHelper.saveTemporaryFile(this,
SaveLogHelper.TEMP_LOG_FILENAME, null,
getCurrentLogAsListOfStrings());
files.add(tempLogFile);
}
}
if (includeDeviceInfo) {
// include device info
String deviceInfo = BuildHelper.getBuildInformationAsString();
if (asText) {
// append to top of body
body.append(deviceInfo).append('\n');
} else {
// or create as separate file called device.txt
File tempFile = SaveLogHelper.saveTemporaryFile(this,
SaveLogHelper.TEMP_DEVICE_INFO_FILENAME, deviceInfo,
null);
files.add(tempFile);
}
}
if (asText) {
body.append(getCurrentLogAsCharSequence());
}
sendLogDetails.setBody(body.toString());
sendLogDetails.setSubject(getString(R.string.subject_log_report));
// either zip up multiple files or just attach the one file
switch (files.size()) {
case 0: // no attachments
sendLogDetails
.setAttachmentType(SendLogDetails.AttachmentType.None);
break;
case 1: // one plaintext file attachment
sendLogDetails
.setAttachmentType(SendLogDetails.AttachmentType.Text);
sendLogDetails.setAttachment(files.get(0));
break;
default: // 2 files - need to zip them up
File zipFile = SaveLogHelper.saveTemporaryZipFile(
SaveLogHelper.TEMP_ZIP_FILENAME, files);
File tmpDirectory = SaveLogHelper.getTempDirectory();
for (File file : files) {
// delete original files
if (file.getParentFile().equals(tmpDirectory)) { // only delete
// temporary
// files
file.delete();
}
}
sendLogDetails.setAttachmentType(SendLogDetails.AttachmentType.Zip);
sendLogDetails.setAttachment(zipFile);
break;
}
return sendLogDetails;
}
private List<CharSequence> getCurrentLogAsListOfStrings() {
List<CharSequence> result = new ArrayList<CharSequence>(
adapter.getCount());
for (int i = 0; i < adapter.getCount(); i++) {
result.add(adapter.getItem(i).getOriginalLine());
}
return result;
}
private CharSequence getCurrentLogAsCharSequence() {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < adapter.getCount(); i++) {
stringBuilder.append(adapter.getItem(i).getOriginalLine()).append(
'\n');
}
return stringBuilder;
}
private void showSaveLogDialog() {
if (!SaveLogHelper.checkSdCard(this)) {
return;
}
final EditText editText = DialogHelper
.createEditTextForFilenameSuggestingDialog(this);
DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (DialogHelper.isInvalidFilename(editText.getText())) {
Toast.makeText(LogcatActivity.this,
R.string.enter_good_filename, Toast.LENGTH_SHORT)
.show();
} else {
String filename = editText.getText().toString();
saveLog(filename);
}
dialog.dismiss();
}
};
DialogHelper.showFilenameSuggestingDialog(this, editText,
onClickListener, null, null, R.string.save_log);
}
private void savePartialLog(final String filename, LogLine first,
LogLine last) {
final List<CharSequence> logLines = new ArrayList<CharSequence>(
adapter.getCount());
// filter based on first and last
boolean started = false;
boolean foundLast = false;
for (int i = 0; i < adapter.getCount(); i++) {
LogLine logLine = adapter.getItem(i);
if (logLine == first) {
started = true;
}
if (started) {
logLines.add(logLine.getOriginalLine());
}
if (logLine == last) {
foundLast = true;
break;
}
}
if (!foundLast || logLines.isEmpty()) {
Toast.makeText(this, R.string.toast_invalid_selection,
Toast.LENGTH_LONG).show();
cancelPartialSelect();
return;
}
AsyncTask<Void, Void, Boolean> saveTask = new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
SaveLogHelper.deleteLogIfExists(filename);
return SaveLogHelper.saveLog(logLines, filename);
}
@Override
protected void onPostExecute(Boolean successfullySavedLog) {
super.onPostExecute(successfullySavedLog);
if (successfullySavedLog) {
Toast.makeText(getApplicationContext(), R.string.log_saved,
Toast.LENGTH_SHORT).show();
openLog(filename);
} else {
Toast.makeText(getApplicationContext(),
R.string.unable_to_save_log, Toast.LENGTH_LONG)
.show();
}
cancelPartialSelect();
}
};
saveTask.execute((Void) null);
}
private void saveLog(final String filename) {
// do in background to avoid jankiness
final List<CharSequence> logLines = getCurrentLogAsListOfStrings();
AsyncTask<Void, Void, Boolean> saveTask = new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
SaveLogHelper.deleteLogIfExists(filename);
return SaveLogHelper.saveLog(logLines, filename);
}
@Override
protected void onPostExecute(Boolean successfullySavedLog) {
super.onPostExecute(successfullySavedLog);
if (successfullySavedLog) {
Toast.makeText(getApplicationContext(), R.string.log_saved,
Toast.LENGTH_SHORT).show();
openLog(filename);
} else {
Toast.makeText(getApplicationContext(),
R.string.unable_to_save_log, Toast.LENGTH_LONG)
.show();
}
}
};
saveTask.execute((Void) null);
}
private void showOpenLogDialog() {
if (!SaveLogHelper.checkSdCard(this)) {
return;
}
final List<CharSequence> filenames = new ArrayList<CharSequence>(
SaveLogHelper.getLogFilenames());
if (filenames.isEmpty()) {
Toast.makeText(this, R.string.no_saved_logs, Toast.LENGTH_SHORT)
.show();
return;
}
int logToSelect = currentlyOpenLog != null ? filenames
.indexOf(currentlyOpenLog) : -1;
ArrayAdapter<CharSequence> dropdownAdapter = new LogFileAdapter(this,
filenames, logToSelect, false);
Builder builder = new Builder(this);
builder.setTitle(R.string.open_log)
.setCancelable(true)
.setSingleChoiceItems(dropdownAdapter,
logToSelect == -1 ? 0 : logToSelect,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String filename = filenames.get(which)
.toString();
openLog(filename);
}
});
builder.show();
}
private void openLog(final String filename) {
// do in background to avoid jank
final AsyncTask<Void, Void, List<LogLine>> openFileTask = new AsyncTask<Void, Void, List<LogLine>>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
resetDisplayedLog(filename);
showProgressBar();
}
@Override
protected List<LogLine> doInBackground(Void... params) {
// remove any lines at the beginning if necessary
final int maxLines = PreferenceHelper
.getDisplayLimitPreference(LogcatActivity.this);
SavedLog savedLog = SaveLogHelper.openLog(filename, maxLines);
List<String> lines = savedLog.getLogLines();
List<LogLine> logLines = new ArrayList<LogLine>();
for (String line : lines) {
logLines.add(LogLine.newLogLine(line, !collapsedMode));
}
// notify the user if the saved file was truncated
if (savedLog.isTruncated()) {
handler.post(new Runnable() {
public void run() {
String toastText = String.format(
getString(R.string.toast_log_truncated),
maxLines);
Toast.makeText(LogcatActivity.this, toastText,
Toast.LENGTH_LONG).show();
}
});
}
return logLines;
}
@Override
protected void onPostExecute(List<LogLine> logLines) {
super.onPostExecute(logLines);
hideProgressBar();
for (LogLine logLine : logLines) {
adapter.addWithFilter(logLine, "");
addToAutocompleteSuggestions(logLine);
}
// scroll to bottom
getListView().setSelection(getListView().getCount() - 1);
}
};
// if the main log task is running, we can only run AFTER it's been
// canceled
if (task != null) {
task.setOnFinished(new Runnable() {
@Override
public void run() {
openFileTask.execute((Void) null);
}
});
task.unpause();
task.killReader();
task = null;
} else {
// main log not running; just open in this thread
openFileTask.execute((Void) null);
}
}
private void hideProgressBar() {
darkProgressBar.setVisibility(View.GONE);
lightProgressBar.setVisibility(View.GONE);
}
private void showProgressBar() {
ColorScheme colorScheme = PreferenceHelper
.getColorScheme(LogcatActivity.this);
darkProgressBar
.setVisibility(colorScheme.isUseLightProgressBar() ? View.GONE
: View.VISIBLE);
lightProgressBar
.setVisibility(colorScheme.isUseLightProgressBar() ? View.VISIBLE
: View.GONE);
}
private void resetDisplayedLog(String filename) {
adapter.clear();
currentlyOpenLog = filename;
collapsedMode = !PreferenceHelper
.getExpandedByDefaultPreference(getApplicationContext());
clearButton.setVisibility(filename == null ? View.VISIBLE : View.GONE);
pauseButton.setVisibility(filename == null ? View.VISIBLE : View.GONE);
pauseButtonImage.setImageResource(R.drawable.ic_action_pause);
expandButtonImage
.setImageResource(collapsedMode ? R.drawable.ic_action_navigation_expand
: R.drawable.ic_action_navigation_collapse);
addFiltersToSuggestions(); // filters are what initial populate the
// suggestions
updateDisplayedFilename();
resetFilter();
}
private void updateDisplayedFilename() {
mainFilenameLayout
.setVisibility(currentlyOpenLog != null ? View.VISIBLE
: View.GONE);
if (currentlyOpenLog != null) {
filenameTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP,
PreferenceHelper.getTextSizePreference(this) + 2);
ColorScheme colorScheme = PreferenceHelper.getColorScheme(this);
borderView1
.setBackgroundColor(colorScheme.getForegroundColor(this));
borderView2
.setBackgroundColor(colorScheme.getForegroundColor(this));
borderView3
.setBackgroundColor(colorScheme.getForegroundColor(this));
borderView4
.setBackgroundColor(colorScheme.getForegroundColor(this));
filenameTextView.setTextColor(colorScheme.getForegroundColor(this));
filenameTextView.setBackgroundColor(colorScheme
.getBubbleBackgroundColor(this));
filenameTextView.setText(currentlyOpenLog);
}
}
private void resetFilter() {
try {
String defaultLogLevel = Character.toString(PreferenceHelper
.getDefaultLogLevelPreference(this));
CharSequence[] logLevels = getResources().getStringArray(
R.array.log_levels_values);
int logLevelLimit = ArrayUtil.indexOf(logLevels, defaultLogLevel);
adapter.setLogLevelLimit(logLevelLimit);
logLevelChanged();
// silently change edit text without invoking filtering
searchEditText.removeTextChangedListener(this);
searchEditText.setText("");
searchEditText.addTextChangedListener(this);
} catch (NullPointerException e) {
}
}
private void showLogLevelDialog() {
CharSequence[] logLevels = getResources().getStringArray(
R.array.log_levels);
// put the word "default" after whatever the default log level is
String defaultLogLevel = Character.toString(PreferenceHelper
.getDefaultLogLevelPreference(this));
int index = ArrayUtil.indexOf(
getResources().getStringArray(R.array.log_levels_values),
defaultLogLevel);
logLevels[index] = logLevels[index].toString() + " "
+ getString(R.string.default_in_parens);
Builder builder = new Builder(this);
builder.setTitle(R.string.log_level)
.setCancelable(true)
.setSingleChoiceItems(logLevels, adapter.getLogLevelLimit(),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
adapter.setLogLevelLimit(which);
logLevelChanged();
dialog.dismiss();
}
});
builder.show();
}
private void setUpWidgets() {
darkProgressBar = (ProgressBar) findViewById(R.id.main_dark_progress_bar);
lightProgressBar = (ProgressBar) findViewById(R.id.main_light_progress_bar);
backgroundLayout = findViewById(R.id.main_background);
clearButton = findViewById(R.id.main_clear_button);
expandButton = findViewById(R.id.main_more_button);
pauseButton = findViewById(R.id.main_pause_button);
expandButtonImage = (ImageView) findViewById(R.id.main_expand_button_image);
pauseButtonImage = (ImageView) findViewById(R.id.main_pause_button_image);
for (View view : new View[] { clearButton, expandButton, pauseButton }) {
view.setOnClickListener(this);
}
clearButton.setOnLongClickListener(this);
filenameTextView = (TextView) findViewById(R.id.main_filename_text_view);
mainFilenameLayout = findViewById(R.id.main_filename_linear_layout);
borderView1 = findViewById(R.id.main_border_view_1);
borderView2 = findViewById(R.id.main_border_view_2);
borderView3 = findViewById(R.id.main_border_view_3);
borderView4 = findViewById(R.id.main_border_view_4);
}
private void setUpAdapter() {
adapter = new LogLineAdapter(this, R.layout.logcat_list_item,
new ArrayList<LogLine>());
setListAdapter(adapter);
getListView().setOnScrollListener(this);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
LogLine logLine = adapter.getItem(position);
if (partialSelectMode) {
logLine.setHighlighted(true);
partiallySelectedLogLines.add(logLine);
handler.post(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
});
if (partiallySelectedLogLines.size() == 2) {
// last line
handler.post(new Runnable() {
@Override
public void run() {
completePartialSelect();
}
});
}
} else {
logLine.setExpanded(!logLine.isExpanded());
adapter.notifyDataSetChanged();
}
}
private void completePartialSelect() {
if (!SaveLogHelper.checkSdCard(this)) {
cancelPartialSelect();
return;
}
final EditText editText = DialogHelper
.createEditTextForFilenameSuggestingDialog(this);
DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (DialogHelper.isInvalidFilename(editText.getText())) {
cancelPartialSelect();
Toast.makeText(LogcatActivity.this,
R.string.enter_good_filename, Toast.LENGTH_SHORT)
.show();
} else {
String filename = editText.getText().toString();
savePartialLog(filename, partiallySelectedLogLines.get(0),
partiallySelectedLogLines.get(1));
}
dialog.dismiss();
}
};
DialogInterface.OnClickListener onCancelListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
cancelPartialSelect();
}
};
DialogHelper.showFilenameSuggestingDialog(this, editText,
onClickListener, null, onCancelListener, R.string.save_log);
}
private void cancelPartialSelect() {
partialSelectMode = false;
boolean changed = false;
for (LogLine logLine : partiallySelectedLogLines) {
if (logLine.isHighlighted()) {
logLine.setHighlighted(false);
changed = true;
}
}
partiallySelectedLogLines.clear();
if (changed) {
handler.post(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
});
}
}
private void silentlySetSearchText(String text) {
// sets the search text without invoking autosuggestions, which are
// really only useful when typing
searchEditText.setFocusable(false);
searchEditText.setFocusableInTouchMode(false);
searchEditText.setText(text);
searchEditText.setFocusable(true);
searchEditText.setFocusableInTouchMode(true);
}
private void unfocusEditText() {
searchEditText.setFocusable(false);
searchEditText.setFocusableInTouchMode(false);
searchEditText.setFocusable(true);
searchEditText.setFocusableInTouchMode(true);
}
@Override
public void afterTextChanged(Editable s) {
// do nothing
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
CharSequence filterText = searchEditText.getText();
log.d("filtering: %s", filterText);
filter(filterText);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) {
// show keyboard
searchEditText.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(searchEditText, 0);
return true;
}
return super.onKeyDown(keyCode, event);
}
private void filter(CharSequence filterText) {
Filter filter = adapter.getFilter();
filter.filter(filterText, this);
}
@Override
public boolean onLongClick(View v) {
// clear button long-pressed, undo clear
startUpMainLog();
return true;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.main_edit_text:
if (searchEditText != null && searchEditText.length() > 0) {
// I think it's intuitive to click an edit text and have all the
// text selected
searchEditText.setSelection(0, searchEditText.length());
}
break;
case R.id.main_clear_button:
unfocusEditText();
if (adapter != null) {
adapter.clear();
}
if (searchEditText != null) {
searchEditText.setText("");
}
Toast.makeText(this, R.string.log_cleared, Toast.LENGTH_LONG)
.show();
break;
case R.id.main_more_button:
unfocusEditText();
expandOrCollapseAll(true);
break;
case R.id.main_pause_button:
unfocusEditText();
pauseUnpause();
break;
}
}
private void pauseUnpause() {
// youtube.com/pauseunpause
LogReaderAsyncTask currentTask = task;
if (currentTask != null) {
if (currentTask.isPaused()) {
currentTask.unpause();
} else {
currentTask.pause();
}
// pauseButtonImage.setImageResource(
menuPauseButton
.setIcon(currentTask.isPaused() ? R.drawable.ic_action_play
: R.drawable.ic_action_pause);
menuPauseButton.setTitle(currentTask.isPaused() ? R.string.resume
: R.string.pause);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// update what the first viewable item is
this.firstVisibleItem = firstVisibleItem;
// if the bottom of the list isn't visible anymore, then stop
// autoscrolling
autoscrollToBottom = (firstVisibleItem + visibleItemCount == totalItemCount);
// only hide the fast scroll if we're unpaused and at the bottom of the
// list
boolean enableFastScroll = task == null || task.isPaused()
|| !autoscrollToBottom;
getListView().setFastScrollEnabled(enableFastScroll);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// do nothing
}
@Override
public void onFilterComplete(int count) {
// always scroll to the bottom when searching
getListView().setSelection(count);
}
private void dismissSoftKeyboard() {
log.d("dismissSoftKeyboard()");
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), 0);
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
log.d("actionId: " + actionId + " event:" + event);
if (event != null && event.getAction() == KeyEvent.ACTION_DOWN) {
dismissSoftKeyboard();
unfocusEditText();
return true;
}
return false;
}
private void logLevelChanged() {
try {
filter(searchEditText.getText());
} catch (NullPointerException e) {
}
}
private void updateBackgroundColor() {
ColorScheme colorScheme = PreferenceHelper.getColorScheme(this);
final int color = colorScheme.getBackgroundColor(LogcatActivity.this);
handler.post(new Runnable() {
public void run() {
backgroundLayout.setBackgroundColor(color);
}
});
getListView().setCacheColorHint(color);
getListView().setDivider(new ColorDrawable(color));
}
private void addToAutocompleteSuggestions(LogLine logLine) {
// add the tags to the autocompletetextview
if (!StringUtil.isEmptyOrWhitespaceOnly(logLine.getTag())) {
String trimmed = logLine.getTag().trim();
addToAutocompleteSuggestions(trimmed);
}
}
private void addToAutocompleteSuggestions(String trimmed) {
if (searchSuggestionsSet.size() < MAX_NUM_SUGGESTIONS
&& !searchSuggestionsSet.contains(trimmed)) {
searchSuggestionsSet.add(trimmed);
searchSuggestionsAdapter.add(trimmed);
}
}
private class LogReaderAsyncTask extends AsyncTask<Void, LogLine, Void> {
private int counter = 0;
private volatile boolean paused;
private final Object lock = new Object();
private boolean firstLineReceived;
private boolean killed;
private LogcatReader reader;
private Runnable onFinished;
@Override
protected Void doInBackground(Void... params) {
log.d("doInBackground()");
try {
// use "recordingMode" because we want to load all the existing
// lines at once
// for a performance boost
LogcatReaderLoader loader = LogcatReaderLoader.create(
LogcatActivity.this, true);
reader = loader.loadReader();
int maxLines = PreferenceHelper
.getDisplayLimitPreference(LogcatActivity.this);
String line;
LinkedList<LogLine> initialLines = new LinkedList<LogLine>();
while ((line = reader.readLine()) != null) {
if (paused) {
synchronized (lock) {
if (paused) {
lock.wait();
}
}
}
LogLine logLine = LogLine.newLogLine(line, !collapsedMode);
if (!reader.readyToRecord()) {
// "ready to record" in this case means all the initial
// lines have been flushed from the reader
initialLines.add(logLine);
if (initialLines.size() > maxLines) {
initialLines.removeFirst();
}
} else if (!initialLines.isEmpty()) {
// flush all the initial lines we've loaded
initialLines.add(logLine);
publishProgress(ArrayUtil.toArray(initialLines,
LogLine.class));
initialLines.clear();
} else {
// just proceed as normal
publishProgress(logLine);
}
}
} catch (InterruptedException e) {
log.d(e, "expected error");
} catch (Exception e) {
log.d(e, "unexpected error");
} finally {
killReader();
log.d("AsyncTask has died");
}
return null;
}
public void killReader() {
if (!killed) {
synchronized (lock) {
if (!killed && reader != null) {
reader.killQuietly();
killed = true;
}
}
}
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
log.d("onPostExecute()");
doWhenFinished();
}
@Override
protected void onPreExecute() {
super.onPreExecute();
log.d("onPreExecute()");
resetDisplayedLog(null);
showProgressBar();
}
@Override
protected void onProgressUpdate(LogLine... values) {
super.onProgressUpdate(values);
if (!firstLineReceived) {
firstLineReceived = true;
hideProgressBar();
}
for (LogLine logLine : values) {
adapter.addWithFilter(logLine, searchEditText.getText());
addToAutocompleteSuggestions(logLine);
}
// how many logs to keep in memory? this avoids OutOfMemoryErrors
int maxNumLogLines = PreferenceHelper
.getDisplayLimitPreference(LogcatActivity.this);
// check to see if the list needs to be truncated to avoid out of
// memory errors
if (++counter % UPDATE_CHECK_INTERVAL == 0
&& adapter.getTrueValues().size() > maxNumLogLines) {
int numItemsToRemove = adapter.getTrueValues().size()
- maxNumLogLines;
adapter.removeFirst(numItemsToRemove);
log.d("truncating %d lines from log list to avoid out of memory errors",
numItemsToRemove);
}
if (autoscrollToBottom) {
getListView().setSelection(getListView().getCount());
}
}
private void doWhenFinished() {
if (paused) {
unpause();
}
if (onFinished != null) {
onFinished.run();
}
}
public void pause() {
synchronized (lock) {
paused = true;
}
}
public void unpause() {
synchronized (lock) {
paused = false;
lock.notify();
}
}
public boolean isPaused() {
return paused;
}
public void setOnFinished(Runnable onFinished) {
this.onFinished = onFinished;
}
}
}