package org.bitseal.activities; import info.guardianproject.cacheword.CacheWordHandler; import info.guardianproject.cacheword.ICacheWordSubscriber; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import org.bitseal.R; import org.bitseal.crypt.AddressGenerator; import org.bitseal.data.Address; import org.bitseal.data.AddressBookRecord; import org.bitseal.data.Message; import org.bitseal.database.AddressBookRecordProvider; import org.bitseal.database.AddressBookRecordsTable; import org.bitseal.database.AddressProvider; import org.bitseal.database.MessageProvider; import org.bitseal.database.MessagesTable; import org.bitseal.services.AppLockHandler; import org.bitseal.services.BackgroundService; import org.bitseal.services.ExceptionHandler; import org.bitseal.services.NotificationsService; import org.bitseal.util.ColourCalculator; import android.annotation.SuppressLint; import android.app.ListActivity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; 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 inbox. * * @author Jonathan Coe */ public class InboxActivity extends ListActivity implements ICacheWordSubscriber { private ArrayList<Message> mMessages; private ListView mInboxListView; private int mListPosition = 0; private static final String INBOX_FIRST_RUN = "inbox_first_run"; private static final String FIRST_ADDRESS_LABEL = "Me"; private static final String INBOX_ACTIVITY_LIST_POSITION = "inboxActivityListPosition"; /** A key used to store the time of the last successful 'check for new msgs' server request */ private static final String LAST_MSG_CHECK_TIME = "lastMsgCheckTime"; /** Stores the Unix timestamp of the last msg payload we processed. This can be used to tell us how far behind the network we are. */ private static final String LAST_PROCESSED_MSG_TIME = "lastProcessedMsgTime"; // Used when receiving Intents to the UI so that it can refresh the data it is displaying public static final String UI_NOTIFICATION = "uiNotification"; private static final int INBOX_COLOURS_ALPHA_VALUE = 70; /** 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"; private CacheWordHandler mCacheWordHandler; private static final String TAG = "INBOX_ACTIVITY"; @SuppressLint("InlinedApi") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_inbox); // Check whether the user has set a database encryption passphrase SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false)) { Log.i(TAG, "We detected that the user has a database encryption passphrase set"); // If Bitseal is being launched (rather than re-opened) if (getIntent().hasCategory(Intent.CATEGORY_LAUNCHER)) { Log.i(TAG, "InboxActivity's starting intent had Category_Launcher set"); onCacheWordLocked(); return; } // Connect to the CacheWordService mCacheWordHandler = new CacheWordHandler(this); mCacheWordHandler.connectToService(); if (getIntent().hasExtra(LockScreenActivity.EXTRA_DATABASE_UNLOCKED)) { // Start the BackgroundService Intent firstStartIntent = new Intent(this, BackgroundService.class); firstStartIntent.putExtra(BackgroundService.PERIODIC_BACKGROUND_PROCESSING_REQUEST, BackgroundService.BACKGROUND_PROCESSING_REQUEST); BackgroundService.sendWakefulWork(this, firstStartIntent); } } else { Log.i(TAG, "We detected that the user does NOT have a database encryption passphrase set"); } // Check whether this is the first time the inbox activity has been opened - if so then run the 'first launch' routine if (prefs.getBoolean(INBOX_FIRST_RUN, true)) { runFirstLaunchRoutine(); } mInboxListView = new ListView(this); mInboxListView = (ListView)findViewById(android.R.id.list); setTitle(getResources().getString(R.string.inbox_activity_title)); // Sometimes the CacheWordService will take too long to initialize, and as a result we will fail to detect // that the app is locked. Therefore if our attempt to access the database fails and the user has a database // passphrase set, we will redirect to the lock screen. try { MessageProvider msgProv = MessageProvider.get(this); mMessages =msgProv.searchMessages(MessagesTable.COLUMN_BELONGS_TO_ME, String.valueOf(0)); // 0 stands for "false" in the database // Sort the messages so that the most recent are displayed first Collections.sort(mMessages); MessageAdapter adapter = new MessageAdapter(mMessages); setListAdapter(adapter); } catch (Exception e) { Log.e(TAG, "While running InboxActivity.onCreate, our attempt to access the database failed.\n" + "The exception message was: " + e.getMessage()); if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false)) { Log.e(TAG, "The user has a database passphrase set. Calling onCacheWordLocked()."); onCacheWordLocked(); } else { Toast.makeText(getBaseContext(), R.string.inbox_toast_unknown_database_error, Toast.LENGTH_LONG).show(); Log.e(TAG, "Unknown exception occurred in InboxActivity.onCreate"); } } // If we have reached this point without crashing, then it should be safe to reset the uncaught exception handler flag SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(ExceptionHandler.UNCAUGHT_EXCEPTION_HANDLED, false); editor.commit(); } @Override protected void onResume() { super.onResume(); // Register the broadcast receiver registerReceiver(receiver, new IntentFilter(UI_NOTIFICATION)); Intent intent = getIntent(); if (intent.hasExtra(NotificationsService.EXTRA_NEW_MESSAGES_NOTIFICATION_CLEARED)) { // Set the 'new messages notification currently displayed' shared preference to false SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(NotificationsService.KEY_NEW_MESSAGES_NOTIFICATION_CURRENTLY_DISPLAYED, false); editor.commit(); } // If we are returning to this activity after an inbox message has been deleted, we need to do a // special adjustment to the list position SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (prefs.getBoolean(InboxMessageActivity.FLAG_INBOX_MESSAGE_DELETED, false)) { mListPosition = prefs.getInt(INBOX_ACTIVITY_LIST_POSITION, 0); if (mListPosition > 0) { Log.i(TAG, "We detected that an inbox message has just been deleted - setting the list position to " + mListPosition); getListView().setSelection(mListPosition); } SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(InboxMessageActivity.FLAG_INBOX_MESSAGE_DELETED, false); editor.commit(); } } @Override protected void onPause() { super.onPause(); unregisterReceiver(receiver); // Save the listView position so that we can resume in the same position even if a record is deleted SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); SharedPreferences.Editor editor = prefs.edit(); mListPosition = getListView().getFirstVisiblePosition(); editor.putInt(INBOX_ACTIVITY_LIST_POSITION, mListPosition); editor.commit(); } @Override protected void onRestart() { super.onRestart(); updateListView(); } private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "InboxActivity.BroadcastReceiver.onReceive() called"); updateListView(); } }; /** * Adds some default entries to the address book, adds a welcome message to * the inbox, generates a new Bitmessage address for the user, and starts the * BackgroundService for the first time. */ private void runFirstLaunchRoutine() { // Set a flag in SharedPreferences so that this will not be called again SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(INBOX_FIRST_RUN, false); editor.commit(); // Add some default entries to the address book Resources resources = getResources(); AddressBookRecord addressBookEntry0 = new AddressBookRecord(); addressBookEntry0.setLabel(resources.getString(R.string.inbox_default_address_book_entry_0_label)); addressBookEntry0.setAddress(resources.getString(R.string.inbox_default_address_book_entry_0_address)); AddressBookRecord addressBookEntry1 = new AddressBookRecord(); addressBookEntry1.setLabel(resources.getString(R.string.inbox_default_address_book_entry_1_label)); addressBookEntry1.setAddress(resources.getString(R.string.inbox_default_address_book_entry_1_address)); AddressBookRecordProvider addBookProv = AddressBookRecordProvider.get(this); addBookProv.addAddressBookRecord(addressBookEntry0); addBookProv.addAddressBookRecord(addressBookEntry1); // Add the 'Welcome to Bitseal' message to the inbox Message welcomeMessage = new Message(); welcomeMessage.setBelongsToMe(false); welcomeMessage.setToAddress(resources.getString(R.string.inbox_welcome_message_to_address)); welcomeMessage.setFromAddress(resources.getString(R.string.inbox_welcome_message_from_address)); welcomeMessage.setSubject(resources.getString(R.string.inbox_welcome_message_subject)); welcomeMessage.setBody(resources.getString(R.string.inbox_welcome_message_body)); welcomeMessage.setTime(System.currentTimeMillis() / 1000); MessageProvider msgProv = MessageProvider.get(getApplicationContext()); long msg0Id = msgProv.addMessage(welcomeMessage); welcomeMessage.setId(msg0Id); mMessages = new ArrayList<Message>(); mMessages.add(welcomeMessage); // Generate a new Bitmessage address try { AddressGenerator addGen = new AddressGenerator(); Address firstAddress = addGen.generateAndSaveNewAddress(); firstAddress.setLabel(FIRST_ADDRESS_LABEL); AddressProvider addProv = AddressProvider.get(getApplicationContext()); addProv.updateAddress(firstAddress); // Set the 'last msg check time' to the current time - otherwise the app will start checking for msgs sent // within the last 2.5 days, which makes no sense as our address has only just been generated. long currentTime = System.currentTimeMillis() / 1000; editor.putLong(LAST_MSG_CHECK_TIME, currentTime); editor.commit(); Log.i(TAG, "Updated the 'last successful msg check time' value stored in SharedPreferences to " + currentTime); // Set the 'last msg processed time' to the current time. As above, we do not have any addresses yet, so we // cannot have been sent a message yet. editor.putLong(LAST_PROCESSED_MSG_TIME, currentTime); editor.commit(); Log.i(TAG, "Updated the 'last processed msg time' value stored in SharedPreferences to " + currentTime); // Start the BackgroundService in order to complete the 'create new identity' task Intent intent = new Intent(getBaseContext(), BackgroundService.class); intent.putExtra(BackgroundService.UI_REQUEST, BackgroundService.UI_REQUEST_CREATE_IDENTITY); intent.putExtra(BackgroundService.ADDRESS_ID, firstAddress.getId()); BackgroundService.sendWakefulWork(this, intent); Log.i(TAG, "Starting BackgroundService for the first time"); } catch (Exception e) { Log.e(TAG, "Exception occured in InboxActivity while running runFirstLaunchRoutine(). \n " + "Exception message: " + e.getMessage()); } } /** * Needed to update the ListView when a Message has been read, and so should not longer * be highlighted as unread **/ private void updateListView() { // Get all Messages that do not 'belong to me' (i.e. were sent by someone else) from the database MessageProvider msgProv = MessageProvider.get(getApplicationContext()); mMessages =msgProv.searchMessages(MessagesTable.COLUMN_BELONGS_TO_ME, String.valueOf(0)); // 0 stands for "false" in the database Collections.sort(mMessages); // Save ListView state so that we can resume at the same scroll position Parcelable state = mInboxListView.onSaveInstanceState(); // Re-instantiate the ListView and re-populate it mInboxListView = new ListView(this); mInboxListView = (ListView)findViewById(android.R.id.list); mInboxListView.setAdapter(new MessageAdapter(mMessages)); // Restore previous state (including selected item index and scroll position) mInboxListView.onRestoreInstanceState(state); } /** * A ViewHolder used to speed up this activity's ListView. */ static class ViewHolder { public TextView fromAddressTextView; public TextView dateTextView; public TextView subjectTextView; public TextView unreadTextView; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { ((MessageAdapter)mInboxListView.getAdapter()).notifyDataSetChanged(); } private class MessageAdapter extends ArrayAdapter<Message> { public MessageAdapter(ArrayList<Message> messages) { super(getBaseContext(), android.R.layout.simple_list_item_1, messages); } @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_inbox, parent, false); // Configure the view holder ViewHolder viewHolder = new ViewHolder(); viewHolder.fromAddressTextView = (TextView) convertView.findViewById(R.id.inbox_messagelist_item_fromaddress_textview); viewHolder.dateTextView = (TextView) convertView.findViewById(R.id.inbox_messagelist_item_date_textview); viewHolder.subjectTextView = (TextView) convertView.findViewById(R.id.inbox_messagelist_item_subject_textview); viewHolder.unreadTextView = (TextView) convertView.findViewById(R.id.inbox_messagelist_item_unread_textview); convertView.setTag(viewHolder); } ViewHolder holder = (ViewHolder) convertView.getTag(); // Get the message Message m = getItem(position); // Set the value that will be displayed in the 'date' field //mDateTextView = (TextView) convertView.findViewById(R.id.inbox_messagelist_item_date_textview); long messageTime = m.getTime(); long currentTime = System.currentTimeMillis() / 1000; Date currentDate = new Date(currentTime * 1000); Date messageDate = new Date(messageTime * 1000); Calendar calendar = Calendar.getInstance(); calendar.setTime(messageDate); int messageYear = calendar.get(Calendar.YEAR); int messageDay = calendar.get(Calendar.DAY_OF_YEAR); calendar.setTime(currentDate); int currentYear = calendar.get(Calendar.YEAR); int currentDay = calendar.get(Calendar.DAY_OF_YEAR); if (messageYear != currentYear) { SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy", Locale.getDefault()); sdf.setTimeZone(TimeZone.getDefault()); String formattedDate = sdf.format(messageDate); holder.dateTextView.setText(formattedDate); } else if (messageDay == currentDay) { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); sdf.setTimeZone(TimeZone.getDefault()); String formattedDate = sdf.format(messageDate); holder.dateTextView.setText(formattedDate); } else { SimpleDateFormat sdf = new SimpleDateFormat("dd MMM", Locale.getDefault()); sdf.setTimeZone(TimeZone.getDefault()); String formattedDate = sdf.format(messageDate); holder.dateTextView.setText(formattedDate); } // Configure the view for the 'from address' field String fromAddressString = m.getFromAddress(); // Set the value that will be displayed in the 'subject' field if (m.getSubject() == null) { holder.subjectTextView.setText("[No subject]"); } else { holder.subjectTextView.setText(m.getSubject()); } // Declare the colour variables int color; int r; int g; int b; // Check if we have an entry for this address in our address book. If we do, substitute the label of that entry for the address. AddressBookRecordProvider addBookProv = AddressBookRecordProvider.get(getApplicationContext()); ArrayList<AddressBookRecord> retrievedRecords = addBookProv.searchAddressBookRecords(AddressBookRecordsTable.COLUMN_ADDRESS, fromAddressString); if (retrievedRecords.size() > 0) { holder.fromAddressTextView.setText(retrievedRecords.get(0).getLabel()); holder.fromAddressTextView.setTextSize(14); r = retrievedRecords.get(0).getColourR(); g = retrievedRecords.get(0).getColourG(); b = retrievedRecords.get(0).getColourB(); } else { holder.fromAddressTextView.setText(fromAddressString); holder.fromAddressTextView.setTextSize(12); int[] colourValues = ColourCalculator.calculateColoursFromAddress(fromAddressString); r = colourValues[0]; g = colourValues[1]; b = colourValues[2]; } // Set the colours for this view color = Color.argb(0, r, g, b); holder.subjectTextView.setBackgroundColor(color); holder.fromAddressTextView.setBackgroundColor(color); holder.dateTextView.setBackgroundColor(color); convertView.setBackgroundColor(Color.argb(INBOX_COLOURS_ALPHA_VALUE, r, g, b)); // If this message is unread, show the 'unread' tag if (m.hasBeenRead() == false) { holder.unreadTextView.setVisibility(View.VISIBLE); } else { holder.unreadTextView.setVisibility(View.GONE); } // Need to create some final variables that can be used inside the onClickListener final int selectedColorR = r; final int selectedColorG = g; final int selectedColorB = b; final Message selectedMessage = m; convertView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.i(TAG, "Inbox list item clicked"); // Start the InboxMessageActivity Intent i = new Intent(getBaseContext(), InboxMessageActivity.class); i.putExtra(InboxMessageActivity.EXTRA_MESSAGE_ID, selectedMessage.getId()); i.putExtra(InboxMessageActivity.EXTRA_COLOUR_R, selectedColorR); i.putExtra(InboxMessageActivity.EXTRA_COLOUR_G, selectedColorG); i.putExtra(InboxMessageActivity.EXTRA_COLOUR_B, selectedColorB); startActivityForResult(i, 0); } }); 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: // We are already here, so there's nothing to do 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() { Log.i(TAG, "InboxActivity.onCacheWordLocked() called"); // 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 } }