package org.bitseal.activities;
import info.guardianproject.cacheword.CacheWordHandler;
import info.guardianproject.cacheword.ICacheWordSubscriber;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import org.bitseal.R;
import org.bitseal.services.AppLockHandler;
import android.annotation.SuppressLint;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
/**
* The Activity class for the app's View Log screen.
*
* @author Jonathan Coe
*/
public class ViewErrorsActivity extends ListActivity implements ICacheWordSubscriber
{
/** The frequency in milliseconds by which we will update the error list */
private static final long UPDATE_FREQUENCY_MILLISECONDS = 2000;
/** The maximum number of error items to be displayed */
private static final int MAXIMUM_ERRORS_TO_DISPLAY = 50;
/** The key for a boolean variable that records whether or not a user-defined database encryption passphrase has been saved */
private static final String KEY_DATABASE_PASSPHRASE_SAVED = "databasePassphraseSaved";
/** The maximum number of lines that we will read from logcat's output */
private static final int LOGCAT_MAXIMUM_LINES = 2000;
private static final String LOG_LEVEL_ERROR = "E";
private CacheWordHandler mCacheWordHandler;
private ListView mErrorListView;
private LogAdapter mErrorAdapter;
private ArrayList<String> mErrors;
private TimerTask refreshListTask;
private String mLastLine;
private int mProcessID;
private static final String TAG = "VIEW_ERRORS_ACTIVITY";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_errors);
try
{
// Check whether the user has set a database encryption passphrase
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false))
{
// Connect to the CacheWordService
mCacheWordHandler = new CacheWordHandler(this);
mCacheWordHandler.connectToService();
}
// Get Bitseal's current process ID
mProcessID = android.os.Process.myPid();
// Populate a ListView with Bitseal's recent errors
mErrorListView = (ListView) findViewById(android.R.id.list);
updateListView();
}
catch (Exception e)
{
Log.e(TAG, "Exception ocurred in ViewErrorsActivity.onCreate(). The exception message was:\n"
+ e.getMessage());
}
}
@Override
protected void onPause()
{
super.onPause();
refreshListTask.cancel();
}
protected void onResume()
{
super.onResume();
// Check for new log lines regularly and update the ListView if any are found
refreshListTask = new TimerTask()
{
@Override
public void run()
{
runOnUiThread(new Runnable()
{
public void run()
{
if (checkForNewLines())
{
updateListView();
}
}
});
}
};
new Timer().schedule(refreshListTask, 0, UPDATE_FREQUENCY_MILLISECONDS);
}
/**
* Checks whether there are new errors to be displayed
*/
private boolean checkForNewLines()
{
try
{
Process process = Runtime.getRuntime().exec("logcat -d");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = "";
// Check whether the last line of the error output is new
String newLastLine = "";
while ((line = bufferedReader.readLine()) != null)
{
// Get the most recent line of the output we want to display
if (filterErrors(line))
{
newLastLine = line;
}
}
// If the last line is new (i.e. there is new error output), refresh the displayed text
if (newLastLine.equals(mLastLine) == false)
{
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
Log.e(TAG, "Exception ocurred in ViewErrorsActivity.checkForNewLines. The exception message was:\n"
+ e.getMessage());
return false;
}
}
/**
* Returns the an ArrayList<String> containing the errors
* which should be displayed
*/
private ArrayList<String> getErrors()
{
try
{
// Get the logcat output and prepare to read it
Process process = Runtime.getRuntime().exec("logcat -d");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
ArrayList<String> errorLines = new ArrayList<String>();
String line = "";
// Count the number of lines in the logcat output
int lines = 0;
while ((bufferedReader.readLine()) != null)
{
lines ++;
}
// Create a new BufferedReader so we can read from the start again
process = Runtime.getRuntime().exec("logcat -d");
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
// If there are more lines than we are prepared to process, only process the most recent ones
int startPoint = lines - LOGCAT_MAXIMUM_LINES;
if (lines > LOGCAT_MAXIMUM_LINES)
{
// Skip through lines until we are at the correct start point
for (int i = 0; i < startPoint && bufferedReader.ready(); bufferedReader.readLine()) { }
}
// Read the selected lines
while ((line = bufferedReader.readLine()) != null)
{
// Filter log output by Bitseal's current process number and by removing unwanted lines
if (filterErrors(line))
{
errorLines.add(line);
}
}
// If there are no lines to display, return a placeholder message
if (errorLines.size() == 0)
{
errorLines.add(getResources().getString(R.string.activity_view_errors_placeholder_message));
}
else
{
// Record the last read line
mLastLine = errorLines.get(errorLines.size() - 1);
// If the log text is over the maximum number of items, shorten it
if (errorLines.size() > MAXIMUM_ERRORS_TO_DISPLAY)
{
errorLines = new ArrayList<String>(errorLines.subList(errorLines.size() - MAXIMUM_ERRORS_TO_DISPLAY, errorLines.size()));
}
}
return errorLines;
}
catch (Exception e)
{
Log.e(TAG, "Exception ocurred in ViewErrorsActivity.getErrors(). The exception message was:\n"
+ e.getMessage());
ArrayList<String> placeholderList = new ArrayList<String>();
placeholderList.add(getResources().getString(R.string.activity_view_errors_placeholder_message));
return placeholderList;
}
}
/**
* Filters a log line, returning whether or not is should be included
* in the displayed output
*/
private boolean filterErrors(String line)
{
// Filter log output by Bitseal's current process number
if (line.contains(String.valueOf(mProcessID)))
{
// Filter log output by Bitseal's current process number
if (line.startsWith(LOG_LEVEL_ERROR))
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/**
* Updates the error log ListView
**/
private void updateListView()
{
// Get the error log lines to display
mErrors = getErrors();
// Save ListView state so that we can resume at the same scroll position
Parcelable state = mErrorListView.onSaveInstanceState();
// Re-instantiate the ListView and re-populate it
mErrorListView = new ListView(this);
mErrorListView = (ListView)findViewById(android.R.id.list);
mErrorAdapter = new LogAdapter(mErrors);
mErrorListView.setAdapter(mErrorAdapter);
// Restore previous state (including selected item index and scroll position)
mErrorListView.onRestoreInstanceState(state);
// If the user has scrolled to the bottom of the ListView, keep scrolling to the
// bottom as new items are added
try
{
if (mErrorListView.getLastVisiblePosition() == mErrorListView.getAdapter().getCount() -1 &&
mErrorListView.getChildAt(mErrorListView.getChildCount() - 1).getBottom() <= mErrorListView.getHeight())
{
scrollMyListViewToBottom();;
}
}
catch (Exception e)
{
Log.e(TAG, "Exception ocurred in ViewErrorsActivity.updateListView(). The exception message was:\n"
+ e.getMessage());
}
}
private void scrollMyListViewToBottom()
{
mErrorListView.post(new Runnable()
{
@Override
public void run()
{
// Select the last row so it will scroll into view
mErrorListView.setSelection(mErrorAdapter.getCount() - 1);
}
});
}
/**
* A ViewHolder used to speed up this activity's ListView.
*/
static class ViewHolder
{
public TextView logLineTextView;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
((LogAdapter)mErrorListView.getAdapter()).notifyDataSetChanged();
}
private class LogAdapter extends ArrayAdapter<String>
{
public LogAdapter(ArrayList<String> logLines)
{
super(getBaseContext(), android.R.layout.simple_list_item_1, logLines);
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
// If we weren't given a view that can be recycled, inflate a new one
if (convertView == null)
{
convertView = getLayoutInflater().inflate(R.layout.list_item_log, parent, false);
// Configure the view holder
ViewHolder viewHolder = new ViewHolder();
viewHolder.logLineTextView = (TextView) convertView.findViewById(R.id.view_log_line_textview);
convertView.setTag(viewHolder);
}
ViewHolder holder = (ViewHolder) convertView.getTag();
// Get the log line
final String error = getItem(position);
holder.logLineTextView.setText(error);
if (error.equals(getResources().getString(R.string.activity_view_errors_placeholder_message)))
{
holder.logLineTextView.setTextColor(Color.BLACK);
holder.logLineTextView.setTextSize(18);
}
else
{
holder.logLineTextView.setTextColor(Color.RED);
convertView.setOnLongClickListener(new View.OnLongClickListener()
{
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@Override
public boolean onLongClick(View v)
{
Log.i(TAG, "Error list item long clicked");
// Copy the error to the clipboard
int sdk = android.os.Build.VERSION.SDK_INT;
if(sdk < android.os.Build.VERSION_CODES.HONEYCOMB)
{
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(error);
}
else
{
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("COPIED_MESSAGE_TEXT", error);
clipboard.setPrimaryClip(clip);
}
Toast.makeText(getApplicationContext(), R.string.activity_view_errors_error_copied_toast, Toast.LENGTH_LONG).show();
// Indicate that we don't want any further processing
return true;
}
});
}
return convertView;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.options_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false) == false)
{
menu.removeItem(R.id.menu_item_lock);
}
return super.onPrepareOptionsMenu(menu);
}
@SuppressLint("InlinedApi")
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch(item.getItemId())
{
case R.id.menu_item_inbox:
Intent intent1 = new Intent(this, InboxActivity.class);
intent1.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent1);
break;
case R.id.menu_item_sent:
Intent intent2 = new Intent(this, SentActivity.class);
intent2.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent2);
break;
case R.id.menu_item_compose:
Intent intent3 = new Intent(this, ComposeActivity.class);
intent3.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent3);
break;
case R.id.menu_item_identities:
Intent intent4 = new Intent(this, IdentitiesActivity.class);
intent4.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent4);
break;
case R.id.menu_item_addressBook:
Intent intent5 = new Intent(this, AddressBookActivity.class);
intent5.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent5);
break;
case R.id.menu_item_settings:
Intent intent6 = new Intent(this, SettingsActivity.class);
intent6.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent6);
break;
case R.id.menu_item_lock:
AppLockHandler.runLockRoutine(mCacheWordHandler);
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
protected void onStop()
{
super.onStop();
if (mCacheWordHandler != null)
{
mCacheWordHandler.disconnectFromService();
}
}
@SuppressLint("InlinedApi")
@Override
public void onCacheWordLocked()
{
// Redirect to the lock screen activity
Intent intent = new Intent(getBaseContext(), LockScreenActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) // FLAG_ACTIVITY_CLEAR_TASK only exists in API 11 and later
{
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);// Clear the stack of activities
}
else
{
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
@Override
public void onCacheWordOpened()
{
// Nothing to do here currently
}
@Override
public void onCacheWordUninitialized()
{
// Database encryption is currently not enabled by default, so there is nothing to do here
}
}