From 348fa8daed6659c0d751595bf499c5dcbe9501b0 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Thu, 8 Oct 2015 14:11:45 +0200 Subject: [PATCH 001/110] Some sync adapter code and bugfixes around it - not yet functional --- app/src/main/AndroidManifest.xml | 141 +++++++++++------- .../apps/abit/AbstractItemListFragment.java | 27 +++- .../dissem/apps/abit/MessageListActivity.java | 48 +++++- .../dissem/apps/abit/MessageListFragment.java | 9 ++ .../abit/synchronization/Authenticator.java | 82 ++++++++++ .../synchronization/AuthenticatorService.java | 31 ++++ .../abit/synchronization/StubProvider.java | 65 ++++++++ .../abit/synchronization/SyncAdapter.java | 74 +++++++++ .../abit/synchronization/SyncService.java | 49 ++++++ .../activity_message_list.xml | 0 .../layout/activity_open_bitmessage_link.xml | 122 ++++++++------- app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/authenticator.xml | 6 + app/src/main/res/xml/preferences.xml | 19 ++- app/src/main/res/xml/syncadapter.xml | 9 ++ 16 files changed, 556 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java rename app/src/main/res/{layout-sw600dp => layout-w720dp}/activity_message_list.xml (100%) create mode 100644 app/src/main/res/xml/authenticator.xml create mode 100644 app/src/main/res/xml/syncadapter.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19861c3..7489cd6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,104 +1,131 @@ + xmlns:tools="http://schemas.android.com/tools" + package="ch.dissem.apps.abit"> - - - - - + + + + + + + + + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme"> + android:name=".MessageListActivity" + android:label="@string/app_name"> - + - + + android:name=".MessageDetailActivity" + android:label="@string/title_message_detail" + android:parentActivityName=".MessageListActivity" + tools:ignore="UnusedAttribute"> + android:name="android.support.PARENT_ACTIVITY" + android:value=".MessageListActivity" /> + android:name=".SubscriptionDetailActivity" + android:label="@string/title_subscription_detail" + android:parentActivityName=".MessageListActivity" + tools:ignore="UnusedAttribute"> + android:name="android.support.PARENT_ACTIVITY" + android:value=".MessageListActivity" /> + android:name=".ComposeMessageActivity" + android:label="Compose" + android:parentActivityName=".MessageListActivity"> + android:name="android.support.PARENT_ACTIVITY" + android:value=".MessageListActivity" /> - + - - - + + + - + - + - + - + - + - + - + + android:name=".SettingsActivity" + android:label="@string/settings" + android:parentActivityName=".MessageListActivity"> - + - + + android:name=".OpenBitmessageLinkActivity" + android:label="@string/title_activity_open_bitmessage_link" + android:theme="@style/Theme.AppCompat.Light.Dialog"> - + - - - + + + - - + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 7ddfce9..3500931 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; + import ch.dissem.apps.abit.listeners.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; @@ -54,6 +55,7 @@ public abstract class AbstractItemListFragment extends ListFragment { * The current activated item position. Only used on tablets. */ private int activatedPosition = ListView.INVALID_POSITION; + private boolean activateOnItemClick; abstract void updateList(Label label); @@ -75,6 +77,17 @@ public abstract class AbstractItemListFragment extends ListFragment { } } + @Override + public void onResume() { + super.onResume(); + + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + getListView().setChoiceMode(activateOnItemClick + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); + } + @Override public void onAttach(Activity activity) { super.onAttach(activity); @@ -118,11 +131,15 @@ public abstract class AbstractItemListFragment extends ListFragment { * given the 'activated' state when touched. */ public void setActivateOnItemClick(boolean activateOnItemClick) { - // When setting CHOICE_MODE_SINGLE, ListView will automatically - // give items the 'activated' state when touched. - getListView().setChoiceMode(activateOnItemClick - ? ListView.CHOICE_MODE_SINGLE - : ListView.CHOICE_MODE_NONE); + this.activateOnItemClick = activateOnItemClick; + + if (isVisible()) { + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + getListView().setChoiceMode(activateOnItemClick + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); + } } private void setActivatedPosition(int position) { diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 0ec6bfa..dbe16ea 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -1,5 +1,8 @@ package ch.dissem.apps.abit; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -13,6 +16,8 @@ import android.widget.AdapterView; import ch.dissem.apps.abit.listeners.ActionBarListener; import ch.dissem.apps.abit.listeners.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.apps.abit.synchronization.SyncAdapter; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; @@ -66,6 +71,8 @@ public class MessageListActivity extends AppCompatActivity private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class); private static final int ADD_IDENTITY = 1; + private Account account; + /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. @@ -97,6 +104,10 @@ public class MessageListActivity extends AppCompatActivity // res/values-sw600dp). If this view is present, then the // activity should be in two-pane mode. twoPane = true; + + // In two-pane mode, list items should be given the + // 'activated' state when touched. + listFragment.setActivateOnItemClick(true); } createDrawer(toolbar); @@ -107,17 +118,40 @@ public class MessageListActivity extends AppCompatActivity if (getIntent().hasExtra(EXTRA_SHOW_MESSAGE)) { onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); } + + account = createSyncAccount(this); + getContentResolver().setSyncAutomatically(account, SyncAdapter.AUTHORITY, true); + } + + private Account createSyncAccount(Context context) { + // Create the account type and default account + Account newAccount = new Account(Authenticator.ACCOUNT_NAME, Authenticator.ACCOUNT_TYPE); + // Get an instance of the Android account manager + AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE); + /* + * Add the account and account type, no password or user data + * If successful, return the Account object, otherwise report an error. + */ + if (accountManager.addAccountExplicitly(newAccount, null, null)) { + /* + * If you don't set android:syncable="true" in + * in your element in the manifest, + * then call context.setIsSyncable(account, AUTHORITY, 1) + * here. + */ + } else { + /* + * The account exists or some other error occurred. Log this, report it, + * or handle it internally. + */ + LOG.error("Couldn't add account"); + } + return newAccount; } @Override protected void onResume() { super.onResume(); - if (twoPane) { - // In two-pane mode, list items should be given the - // 'activated' state when touched. - ((MessageListFragment) getSupportFragmentManager().findFragmentById(R.id.item_list)) - .setActivateOnItemClick(true); - } } private void changeList(AbstractItemListFragment listFragment) { @@ -232,7 +266,7 @@ public class MessageListActivity extends AppCompatActivity if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment)) { - MessageListFragment listFragment = new MessageListFragment(); + MessageListFragment listFragment = new MessageListFragment(getApplicationContext()); changeList(listFragment); listFragment.updateList(selectedLabel); } else { diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index cb02e54..a5515c5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,6 +1,7 @@ package ch.dissem.apps.abit; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; @@ -11,6 +12,7 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; + import ch.dissem.apps.abit.listeners.ActionBarListener; import ch.dissem.apps.abit.listeners.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; @@ -41,6 +43,10 @@ public class MessageListFragment extends AbstractItemListFragment { public MessageListFragment() { } + public MessageListFragment(Context ctx) { + bmc = Singleton.getBitmessageContext(ctx); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -58,6 +64,9 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { @Override public void updateList(Label label) { currentLabel = label; + + if (!isVisible()) return; + setListAdapter(new ArrayAdapter<Plaintext>( getActivity(), android.R.layout.simple_list_item_activated_1, diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java new file mode 100644 index 0000000..5a6d3c3 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java @@ -0,0 +1,82 @@ +package ch.dissem.apps.abit.synchronization; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.NetworkErrorException; +import android.content.Context; +import android.os.Bundle; + +/* + * Implement AbstractAccountAuthenticator and stub out all + * of its methods + */ +public class Authenticator extends AbstractAccountAuthenticator { + public static final String ACCOUNT_NAME = "Bitmessage"; + public static final String ACCOUNT_TYPE = "bitmessage.dissem.ch"; + + // Simple constructor + public Authenticator(Context context) { + super(context); + } + + // Editing properties is not supported + @Override + public Bundle editProperties( + AccountAuthenticatorResponse r, String s) { + throw new UnsupportedOperationException(); + } + + // Don't add additional accounts + @Override + public Bundle addAccount( + AccountAuthenticatorResponse r, + String s, + String s2, + String[] strings, + Bundle bundle) throws NetworkErrorException { + return null; + } + + // Ignore attempts to confirm credentials + @Override + public Bundle confirmCredentials( + AccountAuthenticatorResponse r, + Account account, + Bundle bundle) throws NetworkErrorException { + return null; + } + + // Getting an authentication token is not supported + @Override + public Bundle getAuthToken( + AccountAuthenticatorResponse r, + Account account, + String s, + Bundle bundle) throws NetworkErrorException { + throw new UnsupportedOperationException(); + } + + // Getting a label for the auth token is not supported + @Override + public String getAuthTokenLabel(String s) { + throw new UnsupportedOperationException(); + } + + // Updating user credentials is not supported + @Override + public Bundle updateCredentials( + AccountAuthenticatorResponse r, + Account account, + String s, Bundle bundle) throws NetworkErrorException { + throw new UnsupportedOperationException(); + } + + // Checking features for the account is not supported + @Override + public Bundle hasFeatures( + AccountAuthenticatorResponse r, + Account account, String[] strings) throws NetworkErrorException { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java new file mode 100644 index 0000000..8fe717d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java @@ -0,0 +1,31 @@ +package ch.dissem.apps.abit.synchronization; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * A bound Service that instantiates the authenticator + * when started. + */ +public class AuthenticatorService extends Service { + /** + * Instance field that stores the authenticator object + */ + private Authenticator authenticator; + + @Override + public void onCreate() { + // Create a new authenticator object + authenticator = new Authenticator(this); + } + + /* + * When the system binds to this Service to make the RPC call + * return the authenticator's IBinder. + */ + @Override + public IBinder onBind(Intent intent) { + return authenticator.getIBinder(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java new file mode 100644 index 0000000..c79f074 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java @@ -0,0 +1,65 @@ +package ch.dissem.apps.abit.synchronization; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/* + * Define an implementation of ContentProvider that stubs out + * all methods + */ +public class StubProvider extends ContentProvider { + /* + * Always return true, indicating that the + * provider loaded correctly. + */ + @Override + public boolean onCreate() { + return true; + } + /* + * Return no type for MIME type + */ + @Override + public String getType(Uri uri) { + return null; + } + /* + * query() always returns no results + * + */ + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + return null; + } + /* + * insert() always returns null (no URI) + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + /* + * delete() always returns "no rows affected" (0) + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + /* + * update() always returns "no rows affected" (0) + */ + public int update( + Uri uri, + ContentValues values, + String selection, + String[] selectionArgs) { + return 0; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java new file mode 100644 index 0000000..890b097 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -0,0 +1,74 @@ +package ch.dissem.apps.abit.synchronization; + +import android.accounts.Account; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.Bundle; +import android.preference.PreferenceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; + +/** + * Sync Adapter to synchronize with the Bitmessage network - fetches + * new objects and then disconnects. + */ +public class SyncAdapter extends AbstractThreadedSyncAdapter { + private final static Logger LOG = LoggerFactory.getLogger(SyncAdapter.class); + + public static final String AUTHORITY = "ch.dissem.bitmessage.provider"; + + private final BitmessageContext bmc; + + public SyncAdapter(Context context) { + super(context, true, false); + bmc = Singleton.getBitmessageContext(context); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + // If the Bitmessage context acts as a full node, synchronization isn't necessary + if (bmc.isRunning()) return; + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + + String trustedNode = preferences.getString("trusted_node", null); + if (trustedNode == null) return; + trustedNode = trustedNode.trim(); + if (trustedNode.isEmpty()) return; + + int port; + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + String portString = trustedNode.substring(index + 1); + trustedNode = trustedNode.substring(0, index); + try { + port = Integer.parseInt(portString);// FIXME + } catch (NumberFormatException e) { + LOG.error("Invalid port " + portString); + // TODO: show error as notification + return; + } + } else { + port = 8444; + } + long timeoutInSeconds = preferences.getInt("sync_timeout", 120); + try { + LOG.info("Synchronization started"); + bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); + LOG.info("Synchronization finished"); + } catch (UnknownHostException e) { + LOG.error("Couldn't synchronize", e); + // TODO: show error as notification + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java new file mode 100644 index 0000000..fb6fd9f --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -0,0 +1,49 @@ +package ch.dissem.apps.abit.synchronization; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * Define a Service that returns an IBinder for the + * sync adapter class, allowing the sync adapter framework to call + * onPerformSync(). + */ +public class SyncService extends Service { + // Storage for an instance of the sync adapter + private static SyncAdapter syncAdapter = null; + // Object to use as a thread-safe lock + private static final Object syncAdapterLock = new Object(); + + /* + * Instantiate the sync adapter object. + */ + @Override + public void onCreate() { + /* + * Create the sync adapter as a singleton. + * Set the sync adapter as syncable + * Disallow parallel syncs + */ + synchronized (syncAdapterLock) { + if (syncAdapter == null) { + syncAdapter = new SyncAdapter(getApplicationContext()); + } + } + } + + /** + * Return an object that allows the system to invoke + * the sync adapter. + */ + @Override + public IBinder onBind(Intent intent) { + /* + * Get the object that allows external processes + * to call onPerformSync(). The object is created + * in the base class code when the SyncAdapter + * constructors call super() + */ + return syncAdapter.getSyncAdapterBinder(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_message_list.xml b/app/src/main/res/layout-w720dp/activity_message_list.xml similarity index 100% rename from app/src/main/res/layout-sw600dp/activity_message_list.xml rename to app/src/main/res/layout-w720dp/activity_message_list.xml diff --git a/app/src/main/res/layout/activity_open_bitmessage_link.xml b/app/src/main/res/layout/activity_open_bitmessage_link.xml index 95f792d..fedbd9d 100644 --- a/app/src/main/res/layout/activity_open_bitmessage_link.xml +++ b/app/src/main/res/layout/activity_open_bitmessage_link.xml @@ -1,83 +1,81 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="16dp"> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp"> <TextView - android:id="@+id/address" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="BM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - android:textAppearance="?android:attr/textAppearanceSmall"/> + android:id="@+id/address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="BM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + android:textAppearance="?android:attr/textAppearanceSmall" /> <android.support.design.widget.TextInputLayout - android:id="@+id/label_wrapper" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@+id/address"> + android:id="@+id/label_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/address"> <EditText - android:id="@+id/label" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textPersonName" - android:hint="@string/label"/> + android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label" + android:inputType="textPersonName" /> </android.support.design.widget.TextInputLayout> <Switch - android:id="@+id/import_contact" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/import_contact" - android:layout_below="@+id/label_wrapper" - android:layout_centerHorizontal="true" - android:layout_marginTop="8dp"/> + android:id="@+id/import_contact" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/label_wrapper" + android:layout_centerHorizontal="true" + android:layout_marginTop="8dp" + android:text="@string/import_contact" /> <Switch - android:id="@+id/subscribe" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/subscribe" - android:layout_below="@+id/import_contact" - android:layout_centerHorizontal="true" - android:layout_marginTop="8dp" - android:layout_marginBottom="8dp"/> + android:id="@+id/subscribe" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/import_contact" + android:layout_centerHorizontal="true" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:text="@string/subscribe" /> <Button - android:id="@+id/compose_message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Message" - style="?android:attr/borderlessButtonStyle" - android:layout_below="@+id/subscribe" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp"/> + android:id="@+id/do_import" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_below="@+id/subscribe" + android:layout_marginBottom="12dp" + android:layout_marginTop="12dp" + android:text="@string/do_import" /> <Button - android:id="@+id/do_import" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/do_import" - style="?android:attr/borderlessButtonStyle" - android:layout_below="@+id/subscribe" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp"/> + android:id="@+id/compose_message" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/do_import" + android:layout_below="@+id/subscribe" + android:layout_toLeftOf="@+id/do_import" + android:layout_toStartOf="@+id/do_import" + android:text="@string/write_message" /> <Button - android:id="@+id/cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/cancel" - style="?android:attr/borderlessButtonStyle" - android:layout_alignTop="@+id/do_import" - android:layout_toLeftOf="@+id/do_import" - android:layout_toStartOf="@+id/do_import"/> + android:id="@+id/cancel" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/compose_message" + android:layout_toLeftOf="@+id/compose_message" + android:layout_toStartOf="@+id/compose_message" + android:text="@string/cancel" /> </RelativeLayout> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2222570..b86c0f8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -33,4 +33,8 @@ <string name="stream_number">Stream #%d</string> <string name="enabled">Aktiv</string> <string name="title_subscription_detail">Abonnement</string> + <string name="sync_timeout">Zeitbeschränkung der Synchronisierung</string> + <string name="sync_timeout_summary">Timeout in Sekunden</string> + <string name="trusted_node">Vertrauenswürdiger Knoten</string> + <string name="trusted_node_summary">Diese Adresse wird für die Synchronisation verwendet</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43775b5..72fa388 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,4 +33,9 @@ <string name="empty_trash">Empty Trash</string> <string name="stream_number">Stream #%d</string> <string name="enabled">Enabled</string> + <string name="trusted_node">Trusted node</string> + <string name="trusted_node_summary">Use this node for synchronization</string> + <string name="sync_timeout">Synchronization Timeout</string> + <string name="sync_timeout_summary">Timeout in seconds</string> + <string name="write_message">Write message</string> </resources> diff --git a/app/src/main/res/xml/authenticator.xml b/app/src/main/res/xml/authenticator.xml new file mode 100644 index 0000000..aa46bf6 --- /dev/null +++ b/app/src/main/res/xml/authenticator.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" + android:accountType="bitmessage.dissem.ch" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:smallIcon="@mipmap/ic_launcher" /> \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e1f7327..d026e3f 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -9,8 +9,19 @@ android:entryValues="@array/connection_mode_values"/> --> <SwitchPreference - android:key="wifi_only" - android:title="@string/wifi_only" - android:summary="@string/wifi_only_summary" - android:defaultValue="true"/> + android:defaultValue="true" + android:key="wifi_only" + android:summary="@string/wifi_only_summary" + android:title="@string/wifi_only" /> + <EditTextPreference + android:inputType="textUri" + android:key="trusted_node" + android:summary="@string/trusted_node_summary" + android:title="@string/trusted_node" /> + <EditTextPreference + android:defaultValue="120" + android:inputType="number" + android:key="sync_timeout" + android:summary="@string/sync_timeout_summary" + android:title="@string/sync_timeout" /> </PreferenceScreen> \ No newline at end of file diff --git a/app/src/main/res/xml/syncadapter.xml b/app/src/main/res/xml/syncadapter.xml new file mode 100644 index 0000000..20cacd2 --- /dev/null +++ b/app/src/main/res/xml/syncadapter.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<sync-adapter + xmlns:android="http://schemas.android.com/apk/res/android" + android:contentAuthority="ch.dissem.bitmessage.provider" + android:accountType="ch.dissem.bitmessage" + android:userVisible="true" + android:supportsUploading="true" + android:allowParallelSyncs="false" + android:isAlwaysSyncable="true"/> \ No newline at end of file From 67c06b9884f2a9750bad6ff51a414960da400217 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 12 Oct 2015 14:44:01 +0200 Subject: [PATCH 002/110] Some sync adapter fixes and changes - still doesn't work --- app/src/main/AndroidManifest.xml | 3 +- .../dissem/apps/abit/MessageListActivity.java | 127 +++++++----------- .../dissem/apps/abit/service/Singleton.java | 1 - .../abit/synchronization/StubProvider.java | 7 + .../abit/synchronization/SyncAdapter.java | 25 +++- .../abit/synchronization/SyncService.java | 2 +- app/src/main/res/menu/compose.xml | 2 +- app/src/main/res/menu/main.xml | 10 -- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values/strings.xml | 4 +- 10 files changed, 82 insertions(+), 104 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7489cd6..f43e1e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -103,9 +103,10 @@ </intent-filter> </activity> + <!-- Synchronization --> <provider android:name=".synchronization.StubProvider" - android:authorities="ch.dissem.bitmessage.provider" + android:authorities="ch.dissem.apps.abit.provider" android:exported="false" android:syncable="true" /> <service android:name=".synchronization.AuthenticatorService"> diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index dbe16ea..bc40eec 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -2,27 +2,15 @@ package ch.dissem.apps.abit; import android.accounts.Account; import android.accounts.AccountManager; -import android.content.Context; +import android.content.ContentResolver; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; - -import ch.dissem.apps.abit.listeners.ActionBarListener; -import ch.dissem.apps.abit.listeners.ListSelectionListener; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.apps.abit.synchronization.Authenticator; -import ch.dissem.apps.abit.synchronization.SyncAdapter; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.Streamable; -import ch.dissem.bitmessage.entity.valueobject.Label; +import android.widget.CompoundButton; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; @@ -34,10 +22,11 @@ import com.mikepenz.materialdrawer.accountswitcher.AccountHeaderBuilder; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem; -import com.mikepenz.materialdrawer.model.SecondaryDrawerItem; +import com.mikepenz.materialdrawer.model.SwitchDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IProfile; import com.mikepenz.materialdrawer.model.interfaces.Nameable; +import com.mikepenz.materialdrawer.model.interfaces.OnCheckedChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +34,17 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; +import ch.dissem.apps.abit.listeners.ActionBarListener; +import ch.dissem.apps.abit.listeners.ListSelectionListener; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; + /** * An activity representing a list of Messages. This activity @@ -69,10 +69,9 @@ public class MessageListActivity extends AppCompatActivity public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class); + private static final long SYNC_FREQUENCY = 15 * 60; // seconds private static final int ADD_IDENTITY = 1; - private Account account; - /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. @@ -82,7 +81,6 @@ public class MessageListActivity extends AppCompatActivity private AccountHeader accountHeader; private BitmessageContext bmc; private Label selectedLabel; - private Menu menu; @Override protected void onCreate(Bundle savedInstanceState) { @@ -119,39 +117,22 @@ public class MessageListActivity extends AppCompatActivity onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); } - account = createSyncAccount(this); - getContentResolver().setSyncAutomatically(account, SyncAdapter.AUTHORITY, true); + createSyncAccount(); } - private Account createSyncAccount(Context context) { - // Create the account type and default account - Account newAccount = new Account(Authenticator.ACCOUNT_NAME, Authenticator.ACCOUNT_TYPE); - // Get an instance of the Android account manager - AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE); - /* - * Add the account and account type, no password or user data - * If successful, return the Account object, otherwise report an error. - */ - if (accountManager.addAccountExplicitly(newAccount, null, null)) { - /* - * If you don't set android:syncable="true" in - * in your <provider> element in the manifest, - * then call context.setIsSyncable(account, AUTHORITY, 1) - * here. - */ - } else { - /* - * The account exists or some other error occurred. Log this, report it, - * or handle it internally. - */ - LOG.error("Couldn't add account"); + private void createSyncAccount() { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = new Account(Authenticator.ACCOUNT_NAME, Authenticator.ACCOUNT_TYPE); + + if (AccountManager.get(this).addAccountExplicitly(account, null, null)) { + // Inform the system that this account supports sync + ContentResolver.setIsSyncable(account, AUTHORITY, 1); + // Inform the system that this account is eligible for auto sync when the network is up + ContentResolver.setSyncAutomatically(account, AUTHORITY, true); + // Recommend a schedule for automatic synchronization. The system may modify this based + // on other scheduled syncs and network utilization. + ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); } - return newAccount; - } - - @Override - protected void onResume() { - super.onResume(); } private void changeList(AbstractItemListFragment<?> listFragment) { @@ -253,12 +234,24 @@ public class MessageListActivity extends AppCompatActivity .withAccountHeader(accountHeader) .withDrawerItems(drawerItems) .addStickyDrawerItems( - new SecondaryDrawerItem() + new PrimaryDrawerItem() .withName(R.string.subscriptions) .withIcon(CommunityMaterial.Icon.cmd_rss_box), - new SecondaryDrawerItem() + new PrimaryDrawerItem() .withName(R.string.settings) - .withIcon(GoogleMaterial.Icon.gmd_settings) + .withIcon(GoogleMaterial.Icon.gmd_settings), + new SwitchDrawerItem() + .withName(R.string.full_node) + .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) + .withOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { + // TODO: warn user, option to restrict to WiFi + if (isChecked && !bmc.isRunning()) bmc.startup(); + else if (bmc.isRunning()) bmc.shutdown(); + } + }) + .withChecked(bmc.isRunning()) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -288,6 +281,8 @@ public class MessageListActivity extends AppCompatActivity case R.string.settings: startActivity(new Intent(MessageListActivity.this, SettingsActivity.class)); break; + case R.string.full_node: + return true; } } return false; @@ -297,36 +292,6 @@ public class MessageListActivity extends AppCompatActivity .build(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.main, menu); - this.menu = menu; - updateMenu(); - return true; - } - - private void updateMenu() { - boolean running = bmc.isRunning(); - menu.findItem(R.id.sync_enabled).setVisible(running); - menu.findItem(R.id.sync_disabled).setVisible(!running); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.sync_disabled: - bmc.startup(); - updateMenu(); - return true; - case R.id.sync_enabled: - bmc.shutdown(); - updateMenu(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - /** * Callback method from {@link ListSelectionListener} * indicating that the item with the given ID was selected. diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 76c3301..e16b235 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -1,6 +1,5 @@ package ch.dissem.apps.abit.service; -import android.app.NotificationManager; import android.content.Context; import ch.dissem.apps.abit.listeners.MessageListener; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java index c79f074..f5b3d2d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java @@ -10,6 +10,8 @@ import android.net.Uri; * all methods */ public class StubProvider extends ContentProvider { + public static final String AUTHORITY = "ch.dissem.apps.abit.provider"; + /* * Always return true, indicating that the * provider loaded correctly. @@ -18,6 +20,7 @@ public class StubProvider extends ContentProvider { public boolean onCreate() { return true; } + /* * Return no type for MIME type */ @@ -25,6 +28,7 @@ public class StubProvider extends ContentProvider { public String getType(Uri uri) { return null; } + /* * query() always returns no results * @@ -38,6 +42,7 @@ public class StubProvider extends ContentProvider { String sortOrder) { return null; } + /* * insert() always returns null (no URI) */ @@ -45,6 +50,7 @@ public class StubProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues values) { return null; } + /* * delete() always returns "no rows affected" (0) */ @@ -52,6 +58,7 @@ public class StubProvider extends ContentProvider { public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } + /* * update() always returns "no rows affected" (0) */ diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 890b097..c12d30c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -25,17 +25,32 @@ import ch.dissem.bitmessage.BitmessageContext; public class SyncAdapter extends AbstractThreadedSyncAdapter { private final static Logger LOG = LoggerFactory.getLogger(SyncAdapter.class); - public static final String AUTHORITY = "ch.dissem.bitmessage.provider"; - private final BitmessageContext bmc; - public SyncAdapter(Context context) { - super(context, true, false); + /** + * Set up the sync adapter + */ + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + bmc = Singleton.getBitmessageContext(context); + } + + /** + * Set up the sync adapter. This form of the + * constructor maintains compatibility with Android 3.0 + * and later platform versions + */ + public SyncAdapter( + Context context, + boolean autoInitialize, + boolean allowParallelSyncs) { + super(context, autoInitialize, allowParallelSyncs); bmc = Singleton.getBitmessageContext(context); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + LOG.info("Synchronizing Bitmessage"); // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) return; @@ -52,7 +67,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { String portString = trustedNode.substring(index + 1); trustedNode = trustedNode.substring(0, index); try { - port = Integer.parseInt(portString);// FIXME + port = Integer.parseInt(portString); } catch (NumberFormatException e) { LOG.error("Invalid port " + portString); // TODO: show error as notification diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index fb6fd9f..4beaa04 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -27,7 +27,7 @@ public class SyncService extends Service { */ synchronized (syncAdapterLock) { if (syncAdapter == null) { - syncAdapter = new SyncAdapter(getApplicationContext()); + syncAdapter = new SyncAdapter(getApplicationContext(), true); } } } diff --git a/app/src/main/res/menu/compose.xml b/app/src/main/res/menu/compose.xml index ee25c89..dae8007 100644 --- a/app/src/main/res/menu/compose.xml +++ b/app/src/main/res/menu/compose.xml @@ -5,5 +5,5 @@ android:id="@+id/send" app:showAsAction="always" android:icon="@drawable/ic_action_send" - android:title="@string/disable_sync"/>` + android:title="@string/send"/>` </menu> \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 9f35ce1..9eb7100 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -1,14 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> - <item - android:id="@+id/sync_enabled" - app:showAsAction="always" - android:icon="@drawable/ic_action_notification_sync" - android:title="@string/disable_sync"/> - <item - android:id="@+id/sync_disabled" - app:showAsAction="always" - android:icon="@drawable/ic_action_notification_sync_disabled" - android:title="@string/enable_sync"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b86c0f8..43a43cc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,8 +5,6 @@ <string name="wifi_only">Nur WLAN</string> <string name="wifi_only_summary">Nicht mit Mobilfunknetz verbinden</string> <string name="bitmessage_active">Bitmessage ist aktiv</string> - <string name="disable_sync">Synchronisieren ausschalten</string> - <string name="enable_sync">Synchronisieren einschalten</string> <string name="subject">Betreff</string> <string name="to">An</string> <string name="title_message_detail">Nachricht</string> @@ -37,4 +35,7 @@ <string name="sync_timeout_summary">Timeout in Sekunden</string> <string name="trusted_node">Vertrauenswürdiger Knoten</string> <string name="trusted_node_summary">Diese Adresse wird für die Synchronisation verwendet</string> + <string name="full_node">Aktiver Knoten</string> + <string name="send">Senden</string> + <string name="write_message">Schreiben</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72fa388..adf6794 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,8 +2,6 @@ <string name="app_name">Abit</string> <string name="title_message_detail">Message Detail</string> <string name="title_subscription_detail">Subscription Detail</string> - <string name="disable_sync">Disable Sync</string> - <string name="enable_sync">Enable Sync</string> <string name="bitmessage_active">Bitmessage active</string> <string name="wifi_mode">Wi-Fi Connection Mode</string> <string name="settings">Settings</string> @@ -38,4 +36,6 @@ <string name="sync_timeout">Synchronization Timeout</string> <string name="sync_timeout_summary">Timeout in seconds</string> <string name="write_message">Write message</string> + <string name="full_node">Full node</string> + <string name="send">Send</string> </resources> From 1659aaa1ee552de1d0f95396a0cec87da4bce9a4 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 12 Oct 2015 16:00:43 +0200 Subject: [PATCH 003/110] Added label "Archive" --- .../main/java/ch/dissem/apps/abit/MessageListActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 0ec6bfa..2bb0d6c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -212,6 +212,12 @@ public class MessageListActivity extends AppCompatActivity } drawerItems.add(item); } + drawerItems.add(new PrimaryDrawerItem() + .withName(R.string.archive) + .withTag(null) + .withIcon(CommunityMaterial.Icon.cmd_archive) + ); + new DrawerBuilder() .withActivity(this) From 13cb804fc288a51e3d964fa1a6cc69b1f7fd4b7b Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 18 Oct 2015 13:40:17 +0200 Subject: [PATCH 004/110] Added ongoing notification showing network status - renamed packages to be more consistent - somewhate refactored the way notifications are made --- .../apps/abit/AbstractItemListFragment.java | 2 +- .../apps/abit/MessageDetailFragment.java | 2 +- .../dissem/apps/abit/MessageListActivity.java | 9 +- .../dissem/apps/abit/MessageListFragment.java | 10 +-- .../ActionBarListener.java | 2 +- .../ListSelectionListener.java | 2 +- .../MessageListener.java | 60 ++----------- .../notification/AbstractNotification.java | 33 +++++++ .../notification/NetworkNotification.java | 87 +++++++++++++++++++ .../notification/NewMessageNotification.java | 81 +++++++++++++++++ .../AndroidAddressRepository.java | 2 +- .../AndroidInventory.java | 4 +- .../AndroidMessageRepository.java | 4 +- .../SqlHelper.java | 4 +- .../dissem/apps/abit/service/Singleton.java | 11 ++- .../apps/abit/{utils => util}/Assets.java | 2 +- .../apps/abit/{utils => util}/Drawables.java | 19 +++- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values/strings.xml | 4 + 19 files changed, 262 insertions(+), 82 deletions(-) rename app/src/main/java/ch/dissem/apps/abit/{listeners => listener}/ActionBarListener.java (94%) rename app/src/main/java/ch/dissem/apps/abit/{listeners => listener}/ListSelectionListener.java (95%) rename app/src/main/java/ch/dissem/apps/abit/{listeners => listener}/MessageListener.java (54%) create mode 100644 app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java rename app/src/main/java/ch/dissem/apps/abit/{repositories => repository}/AndroidAddressRepository.java (99%) rename app/src/main/java/ch/dissem/apps/abit/{repositories => repository}/AndroidInventory.java (98%) rename app/src/main/java/ch/dissem/apps/abit/{repositories => repository}/AndroidMessageRepository.java (99%) rename app/src/main/java/ch/dissem/apps/abit/{repositories => repository}/SqlHelper.java (96%) rename app/src/main/java/ch/dissem/apps/abit/{utils => util}/Assets.java (98%) rename app/src/main/java/ch/dissem/apps/abit/{utils => util}/Drawables.java (65%) diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 7ddfce9..65acd36 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -21,7 +21,7 @@ import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; -import ch.dissem.apps.abit.listeners.ListSelectionListener; +import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.valueobject.Label; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index dbca865..4652f6b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -7,7 +7,7 @@ import android.view.*; import android.widget.ImageView; import android.widget.TextView; import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.apps.abit.utils.Drawables; +import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 2bb0d6c..51de702 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -10,13 +10,13 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; -import ch.dissem.apps.abit.listeners.ActionBarListener; -import ch.dissem.apps.abit.listeners.ListSelectionListener; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.listener.ListSelectionListener; +import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.valueobject.Label; import com.mikepenz.community_material_typeface_library.CommunityMaterial; @@ -39,6 +39,8 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; /** @@ -288,6 +290,7 @@ public class MessageListActivity extends AppCompatActivity switch (item.getItemId()) { case R.id.sync_disabled: bmc.startup(); + new NetworkNotification(this).show(); updateMenu(); return true; case R.id.sync_enabled: diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index cb02e54..f251d8a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,21 +1,15 @@ package ch.dissem.apps.abit; -import android.app.Activity; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.ListFragment; import android.view.*; import android.widget.ArrayAdapter; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; -import ch.dissem.apps.abit.listeners.ActionBarListener; -import ch.dissem.apps.abit.listeners.ListSelectionListener; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; diff --git a/app/src/main/java/ch/dissem/apps/abit/listeners/ActionBarListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java similarity index 94% rename from app/src/main/java/ch/dissem/apps/abit/listeners/ActionBarListener.java rename to app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java index 11b309a..7dfcdf2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listeners/ActionBarListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.listeners; +package ch.dissem.apps.abit.listener; /** * Created by chris on 06.09.15. diff --git a/app/src/main/java/ch/dissem/apps/abit/listeners/ListSelectionListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java similarity index 95% rename from app/src/main/java/ch/dissem/apps/abit/listeners/ListSelectionListener.java rename to app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java index 6e050a3..03fdc19 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listeners/ListSelectionListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.listeners; +package ch.dissem.apps.abit.listener; /** * A callback interface that all activities containing this fragment must diff --git a/app/src/main/java/ch/dissem/apps/abit/listeners/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java similarity index 54% rename from app/src/main/java/ch/dissem/apps/abit/listeners/MessageListener.java rename to app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index 104ef70..edf17b3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listeners/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package ch.dissem.apps.abit.listeners; +package ch.dissem.apps.abit.listener; import android.annotation.TargetApi; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -34,9 +33,11 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.style.StyleSpan; + import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.notification.NewMessageNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.Plaintext; @@ -50,18 +51,17 @@ import java.util.LinkedList; * </p> */ public class MessageListener implements BitmessageContext.Listener { - private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD); private final Context ctx; private final NotificationManager manager; private final LinkedList<Plaintext> unacknowledged = new LinkedList<>(); - private final int pictureSize; private int numberOfUnacknowledgedMessages = 0; + private final NewMessageNotification notification; public MessageListener(Context ctx) { this.ctx = ctx.getApplicationContext(); this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); - this.pictureSize = getMaxContactPhotoSize(ctx); + this.notification = new NewMessageNotification(ctx); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @@ -94,57 +94,15 @@ public class MessageListener implements BitmessageContext.Listener { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); if (numberOfUnacknowledgedMessages == 1) { - Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText()); - bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()))) - .setContentTitle(plaintext.getFrom().toString()) - .setContentText(plaintext.getSubject()) - .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) - .setContentInfo("Info"); - - Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); - showMessageIntent.putExtra(MessageListActivity.EXTRA_SHOW_MESSAGE, plaintext); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentIntent(pendingIntent); - - builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent); - builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), pendingIntent); + notification.singleNotification(plaintext); } else { - builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setContentTitle(ctx.getString(R.string.n_new_messages, this.unacknowledged.size())) - .setContentText(ctx.getString(R.string.app_name)); - - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - synchronized (unacknowledged) { - inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)); - for (Plaintext msg : unacknowledged) { - Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); - sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - inboxStyle.addLine(sb); - } - } - builder.setStyle(inboxStyle); - - Intent intent = new Intent(ctx, MessageListActivity.class); - intent.setAction(MessageListActivity.ACTION_SHOW_INBOX); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0); - builder.setContentIntent(pendingIntent); + notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); } - - manager.notify(0, builder.build()); - } - - private Bitmap toBitmap(Identicon identicon) { - Bitmap bitmap = Bitmap.createBitmap(pictureSize, pictureSize, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - identicon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - identicon.draw(canvas); - return bitmap; + notification.show(); } public void resetNotification() { - manager.cancel(0); + notification.hide(); synchronized (unacknowledged) { unacknowledged.clear(); numberOfUnacknowledgedMessages = 0; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java new file mode 100644 index 0000000..60bb86d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java @@ -0,0 +1,33 @@ +package ch.dissem.apps.abit.notification; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; + +/** + * Some base class to create and handle notifications. + */ +public abstract class AbstractNotification { + protected final Context ctx; + protected final NotificationManager manager; + public Notification notification; + + + public AbstractNotification(Context ctx) { + this.ctx = ctx; + this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * @return an id unique to this notification class + */ + protected abstract int getNotificationId(); + + public void show() { + manager.notify(getNotificationId(), notification); + } + + public void hide() { + manager.cancel(getNotificationId()); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java new file mode 100644 index 0000000..7ddb271 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -0,0 +1,87 @@ +package ch.dissem.apps.abit.notification; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v7.app.NotificationCompat; + +import java.util.Timer; +import java.util.TimerTask; + +import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.utils.Property; + +/** + * Shows the network status (as long as the client is connected as a full node) + */ +public class NetworkNotification extends AbstractNotification { + private final BitmessageContext bmc; + private NotificationCompat.Builder builder; + + public NetworkNotification(Context ctx) { + super(ctx); + builder = new NotificationCompat.Builder(ctx); + builder.setSmallIcon(R.drawable.ic_notification_new_message) + .setContentTitle(ctx.getString(R.string.bitmessage_active)); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + bmc = Singleton.getBitmessageContext(ctx); + } + + @SuppressLint("StringFormatMatches") + private boolean update() { + boolean running = bmc.isRunning(); + builder.setOngoing(running); + Property connections = bmc.status().getProperty("network").getProperty("connections"); + if (!running) { + builder.setContentText(ctx.getString(R.string.connection_info_disconnected)); + } else if (connections.getProperties().length == 0) { + builder.setContentText(ctx.getString(R.string.connection_info_pending)); + } else { + StringBuilder info = new StringBuilder(); + for (Property stream : connections.getProperties()) { + int streamNumber = Integer.parseInt(stream.getName().substring("stream ".length())); + Integer nodeCount = (Integer) stream.getProperty("nodes").getValue(); + if (nodeCount == 1) { + info.append(ctx.getString(R.string.connection_info_1, + streamNumber)); + } else { + info.append(ctx.getString(R.string.connection_info_n, + streamNumber, nodeCount)); + } + info.append('\n'); + } + builder.setContentText(info); + } + Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showMessageIntent, 0); + builder.setContentIntent(pendingIntent); + notification = builder.build(); + return running; + } + + @Override + public void show() { + update(); + super.show(); + + final Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + if (!update()) { + cancel(); + } + NetworkNotification.super.show(); + } + }, 10_000, 10_000); + } + + @Override + protected int getNotificationId() { + return 2; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java new file mode 100644 index 0000000..ee10bf1 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -0,0 +1,81 @@ +package ch.dissem.apps.abit.notification; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.support.v7.app.NotificationCompat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.StyleSpan; + +import java.util.LinkedList; + +import ch.dissem.apps.abit.Identicon; +import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.Plaintext; + +import static ch.dissem.apps.abit.util.Drawables.toBitmap; + +public class NewMessageNotification extends AbstractNotification { + private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD); + + public NewMessageNotification(Context ctx) { + super(ctx); + } + + public NewMessageNotification singleNotification(Plaintext plaintext) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); + Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText()); + bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.setSmallIcon(R.drawable.ic_notification_new_message) + .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192)) + .setContentTitle(plaintext.getFrom().toString()) + .setContentText(plaintext.getSubject()) + .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) + .setContentInfo("Info"); + + Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); + showMessageIntent.putExtra(MessageListActivity.EXTRA_SHOW_MESSAGE, plaintext); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(pendingIntent); + + builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent); + builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), pendingIntent); + notification = builder.build(); + return this; + } + + public NewMessageNotification multiNotification(LinkedList<Plaintext> unacknowledged, int numberOfUnacknowledgedMessages) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); + builder.setSmallIcon(R.drawable.ic_notification_new_message) + .setContentTitle(ctx.getString(R.string.n_new_messages, unacknowledged.size())) + .setContentText(ctx.getString(R.string.app_name)); + + NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + synchronized (unacknowledged) { + inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)); + for (Plaintext msg : unacknowledged) { + Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); + sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + inboxStyle.addLine(sb); + } + } + builder.setStyle(inboxStyle); + + Intent intent = new Intent(ctx, MessageListActivity.class); + intent.setAction(MessageListActivity.ACTION_SHOW_INBOX); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0); + builder.setContentIntent(pendingIntent); + notification = builder.build(); + return this; + } + + @Override + protected int getNotificationId() { + return 1; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java similarity index 99% rename from app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java rename to app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 6dee3d0..9d490df 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.repositories; +package ch.dissem.apps.abit.repository; import android.content.ContentValues; import android.database.Cursor; diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java similarity index 98% rename from app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java rename to app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index cafb76b..32b9ed8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.repositories; +package ch.dissem.apps.abit.repository; import android.content.ContentValues; import android.database.Cursor; @@ -36,7 +36,7 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import static ch.dissem.apps.abit.repositories.SqlHelper.join; +import static ch.dissem.apps.abit.repository.SqlHelper.join; import static ch.dissem.bitmessage.utils.UnixTime.now; /** diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java similarity index 99% rename from app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java rename to app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 30bacb8..eba1b0d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repositories/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.repositories; +package ch.dissem.apps.abit.repository; import android.content.ContentValues; import android.content.Context; @@ -40,7 +40,7 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; -import static ch.dissem.apps.abit.repositories.SqlHelper.join; +import static ch.dissem.apps.abit.repository.SqlHelper.join; /** * {@link MessageRepository} implementation using the Android SQL API. diff --git a/app/src/main/java/ch/dissem/apps/abit/repositories/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java similarity index 96% rename from app/src/main/java/ch/dissem/apps/abit/repositories/SqlHelper.java rename to app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 0d8abfd..f79bbb2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repositories/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package ch.dissem.apps.abit.repositories; +package ch.dissem.apps.abit.repository; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import ch.dissem.apps.abit.utils.Assets; +import ch.dissem.apps.abit.util.Assets; /** * Handles database migration and provides access. diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 76c3301..220dc36 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -1,13 +1,12 @@ package ch.dissem.apps.abit.service; -import android.app.NotificationManager; import android.content.Context; -import ch.dissem.apps.abit.listeners.MessageListener; -import ch.dissem.apps.abit.repositories.AndroidAddressRepository; -import ch.dissem.apps.abit.repositories.AndroidInventory; -import ch.dissem.apps.abit.repositories.AndroidMessageRepository; -import ch.dissem.apps.abit.repositories.SqlHelper; +import ch.dissem.apps.abit.listener.MessageListener; +import ch.dissem.apps.abit.repository.AndroidAddressRepository; +import ch.dissem.apps.abit.repository.AndroidInventory; +import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; diff --git a/app/src/main/java/ch/dissem/apps/abit/utils/Assets.java b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java similarity index 98% rename from app/src/main/java/ch/dissem/apps/abit/utils/Assets.java rename to app/src/main/java/ch/dissem/apps/abit/util/Assets.java index 71fe65b..e3e2dc6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/utils/Assets.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.apps.abit.utils; +package ch.dissem.apps.abit.util; import android.content.Context; diff --git a/app/src/main/java/ch/dissem/apps/abit/utils/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java similarity index 65% rename from app/src/main/java/ch/dissem/apps/abit/utils/Drawables.java rename to app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index d646608..e392e36 100644 --- a/app/src/main/java/ch/dissem/apps/abit/utils/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -14,11 +14,16 @@ * limitations under the License. */ -package ch.dissem.apps.abit.utils; +package ch.dissem.apps.abit.util; import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.view.Menu; + +import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.R; + import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; @@ -29,4 +34,16 @@ public class Drawables { public static void addIcon(Context ctx, Menu menu, int menuItem, GoogleMaterial.Icon icon) { menu.findItem(menuItem).setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.primary_text_default_material_dark).actionBar()); } + + public static Bitmap toBitmap(Identicon identicon, int size) { + return toBitmap(identicon, size, size); + } + + public static Bitmap toBitmap(Identicon identicon, int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + identicon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + identicon.draw(canvas); + return bitmap; + } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2222570..d296eca 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -30,7 +30,11 @@ <string name="archive">Archiv</string> <string name="empty_trash">Papierkorb leeren</string> <string name="mark_unread">Als ungelesen markieren</string> - <string name="stream_number">Stream #%d</string> + <string name="stream_number">Stream %d</string> <string name="enabled">Aktiv</string> <string name="title_subscription_detail">Abonnement</string> + <string name="connection_info_1">Stream %1$d: eine Verbindung</string> + <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> + <string name="connection_info_disconnected">Getrennt</string> + <string name="connection_info_pending">Verbindung wird aufgebaut…</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43775b5..baf9d23 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ <string name="title_activity_open_bitmessage_link">Import Contact</string> <string name="action_settings">Settings</string> + <string name="connection_info_1">Stream #%1$d: one connection</string> + <string name="connection_info_n">Stream #%1$d: %2$d connections</string> <string name="import_address">Import Address</string> <string name="import_contact">Add to contacts</string> <string name="label">Label</string> @@ -33,4 +35,6 @@ <string name="empty_trash">Empty Trash</string> <string name="stream_number">Stream #%d</string> <string name="enabled">Enabled</string> + <string name="connection_info_disconnected">Disconnected</string> + <string name="connection_info_pending">Connecting…</string> </resources> From 64e0479a37d7b29b62137c06c58a3d01b0caaa4e Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 18 Oct 2015 16:42:41 +0200 Subject: [PATCH 005/110] Changed notification icon and updated library versions --- app/build.gradle | 6 +++--- .../abit/notification/NetworkNotification.java | 8 ++++---- .../ic_notification_full_node.png | Bin 0 -> 423 bytes .../drawable-hdpi/ic_notification_full_node.png | Bin 0 -> 391 bytes .../ic_notification_full_node.png | Bin 0 -> 291 bytes .../drawable-mdpi/ic_notification_full_node.png | Bin 0 -> 317 bytes .../ic_notification_full_node.png | Bin 0 -> 584 bytes .../drawable-xhdpi/ic_notification_full_node.png | Bin 0 -> 562 bytes .../ic_notification_full_node.png | Bin 0 -> 976 bytes .../drawable-xxhdpi/ic_notification_full_node.png | Bin 0 -> 699 bytes 10 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-hdpi/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png diff --git a/app/build.gradle b/app/build.gradle index 0e3dcb4..be506a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,9 +22,9 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.android.support:support-v4:23.0.1' - compile 'com.android.support:design:23.0.1' + compile 'com.android.support:appcompat-v7:23.1.0' + compile 'com.android.support:support-v4:23.1.0' + compile 'com.android.support:design:23.1.0' compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 7ddb271..b51cc47 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -24,11 +24,11 @@ public class NetworkNotification extends AbstractNotification { public NetworkNotification(Context ctx) { super(ctx); - builder = new NotificationCompat.Builder(ctx); - builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setContentTitle(ctx.getString(R.string.bitmessage_active)); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); bmc = Singleton.getBitmessageContext(ctx); + builder = new NotificationCompat.Builder(ctx); + builder.setSmallIcon(R.drawable.ic_notification_full_node) + .setContentTitle(ctx.getString(R.string.bitmessage_active)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } @SuppressLint("StringFormatMatches") diff --git a/app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..337fc0404b7647452e5ba6b720d466933046efed GIT binary patch literal 423 zcmV;Y0a*TtP)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ0004QNkl<Zc-rmP zyDtPm0LSsWLnPu=2vJEiA|4%;P|^!Z@fQe{L~^teQIbeB9zl1N{{W@l<J2UZ6T;=Z z#&?n_HshAv+ufOFe#xhsz5FsakKIhh#l^)XT!au=3}6<^Sj8e{(2ty{B8`~95sG-i zJHAjs33u4XxE!BmnQpA(O@xaamzU3_QKlUy5z6>W9ilDCGOgGV@v@EPq!zDSxL(Xc zo?7tPT1wPoRIQiG8vdrBoTDRZnHDVJ2Hzst$lW*=walJ+Fs;b7IQ;KSF`z-sOhu#{ z`TO@MU=B?|g+_2K%tbDTXY9%&rCOo26xK|bd|>0(k)U>ytvp~-FXRckInWEeS}s)7 z3*A~SRMrcfS}ydc7n)Yv&{hh1p)Q<R+WKWIRJ+v&4Ihp|y=fk!Gw8&g!j-|9x<KDw zcUlfT*i^GsQkQK~1wZkk-n+NZ8}{a}lY@O<6?yf36xRyoF=Xt^#KpzMmmmHXQCEOu R9A*Fj002ovPDHLkV1kte!Q}t| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_notification_full_node.png b/app/src/main/res/drawable-hdpi/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..fa17418e84855938eecc9a756ee280f2ddfca0bc GIT binary patch literal 391 zcmV;20eJq2P)<h;3K|Lk000e1NJLTq000;O001Tk1^@s6-UnMH0003_Nkl<Zc-rk% z%SyvQ6dgC!f6$Hi2mXa%LHv()EA9g3or!tBR@vthT-0rKQ$_JVOhu@h@!UX*B2`k8 zg)#>Y8D{RBGv`i17?Y5YkoW^AisB$mQ#a4^$1KZU@q7UvFy>Jd?QbkBgxG@cd6Fb= zzV8dyb%o<N4QWhsSYLo=RNLAB!k0l1tVGw5YEa8tiK7Llbi$v+m!w*>^5k35s;UO) z>ll%5!!TUfwk_Ha_wls0RS@1q%zKK<ITvlkiL@_{<HIlFWm)cE;f|tOmh~$l`7;iB zg)~Pi1Bj2i5MPp#z=+aZGzMtPO;_R{>3N<&>yH^uKQ~QN^g+B`*9>lTUj}H`oEe5O z1w|i(NzLFg>dOG#nK0b=Hk@XC7~p<~&6@<1Jzdvlpy-O4rs<zPI7Lwgm>gko3f3D6 l)4qwSszdpslaTmNcn1ovAN9_7+*beq002ovPDHLkV1l3RucQC~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..074a2acbab3ea5e7e8b8b3e2a0d9c1afcb6ec32f GIT binary patch literal 291 zcmV+;0o?wHP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0002$Nkl<Zc-muN z7zIND{Qv)-1Bi`)*bj((fY=ZuMh$~mfH)S2&jax%ApQfzAof`xjs~eA*Kl?qo(nY) z7XZo42B{_0a7G~R{7*b+2k9ZkU~VAx0OEci{!W4gUx8SMkl~6zya|YZ{U;A};4@qR zh>!fI47>s2<<OMIf^I?4f9ipsKs*tMS&=Qc@Spl13E6_LG_c?dvITExV8J(J3%1d~ zf)~gZcmeTWDqFAsz0Bz$P%MxFo&vEddQxQt;!GgE4GKRX{zsyrAoX8>_%IL~6UuaU pKpYIjp(NlSAU3B;F+K{Y3IIqoOQx*0F#7-i002ovPDHLkV1gDtat{Ci literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_notification_full_node.png b/app/src/main/res/drawable-mdpi/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..02890154438263739941e580fa861ebc388c20ff GIT binary patch literal 317 zcmV-D0mA-?P)<h;3K|Lk000e1NJLTq000mG000>X1^@s6IQ*`u00035Nkl<Zc-muN z7zM)_Or1JaV#bUaUel&c^8~Sp)(F%f2-LV_`t<34r%ajhAE^01ko{-o%$bYk&z~=V zS2Iu%7f{35{{H^|-QC?N7$gQX{0z_#4h)T}R;^+NN=5-S@9ORC#n24HAUU9Gs!=re z_4RQAHSe1=X%bjLS63HaJYm9wJ3vDWkpru}r>EzCXJ;o-803=4lP811W)INI{0u<9 zJtkH&G6q=+G-M6~&{;n^Iy(N7gMo&8U;vtSmRtjXdf$T#2x)I`|4$AE8Zs9gkgct) z*==oYpMm&433wkc{16EXXc|AzAa|h2q4+RRV@OL&i^>20{~1R!{9pk9XLEoAr+3aC P00000NkvXXu0mjfw+4as literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..1d59a55d4ddcbbe07c7ea8892afda0c416554dc4 GIT binary patch literal 584 zcmV-O0=NB%P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0006HNkl<Zc-rmQ z&r4KM7{>8wG%zwz!7#O2C@6?K^)Cq82uUj!twazLv5jUSA}H9Rjp#zqqI8w++o%w_ z5?qLiD+?u1(L#)=MjbtV54<ZmGtQ5D@7xRLfzM{pnfJZibI*HEC}d+}V`F1u6ACG% zZs8%G;Vs57gU=Yn8#JK;VLug~gF1|08Q<|6{}B^^<+E)pVF<Oz&35rc=*A}g;6mM; z3;Ej(bfPd@#XpeSLtouXm3s95MDPivUKg+L;|1khzM|Z#;_u;$az6Sy^SxNGuarB{ zx717h8%`H}TON;%c#d7=UUpFJRN=X(K`TCB1;5f<u*pV0df#QdPw)i=XvQ3lTrT?h zZM256Oo}f=yF903%KPl;-8|jm@8AV~sO%+*Zy1m@SEk35WQq@?9$PB=ImfYF$a%D( z_@)N#Vlv5bz{|vB9ggV@G*JU}G8XtXQMte+@fsLZ{*Qi2^&rpGz>*3^_EBkS;HL^s zUYi<-1gL>UQv-(qYGB{gz-oXRh?p7}4^RVprUst+a1)D{1ycj}Wd;dCdgIOidW8NL zfPJ7U(J{%ehx30r=`-%c*N#iZGB_@AE2o%>I|~`sL%)o(Q6EgB`c-xUk5hTM$-@&& zV;u*wTYK@Ac7_vq$zMcAQdd5~dqM-c@Lo>iW?iJuhd#<$Y{4U69R}Oj*x1<EIGHQu W#5SAoq^T4D0000<MNUMnLSTa6Lkzh9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_full_node.png b/app/src/main/res/drawable-xhdpi/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1cf07633c0efcd87500e580117e26f3d2615c7 GIT binary patch literal 562 zcmV-20?qx2P)<h;3K|Lk000e1NJLTq001BW001%w1^@s6J!IKr0005`Nkl<Zc-rlj z%S&596vl5U6pD*NS8lo~>c*lgmkKR{_$V%2bS3@=u9{6r;pY8*Ke&h+H`(M52;ByR zLLjzXwxB2_S@lI1NgaPj5=kQX%Hvv?4?gDJ+?n~lIp@qw3`0XhLqkJD<9~x?Sx>Xs z>^k&kGMQc49q7&H^K0^LXu^ohrqk&V?Vn^asbaBM7v(p;{}yAkGXoP=N<CnuuTrVh zmuNKlFAxZ*tB9E<W<JyZ<)Bu+#f0@xD5S0vZ&Ugv5{Vj-*z8N3yiUdA@snUMcw_N4 zWsHo)#PeH;77B%jgzp&x7imj(5$`m?9K~EN_vA+63>ssANyy8%uZP3orr+;Z{g63o zQbX5@ld~nns}!1Q=eu8Vq0GbF(JRFfo}klL3E3b-dl`vD4oT@dNvrItUh#3lU6PJI zpKmB^nUJECnkci_<xT?|g#6dA#CzG=9Y;iC{@egB#ZifOMg-jhihbI^(KoNxt42*s zG%(;WaP-#>148PeA_nSq86XNj4IVz&WdQz=2sk_*&nZ-+CfXH)>vNKqk2V-+kb<}8 zmk&<f*bs$J9Mdl@4kWkRy$!!ZH3A;9{TI59WNxj;d{}{1csfM!ebY2wU%C_8a5|lj z2+tfH-$M%)2EaX+%Vok<NxA-Ouc4u#p>eNp240ev3vem-JOBUy07*qoM6N<$g4OE) Ag#Z8m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..9b736bfbc9a79b9db4edf8551b2937648969c568 GIT binary patch literal 976 zcmV;>126oEP)<h;3K|Lk000e1NJLTq002k;002k`1^@s6RqeA!000A*Nkl<Zc-rln zTS!z<6owl|D=Q<A9;`HoprogwpofSg0*Op3B!V8Kpy)ynLQxq#R}fSLnH2@ypn8$$ zsf$E*4+9}nG6=e9R+grf&g}lh{DK6X8D~76b7rp}zNf)CXMXoud#$zis8K4FN~Kb% zR4SE9rBbO>|CKQ&1j1km*bYvB``|VB40=E}_yC&t*-@|=ECORdMmicZ7R(0~;5fJm z9?^t0@B{RMe$WSgf=+(+5<CDmz%j5M%mvx$V$5W)6jXs)@C`)F&?SG<0j`5RU>V4j zRTX4|ri0a73B6|cB%$~TtN{5zG$zb&x=eE<bfUv(P?X=<#%ELnA~!J!PXOiM8fZ1{ zOP+BRtODcZ9sF$2I53;Jro)V!+W8NeCGX&8gNnf!<DKN$#eOwsA-Ca=9vUR|(-APw zZ+RyR>;v`2TS@Z12X^tDLVhqP6HEk8Snzo*sl=aivj}@(NH)j=yFm@O%dqM<{?S)Z zIbt=E)EHnAx6%XP5_kq$Xim>C*Ey+5iqczbNiy6vM{0|FPzuh0M)pg7mb4{bz;ck| zwn1~i7WPnnH|AgM1ch#zvj$vX(HAkn(^*;hZdJsEG^fP`S1)Li%#+H%0x&J5yu^eo zU$TiqtRSn8zoC)++I|{Uz%n2+8G~{eTAcw6t3*a65*6#f%w!B&$)2Xqq^BY@NhX!m zoIHevvZYrsc5ToraGvAfoJ0(g*u3Jis@QEwDme&dBx2AG@YvWV=>qmj<sHwU(*fVs zQ_*b}#9=3c>O7w3cqu6dPD&&0WRR?$@5Wxq0$J;1keKtwVuNIX)HoT`mB#f?B9in{ z=VXv{?7uBGNG77KP6jnwo`WQMx}6NVWAQF#n18#R465cTvQ;vwZF4ecn~7aZu~m{@ znw<>Fx3Cu*EIH@D7_Xa`1HcAL4SEVH<Mm@DCU)Ls+ba5HVwWz8;u$38UuBHPh(!jy z2ZupcA~Wf!V1tQWD~VcS&>^tcBr;tzu~QAXYcp`F5l!>Zs%O;X4Rt<UPLtZxG~C3T zw<dNev>@4IAX!Np*{7si;;yGTVUz4t8HStWq*Cr~4FNYwR?R!+oPjg4P^u@b1GhU$ zSSQKN(uZtIq<8CNALF+$>}UHR@ArmvPAzvUNyt;&y6qt+uUo+>Hn>fLO!8fC#rKmd yJ91~J2#hv<z74BVsZ=VJN~Kb%R4SD!1^or;9O1d2CqDrI0000<MNUMnLSTXfX}N^} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png b/app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png new file mode 100644 index 0000000000000000000000000000000000000000..006b25ee9e3d2b102bd82dca7f94207fae85f736 GIT binary patch literal 699 zcmV;s0!00ZP)<h;3K|Lk000e1NJLTq001xm002t}1^@s6Hs=I|0007lNkl<Zc-rlm zOKTHR9L2}?M%-7l-$f8ak%EEhcTi~;LfuIa@|wKgk0N#xh3vXezkplU(pYzD+m%{^ zprN_`{zIZkHI~L?G8g^_4!N1k+<VTw=RT%&9EC!mP$(1%g+ifFC=?3CJke^kPE{(E z>(y#CQ>)cp!w;0d@!5gz``29VxmqX`&ILwzxm-Svut#u@vdQQ3CY#NgbUJNPsgy@4 z%Qg8;?&01~m|Mfhg}I7HNC+W&2pJK46y;udhUa%1jmFKn#D&HS#bVJDluRZkqC6{g zNL}G@_@r;*>GkKiTyA3FhbeV2H#VehKVp-WJWosfunDPqrsC=O+tcj%znJLJ#%$(< ztQh3=<E+FFNISc-vQ0&Fy<T4^l}ZoE=O)~d23*&jDWwezJos<n2yRl*4~a}BlQDjX zACUcG2MuQ?&N;pQSw#CKc+4nVnPgYJPX>E__Vze3-(trVA7bcw^A<iJOZ9is@CIE^ zSzd4abB{pyB0c{lkw};zQIa50?w_IBjLNlfOXL%A9W(=n&B*x)Bae+SCq{ao!&NuP z;)f}x0HT%nWq=iD#TM~HCVDLMOP&GN`B%0X5E7?tPOzsPvek~q<Hk1jm)biHg^rB| zgrukGFdX}~8sLSf?NHdV)qtc&=D-VE4M=)q4%~~yV#ZQ#Ic#E=DRgZ#z$i4my?`(4 zHX0y3%bo$Ubdd{RZ4w`iMz=>^dbqH}4+j>Rxb?@WG!ludV&njtAQSQK*mnaN3Ai6* z@kjmvjFDJ?>paj6kob2nbgb7*Cif*A--R!E_$}=CD7e#a^BL_gbMic^|HM!z6bgkx hp-?Ck3Wee~ege<<9~^<3M?(Mr002ovPDHLkV1jtXN2ve+ literal 0 HcmV?d00001 From e4d7bf48932804969f0d9e2717d79724d161e541 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 18 Oct 2015 18:17:57 +0200 Subject: [PATCH 006/110] Changed to using vector drawables for action icons --- .../ic_notification_full_node.png | Bin 423 -> 0 bytes .../res/drawable-hdpi/ic_action_close.png | Bin 246 -> 0 bytes .../res/drawable-hdpi/ic_action_delete.png | Bin 399 -> 0 bytes .../drawable-hdpi/ic_action_mark_unread.png | Bin 347 -> 0 bytes .../drawable-hdpi/ic_action_placeholder.png | Bin 678 -> 0 bytes .../res/drawable-hdpi/ic_action_reply.png | Bin 632 -> 0 bytes .../main/res/drawable-hdpi/ic_action_send.png | Bin 371 -> 0 bytes .../drawable-hdpi/ic_fab_compose_message.png | Bin 862 -> 0 bytes .../ic_notification_full_node.png | Bin 391 -> 0 bytes .../ic_notification_new_message.png | Bin 356 -> 0 bytes .../main/res/drawable-hdpi/ic_plus_sign.png | Bin 302 -> 0 bytes .../ic_notification_full_node.png | Bin 291 -> 0 bytes .../res/drawable-mdpi/ic_action_close.png | Bin 182 -> 0 bytes .../res/drawable-mdpi/ic_action_delete.png | Bin 257 -> 0 bytes .../drawable-mdpi/ic_action_mark_unread.png | Bin 257 -> 0 bytes .../drawable-mdpi/ic_action_placeholder.png | Bin 433 -> 0 bytes .../res/drawable-mdpi/ic_action_reply.png | Bin 412 -> 0 bytes .../main/res/drawable-mdpi/ic_action_send.png | Bin 277 -> 0 bytes .../drawable-mdpi/ic_fab_compose_message.png | Bin 593 -> 0 bytes .../ic_notification_full_node.png | Bin 317 -> 0 bytes .../ic_notification_new_message.png | Bin 273 -> 0 bytes .../main/res/drawable-mdpi/ic_plus_sign.png | Bin 232 -> 0 bytes .../ic_notification_full_node.png | Bin 584 -> 0 bytes .../res/drawable-xhdpi/ic_action_close.png | Bin 280 -> 0 bytes .../res/drawable-xhdpi/ic_action_delete.png | Bin 491 -> 0 bytes .../drawable-xhdpi/ic_action_mark_unread.png | Bin 394 -> 0 bytes .../drawable-xhdpi/ic_action_placeholder.png | Bin 844 -> 0 bytes .../res/drawable-xhdpi/ic_action_reply.png | Bin 849 -> 0 bytes .../res/drawable-xhdpi/ic_action_send.png | Bin 497 -> 0 bytes .../drawable-xhdpi/ic_fab_compose_message.png | Bin 1174 -> 0 bytes .../ic_notification_full_node.png | Bin 562 -> 0 bytes .../ic_notification_new_message.png | Bin 455 -> 0 bytes .../main/res/drawable-xhdpi/ic_plus_sign.png | Bin 383 -> 0 bytes .../ic_notification_full_node.png | Bin 976 -> 0 bytes .../res/drawable-xxhdpi/ic_action_close.png | Bin 422 -> 0 bytes .../res/drawable-xxhdpi/ic_action_delete.png | Bin 849 -> 0 bytes .../drawable-xxhdpi/ic_action_mark_unread.png | Bin 620 -> 0 bytes .../drawable-xxhdpi/ic_action_placeholder.png | Bin 1391 -> 0 bytes .../res/drawable-xxhdpi/ic_action_reply.png | Bin 1364 -> 0 bytes .../res/drawable-xxhdpi/ic_action_send.png | Bin 814 -> 0 bytes .../ic_fab_compose_message.png | Bin 1821 -> 0 bytes .../ic_notification_full_node.png | Bin 699 -> 0 bytes .../ic_notification_new_message.png | Bin 660 -> 0 bytes .../main/res/drawable-xxhdpi/ic_plus_sign.png | Bin 759 -> 0 bytes .../res/drawable-xxxhdpi/ic_action_delete.png | Bin 1072 -> 0 bytes .../res/drawable-xxxhdpi/ic_action_reply.png | Bin 1929 -> 0 bytes .../res/drawable-xxxhdpi/ic_plus_sign.png | Bin 1043 -> 0 bytes .../main/res/drawable/ic_action_archive.xml | 9 ++++ app/src/main/res/drawable/ic_action_close.xml | 9 ++++ .../drawable/ic_action_compose_message.xml | 9 ++++ .../res/drawable/ic_action_mark_unread.xml | 9 ++++ app/src/main/res/drawable/ic_action_reply.xml | 9 ++++ app/src/main/res/drawable/ic_action_send.xml | 9 ++++ .../drawable/ic_notification_full_node.xml | 9 ++++ .../main/res/layout/fragment_message_list.xml | 2 +- .../res/layout/fragment_subscribtions.xml | 2 +- app/src/main/res/layout/toolbar_layout.xml | 47 +++++++++--------- app/src/main/res/menu/message.xml | 8 +-- app/src/main/res/menu/message_list.xml | 2 +- app/src/main/res/values/colors.xml | 1 + build.gradle | 2 +- 61 files changed, 95 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_close.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_delete.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_mark_unread.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_placeholder.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_reply.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_send.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_fab_compose_message.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_notification_new_message.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_plus_sign.png delete mode 100644 app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_close.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_delete.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_mark_unread.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_placeholder.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_reply.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_action_send.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_fab_compose_message.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_notification_new_message.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_plus_sign.png delete mode 100644 app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_close.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_delete.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_mark_unread.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_placeholder.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_reply.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_send.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_fab_compose_message.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_new_message.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_plus_sign.png delete mode 100644 app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_close.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_delete.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_mark_unread.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_placeholder.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_reply.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_send.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_fab_compose_message.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_plus_sign.png delete mode 100755 app/src/main/res/drawable-xxxhdpi/ic_action_delete.png delete mode 100755 app/src/main/res/drawable-xxxhdpi/ic_action_reply.png delete mode 100755 app/src/main/res/drawable-xxxhdpi/ic_plus_sign.png create mode 100644 app/src/main/res/drawable/ic_action_archive.xml create mode 100644 app/src/main/res/drawable/ic_action_close.xml create mode 100644 app/src/main/res/drawable/ic_action_compose_message.xml create mode 100644 app/src/main/res/drawable/ic_action_mark_unread.xml create mode 100644 app/src/main/res/drawable/ic_action_reply.xml create mode 100644 app/src/main/res/drawable/ic_action_send.xml create mode 100644 app/src/main/res/drawable/ic_notification_full_node.xml diff --git a/app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-hdpi-v11/ic_notification_full_node.png deleted file mode 100644 index 337fc0404b7647452e5ba6b720d466933046efed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmV;Y0a*TtP)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ0004QNkl<Zc-rmP zyDtPm0LSsWLnPu=2vJEiA|4%;P|^!Z@fQe{L~^teQIbeB9zl1N{{W@l<J2UZ6T;=Z z#&?n_HshAv+ufOFe#xhsz5FsakKIhh#l^)XT!au=3}6<^Sj8e{(2ty{B8`~95sG-i zJHAjs33u4XxE!BmnQpA(O@xaamzU3_QKlUy5z6>W9ilDCGOgGV@v@EPq!zDSxL(Xc zo?7tPT1wPoRIQiG8vdrBoTDRZnHDVJ2Hzst$lW*=walJ+Fs;b7IQ;KSF`z-sOhu#{ z`TO@MU=B?|g+_2K%tbDTXY9%&rCOo26xK|bd|>0(k)U>ytvp~-FXRckInWEeS}s)7 z3*A~SRMrcfS}ydc7n)Yv&{hh1p)Q<R+WKWIRJ+v&4Ihp|y=fk!Gw8&g!j-|9x<KDw zcUlfT*i^GsQkQK~1wZkk-n+NZ8}{a}lY@O<6?yf36xRyoF=Xt^#KpzMmmmHXQCEOu R9A*Fj002ovPDHLkV1kte!Q}t| diff --git a/app/src/main/res/drawable-hdpi/ic_action_close.png b/app/src/main/res/drawable-hdpi/ic_action_close.png deleted file mode 100644 index 7031319f71d73acfdff14ec2746be72c7de7fc57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUty`C<PAr-gY&hX_sARy3MID6jV zl~vB&_dGp4-bh)^P=D{8_WN15wS+T=VvB%6Qct*%x#RWJw>J;|PEou$_YBvoTZ?9H z{d?wAI=jE&M3q?&R_D#Q)OqRAwLsrPnU~(Iy;h*EvUQsEJ+VD5uc!C-rigA2|L5=U zs_DHXr)jeBCC;r=-+MUKe~@3is5r;Udg1O*$5j|?oi4OX{4(bHtK1x?x?sBHi&Cjy uPRzASTlQZ$Y`^+|eU9wMEgm4}B*uxB)yJN*N}mh#2!p4qpUXO@geCwHq-a+F diff --git a/app/src/main/res/drawable-hdpi/ic_action_delete.png b/app/src/main/res/drawable-hdpi/ic_action_delete.png deleted file mode 100755 index d912755d26e13bf4041b0a32f5bc945d68fb648c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399 zcmV;A0dW3_P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00042Nkl<ZXhZFp zO-chn5QV=8y72&Bz?GK}Tu8iy%bY-5gdpM~<OD8r3&n+yOJtP`<N$6ILTE5y7EX6n zr3V>(v+Y+u@73#?Fd5MEXVCNh5rN;HpeFE_CNKo9jD+5SS0gqH^H}IOlj#z;b~f|| zEHW`)mV8zMN5BJc(vmctN1jbf(C2r(w)1_MhrnZoK&1IxOd>D=L|~^<vPiEdP}?Ha z@>%advtVu;)o9GLeRhQ+1g15*%;dQD<c|=Tc?txucb*s#@Tg_PY62ypbO#Cw$wIPF zNdZ}?p@3dw2ioPQaBZQIsCtoU3uz0LZpmTi+CnA$w1u>V{`(es>hL3P`lvfDUBc#h z<k#peaN#N9z%GHg10VXA@(dUOr=CIv><91+tPR*bBr<^%oJiaN=N0UfY_kGhyv?UZ tq69o5b&3^%PUSTu*8~hAbqeYI1h$0%HZfX9U8n#6002ovPDHLkV1h_jswn^f diff --git a/app/src/main/res/drawable-hdpi/ic_action_mark_unread.png b/app/src/main/res/drawable-hdpi/ic_action_mark_unread.png deleted file mode 100644 index c9ce72462afd9eec60c99f24a6f6374b5273a58f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 347 zcmV-h0i^zkP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0003ZNkl<Zc-rk) zK?=e!5Y*}es;!>=h2T*U#D91bQLJyOMSVqsC;wD8Fv1dwplwZs85k&KvpbWdo6t^& zAP9mW2%;GjMKKZ1JabaTz+U+9%smazfJXyU;mxCXTYFly6ZM7b9~-%$X1}WVu6phv z>9s-O7u4vLY52y#Mz|G*j*%6L)hT@C2oF(vQ^!Y~3wOdniIJ>=@T^4m0JZWax{yKg zp^6EF%MXoY7!#;GL*zEUXrp#^4#L=35i2TvBRoZnp79vDav$e_K}_VV$U=Blac8{K zGag~AX~RKgMI41A-8~?B?sT&eVoe)QVk;`qTg%xh$p^x{UL{^e#^C7-;jOp}-9cXF tXn+Q2fCgy5i-G_Cu|*IBK@bG-9#6$BFeoiTS5W`}002ovPDHLkV1l^$k+T2* diff --git a/app/src/main/res/drawable-hdpi/ic_action_placeholder.png b/app/src/main/res/drawable-hdpi/ic_action_placeholder.png deleted file mode 100644 index cde7713adafb858e31d495ae647969ec49a05c61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 678 zcmV;X0$KfuP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0007QNkl<Zc-rln z$xB=@9LJygxPS|YLZKZ*R8T64Raz<Sp_CC?L=X=Z*Mki8;M!rlX(_FER*^0sp4^Hp z6-8X?idy}j#&6&^Fofid<1<N>1U{E6lkfK>?`8Ro<9JAsNaQ_u?V)`w7;{V|bp8-5 zf{YuqO#wf^W5;BIfKl*{%S!@9pqzLBlDRqo@P*YiZ6F`~0;Ql6d<BQ#lH(Z6gCHmY zOKb!k(+MJqN!W!zw2TWf{m8WVCa{-)aM7r=nGnwAc%KL1JRjIgt<yl)EsBpapi6um zIE(4{1XO`)F5hkbzW{!)!{xtHEj|sba)Ne2E+_y8*D&yp#Akp%T-g@LQfVL{5w!s} z6PPaS$*)Pu+Ep5uQ!w{=mX$1S_|Y8t?5Q?z&ej$c1}Zd&kMdlFftMN$G<#j7YCsQY z0oj!AqYau4GGqFD1A0LNzaf+v=;8J?KS3U7q_2k#*w-!-EN~+;;`)N4un?RIhICN( z6apQRMX)s9Dl7WIX#)l(BxOx*2I?h^W(*jZl#~Um0c*e-um-FFYrq<?2I3p|d=mp@ zl185l7#Nh4J#{mX3cd-hL23;c@C(l1E9{UH??A)uc*0;Ed}jw4Q%6b`I{>q!<s&<- z-TyPOi-Bhv4ZKwq$T-q;FXUHsGuKKlwMqj))fc>#6eb!1!(d->fg7eeLlj3llRWJZ z)#(RMbO$<;!Eb>uCN__|)fUCgPNMftrb0~CfHhzZ+;Ia*f|yxUSPr*I+!oYhd1<DD z-Q5CVQ-XRSQGED+_^UxGEo@s9r5ula`Z4u<w4kx5=3feKr+e-H3ce+T(S})+umAu6 M07*qoM6N<$f?EF}U;qFB diff --git a/app/src/main/res/drawable-hdpi/ic_action_reply.png b/app/src/main/res/drawable-hdpi/ic_action_reply.png deleted file mode 100755 index 7414772f6a8ecd0e21d26893c7786ba7e9d1ff8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 632 zcmV-;0*C#HP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0006%Nkl<ZXhZGT z&C3o^7{~F?llPU^Y$OW{vanNZu)s$C1v~jK?4?++lVm3;DgOY8k}RHuSV&38OR*s2 zt*h>-yD`tqnKN@g&oecrW;1iHGr#Zkc4p3;fseU?>o)={bp*!<j1lPQfiZ<TI<T|( z7=e-t591#Gbarjx0)0l{7!Kmh5CKB`5&Rq?Kp{SYJ~)7B@q+^>iyss~mH5E`97SAJ zW7*e0beiyf-S~=kc#W5Mfgg!&z4f#<`#^{f&$qD&LErHl^LUEityR_-q>9NEUw-vR z+{NR*KvDow(x$CbKjAi>wKXiyZvyDc^Hn<k;t}q2sgXK>S&E+qdV{O@k`s;P01&!E z{FO=fYQ?dXDO48v8JF>)4M7t?tTl@wUPxnzzc^{VNgOEv1w_sskR+lsDfWtMh$3$s zsQ`-&aSIO{)o1LgP3aU~dy5rVjdfU?BBV6u8!n`DrZy2M-ci?Ht3KvdVI#I;8+J^t zxL-G(;Cd5$4nSQxrGAY@WOiXMc4K9Q&^Q=4kN0&Dxd01wr&NGxO`C8C`zk<Ur@lEW zfT~F302KN*;{^6h3I2t%b+ynX0#!B91)x}OKaOL~<VJie#aUD3=mTIno%#TX^|s?A z*5ef}%^IM9h?BX#4z@PnG@?e%A^s))8+}EfSU67R?)P{yw*{vrKS2)`1E8sG=RA*e zI)EEP1qj_cCx-(wPoW$Sce3fb20Dq!F&+VOyw<197=b>$nX}dyfgG>(Y4Z>NhHNn~ SmN@PJ0000<MNUMnLSTY!x)#L% diff --git a/app/src/main/res/drawable-hdpi/ic_action_send.png b/app/src/main/res/drawable-hdpi/ic_action_send.png deleted file mode 100644 index e6c165d03909ecc64fd5b0b5ae3ceb029c7de8ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 371 zcmV-(0gV2MP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0003xNkl<Zc-rmQ zy-LGS9ER~a*;N!26%iGY>LR%7E-o%E-hiD1MWqx(tsjWsq8A`8j=DGtYO%HVls<(V z$k1Z6vFH5D`@(N2ba<dmPLj)Mnx<)*W?)eiZDPoBz!~1Li(!iaTWG{Su!j*#0ZWMh z=M(!FwGc3fYMY2g{P`iqLXUXp5a4{Fgz-=V4t@wohLkZGTEKeBfMm!qra}pr{$+qu z!%6G=z5@pE_QwbpQo)SRi2E)AoH{Ns=Odt)L4fm(Ys~jDU<FTjjqk|yj6!a(;Nw4$ zZyhNxi)E~01KZfa5iW3tM?A-O@J=zeDEJImO9T|#1iYjOSkzCbpZ~;JT}H2Zei_Z^ zMt1J)M%Lv~;xvS#g!#jq`Qvoj2bu{zQtF|;4_~X8f3aoz)uN_pnx<+0uqJ5;hgisF RBH91|002ovPDHLkV1j1;rZNBk diff --git a/app/src/main/res/drawable-hdpi/ic_fab_compose_message.png b/app/src/main/res/drawable-hdpi/ic_fab_compose_message.png deleted file mode 100644 index 4c86788aa39893ff08a92683b679f7caa13742ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0009eNkl<Zc-rlm z`%4u;6vy}WGAYbTBkkdXh+Fjf!63}~p<leh?eRnXtP#{LE@Wl}MnOa*^dc~+gc6e^ zva$y$8Cj57>i=~69ykMsVQ#y7y*oP>8u%c0W_SIZ-I+79v!3Tz$>nl2;41hCroafu zf?Ch@0?8(po;NT9&VgmF${%6Lc^?KrwM*hlKz9If$$MvlfJh_Afaf8Dct*j;DZUsa z48hzE5kxx9yZ#w`0zDw9weJUC0tC<>hkQABp^40aUQnf_)7L%$jK}o^7J+?WKoj~( z_2RhIK0&;-EuI9c#P0-XWpO3-8yptL)Pl)80rc3E&+tF}10I29HHh(d9cLBUrPAWu zg9iC&P-jb?1aE<#n#4nD<73Le9qmapiEG@`0!+ox_t%1pET?2B+w2jOnhatbRB+o4 zQJ=LTyB=KUCBjO>SdfF0o=mfN5`&_xTa1?vppWJd8}eg9;yaIla&gQGa9;~zmq?*g zqP~*rJ1H~SmY2MvuAr@hZi@B^>So+_Le#f$-BKHab<Ah-nNdO_2suD|MZLOK#zvp7 z25$ncVJM3n*SgcFXisweN$^8HwJ7CBL#`%gWh&$>Gs|t4xNZyBZ6h^?l~?=%+r+hQ zaQ%?2yuJp!Do|cQ-EKKwZ!6c`wKXX%R9<mP<oate-9{x}SwwlY3RO=bVMG395#<#v zHo8qz^2DL2<trkTZ?_>o6p?(#e|et(I(X_YJ$Vtt25=Od1(%B;f7*sLy;#)bX?w5$ z`7^fedK)jUXsYk0mkH6fnOBK!%CqEvL14RR-y1TBj7|B*JQ5??Yhi=-{+f?$<VyrC zfUbYY^r>}=*rZMIK8+LdW!g@eiD6K$oi|~Uk7Ms972;*&ia2jrYwxsr=Y`1AkeQ{5 z&y!zSy5f>=C}7r2i_NOJ*yLH_LIJt`i8%j2obo{iuL;_1-sHl^w$_m1-?+Qfc`g)) zJPs&M+2&Z}FWw>L>wO0AH1FB$^js{QvrXc(o!w6RKc1_F2k)e|<wsvmS|Z<vFY@4} o40IlB^h&AlVk<2dXr)yC0-)2;ff$><AOHXW07*qoM6N<$g8jIisQ>@~ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_full_node.png b/app/src/main/res/drawable-hdpi/ic_notification_full_node.png deleted file mode 100644 index fa17418e84855938eecc9a756ee280f2ddfca0bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 391 zcmV;20eJq2P)<h;3K|Lk000e1NJLTq000;O001Tk1^@s6-UnMH0003_Nkl<Zc-rk% z%SyvQ6dgC!f6$Hi2mXa%LHv()EA9g3or!tBR@vthT-0rKQ$_JVOhu@h@!UX*B2`k8 zg)#>Y8D{RBGv`i17?Y5YkoW^AisB$mQ#a4^$1KZU@q7UvFy>Jd?QbkBgxG@cd6Fb= zzV8dyb%o<N4QWhsSYLo=RNLAB!k0l1tVGw5YEa8tiK7Llbi$v+m!w*>^5k35s;UO) z>ll%5!!TUfwk_Ha_wls0RS@1q%zKK<ITvlkiL@_{<HIlFWm)cE;f|tOmh~$l`7;iB zg)~Pi1Bj2i5MPp#z=+aZGzMtPO;_R{>3N<&>yH^uKQ~QN^g+B`*9>lTUj}H`oEe5O z1w|i(NzLFg>dOG#nK0b=Hk@XC7~p<~&6@<1Jzdvlpy-O4rs<zPI7Lwgm>gko3f3D6 l)4qwSszdpslaTmNcn1ovAN9_7+*beq002ovPDHLkV1l3RucQC~ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_new_message.png b/app/src/main/res/drawable-hdpi/ic_notification_new_message.png deleted file mode 100644 index 096746b4dfec5c879cbbf9a98c43a180cd3d7fcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 356 zcmV-q0h|7bP)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ0003iNkl<Zc-rll zF$%&!5Jk1o!a_-{g@Pc~>AXWg$Q?wiy@7W2c6J^?B(+^C5eq>NAc%!s8eRXwfJ;zU zOeTrAA6~V^K4vnrkX+8h<liHND1jzufI?al1<(UNxQa&t-~k8ZlagtHjnHQStR!Sk z;`6LQnX^r5WoEzyBWl&F%(;Yms!Zr?@J8qcYz`q`xx@{f)ite~a*Vb*O{36xS0l8I zTc{bjAfPtYN#>{#y2LHi5Yckxn6My%kk9c^dl7`j970Y6p$>;oo}PnmvIjK|DT|(i zOi7jHb3hw78&V`@%MC1-p4>BXv}v1<<u^l@Co829t{JIcma2k2ama*}BDcy+rATN3 zmSFcps*+}<hEFQhHbR=|OHYWTDpEqBX#NG5#GN}JX}QpzEyDT$0000<MNUMnLSTY+ CP?a<Q diff --git a/app/src/main/res/drawable-hdpi/ic_plus_sign.png b/app/src/main/res/drawable-hdpi/ic_plus_sign.png deleted file mode 100755 index 54aef0566744e0ce8e5335b666401256d624613e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 302 zcmV+}0nz@6P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0002>Nkl<ZXhZE; zyA1*{41FGgMGyl}u>l1WumU5n0uxZM0Tlxv7GVe@B&z5R-HT(P9Pet!=l5~qD2C2I z>%4vlz^@gQ0<%=W-PZti1-P@B-C6@qv5>oO0UU~Ak7jlefPw)jFk}f?h-eg9NERaO zG88Hc$wIPFkO5gJNZ{x$^cjg4mHC$dHpSX2fJbG%=h!Iz4ZsP&s`7`em)*^5Pn`qq zE&vT>_)i7i_u77&I3m9;Gz5E^Mm0(hcA*ZkNM1ED!Je8}O-$HDO+1w>?Lt$T0&-p{ z=b%8}PfUaZ!l5Y#S-2`9fdCL>rG!d>6k3U-FJDc#F#+#y(f|Me07*qoM6N<$g4UgJ ACjbBd diff --git a/app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-mdpi-v11/ic_notification_full_node.png deleted file mode 100644 index 074a2acbab3ea5e7e8b8b3e2a0d9c1afcb6ec32f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 291 zcmV+;0o?wHP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0002$Nkl<Zc-muN z7zIND{Qv)-1Bi`)*bj((fY=ZuMh$~mfH)S2&jax%ApQfzAof`xjs~eA*Kl?qo(nY) z7XZo42B{_0a7G~R{7*b+2k9ZkU~VAx0OEci{!W4gUx8SMkl~6zya|YZ{U;A};4@qR zh>!fI47>s2<<OMIf^I?4f9ipsKs*tMS&=Qc@Spl13E6_LG_c?dvITExV8J(J3%1d~ zf)~gZcmeTWDqFAsz0Bz$P%MxFo&vEddQxQt;!GgE4GKRX{zsyrAoX8>_%IL~6UuaU pKpYIjp(NlSAU3B;F+K{Y3IIqoOQx*0F#7-i002ovPDHLkV1gDtat{Ci diff --git a/app/src/main/res/drawable-mdpi/ic_action_close.png b/app/src/main/res/drawable-mdpi/ic_action_close.png deleted file mode 100644 index c71f1c4468bb206c78608d3816dc4c27e63ea067..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJT2B|pkcwMxr|srtP~>rzY~+-f zr77{OjLTB<sA>C;V<Ow87}ZT{Xc5rp+7+oZY0IXtIWKyz%?@MQ!`2@r$(i^eX>X<K zf$J-@kNBK7JN)*8;N#>Eat8`4xSz1)w|r>+-}0gLFtbBHLlx)3^8!{K3l>Z4>&c5a geuJOWHBfHV9tKI_06*qKK!-4Ry85}Sb4q9e0PF8W1poj5 diff --git a/app/src/main/res/drawable-mdpi/ic_action_delete.png b/app/src/main/res/drawable-mdpi/ic_action_delete.png deleted file mode 100755 index 864553a0637d42fb1fcfdf3d41b1151e535c2338..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ6P_-PArbCxuNihV845T&^w-IG z^H%5RG3|=(w|CrbXmiYS_`ucF8zz^$K0!GrJ$?UKG44rzQ+hw1Z{jdExSR6nnd*cs zHkMKT_u?6BvKBP+fBA2*OF8U^rtK{mhg&H%S68%8*=eEpG_^+Q;9VEKgqH7@Jl|c( z-B54YP%mY7f7ufuKP}H~JHM(joc3ynWN2wL5MVpxAi=@Bl}#+eyk=6p;d<5?NjC%@ zG?eN(i04b#Bq;g*Yn9!X%_zkezirJE@33nt6GIv1Fbd68RxG|Dkp}b=gQu&X%Q~lo FCIH>gWCs8M diff --git a/app/src/main/res/drawable-mdpi/ic_action_mark_unread.png b/app/src/main/res/drawable-mdpi/ic_action_mark_unread.png deleted file mode 100644 index f74f84a9259a037b6fc5ee3aea349dc1f52e364e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 257 zcmV+c0sj7pP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80002UNkl<Zc-rlj zT?&IR5QN2oxr6`cLoX@P($ceO?49fcCXkoXXpOC~10RIM?99VvF^WVY(Z6Y)=NOy= z5&HyC;28*YhyV$g0+^eciEkgY;IUDViM3i^^Akw9ZonPr6~>x(8`erqZQmXkKnF7J z)eC}H<C=Ff)*99{p$(EdL2hq?Xr5W)+%f+86^x*Ff>hv2Q_Xed827xh`7<;{FQDMJ zrGe{$XX74ozy>MFC*xp)NH2ULN=C5+I7EQo{*Z`7qP_J35O?VYq08Ei00000NkvXX Hu0mjf{?BgO diff --git a/app/src/main/res/drawable-mdpi/ic_action_placeholder.png b/app/src/main/res/drawable-mdpi/ic_action_placeholder.png deleted file mode 100644 index 6fd4dcd7dd3393a258d3c18c09cb4ff9d84f1d5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 433 zcmV;i0Z#sjP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80004aNkl<Zc-muN zAQJrl|34pyd5KWR0mLN?)CNF9<$*X0iO&MW5>U*FBpwgMS`4%RK!b~bI0c9o0r3eS zJ_E%d_A($&1>!1NS?&SEdyy=~g%1F65Vb8&Bf?^ITu3d;y^#!U1mYGvmi7Q~1rk4; zQp<UPcps9189<zk#{nSAW03eqfLMeQ2Skz)^yoN^5(iA6f&=DK;(+y3aKLU-EcAmW zGH)o~9-93q!Ol>#oS|li;IiBf&E=LryqO9ium^~B(Zsy5IUoZ~Q8E=B5P>FELW~1a zsOW&GQ3s4VVAKJ`I3OKOQ4$p$5KfE(%+VAXQqcjbKztHO%o$g4Yy-q)pkf%xr-}oh zW*Gr-DG<AmQaNp)mIKJGj;pEQfKEyrzyWQ7ff^?B(d&7rI0$b?lUo86FCwNhL#$&= z&@7pNB-e^2=SGWu!KecU+yQE6mNX&Bm7>WR(W-x#4ee}zI-;^j;^IKO0EmwQaXAn( b5^E*^zvitJ#sOu800000NkvXXu0mjf_qV67 diff --git a/app/src/main/res/drawable-mdpi/ic_action_reply.png b/app/src/main/res/drawable-mdpi/ic_action_reply.png deleted file mode 100755 index a25803b295f16129331b38dd17b1260d13d9aad8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 412 zcmV;N0b~A&P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80004FNkl<ZSV!&F zyGjF55XSLe5WH0uR@&K#rB5Ju>3jwYpT@${LQ4@#EiDBLu@q4dEi42PEG$G3uM8xx z#@)C%iP<ItTkQ_te`dZjXKN~sTE+VM0R0Z6c3=pv-7s(pz#{H&Rs}$a&ttm^fQEP! z-gAJgc+UYA5KlIG)`6IJRNowLxW^4HafQzmQ?@z<MdEonPdLF5p4$N_0%%7(tM`Hf z9G8X214tFmYF=Qs%(D6~fPlHh7NRSK(d`j53x!EH@m@sGEfcmXGW@WY0uVVxehAlp zU>I>RwTiF$D6aE9Dsvg-L1Y)vAjUC?DNJLuNi258Is)cLlmW==M5|3WfEmoyp&Eia zI86m;hA6v=XR(Zt`qtd&1Rx7h79euQu!0G!;UNoAb_Cg+@I=hCogb1g>MG80*eO8d zr~Hz|7?!Z#4S+vRUg7H)$77WMzsOQNkXn6iULT<H4txW$r$8a)g;M?i0000<MNUMn GLSTZ8Td2(d diff --git a/app/src/main/res/drawable-mdpi/ic_action_send.png b/app/src/main/res/drawable-mdpi/ic_action_send.png deleted file mode 100644 index ca5ad5ce44752a535bffbc419ec5488bd2aaa26d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277 zcmV+w0qXvVP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80002oNkl<Zc-rmO zy9&ZE6oBD+*_~YUF?3M`Q3MeLeF2>ueFA3(N2j_8Vg(Vgf}^XW?^J)G0SOduO^;v> zd~5SV+a!)-$Bvzd34$Oi9Z<v)Rmq4mu2`XlRMG+SFhE(Oo>&MAjkEw|gQoEi7N|r7 zC|k6UHWiS25}@qRM#e;h|6;`59-UZ2c-Zuf0Of#gEPDwL$)kh@`WRr031)EN;e?A8 zazyW|kT9V95HQpN7N!F}c?Eu)uR!i26#or>VIvaXiuJ?DOZqU9JC0ebAbKGYzmnOp b^EYz?Sa7w0eRmHZ00000NkvXXu0mjfh^uUu diff --git a/app/src/main/res/drawable-mdpi/ic_fab_compose_message.png b/app/src/main/res/drawable-mdpi/ic_fab_compose_message.png deleted file mode 100644 index ea4de0230320ba8211b9fe709ef68d6a9e064d68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 593 zcmV-X0<QguP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80006QNkl<Zc-qC6 z%PT}t9LMh%kCErZ!YeT*kD5YRNF=rtk|c_Sg`y-@vSLA2#)idsKQ<D{!uwH@*Z(xW zKYeeVrn$z<J$Ib?bm#u=o%8*jbAIP{e%CM@Y~gS?1&o19@B}tMyOE3rArDvqUr`Wy zrjz1cux}x55(4RtOgpbRpc|b2;>5Ua@g}bIF2&t4N-%5Tz?p6bYQa2sqreU*=ChF~ z4&3Sz&IZFE!21f$f{0uLKkai74lL_(U;?}=!Bqv)z#?r|pa^7ubJ}jq*|8&>rTi`f ztL*#*bTSVt(Y6U(V1P^1XjeSIfRhmof>hdjDewV07=(v(q6L(LAzk9?dLDpMK5wHF zFN_BX&QGwwS#jABuLF-3+e+58n(y_|7Ucapn6x``5r2r0;lzm&zQ0CWAp<kTj(82Y zk0EZdCo#dPmss%G9eF(BCjI<xt6gzFxQkO<IM$J15*7d1_jbj745&oJLu!&rM7)wY zJo(~uARmm%VIgZ7hxm>T@p{nAlqp-O06hOuJX=TLZ7ZGj(AJ@x@zL1TCGL;t_)f|1 zlK+n^l1`4UbA2v~z@<`XxFQZAt>T_v^8CA5K;n8rn;fyZI_tUa7(sT<8JMDYp6-Hj z>kpCMz;au=cvTGHSJrnv!--}Qe8L64?YvHZ!>Pe1rBje)l{*<9kzLB3pO)&@NM?iJ f40nLX_=oll%_gz8oQR?A00000NkvXXu0mjfFPs=j diff --git a/app/src/main/res/drawable-mdpi/ic_notification_full_node.png b/app/src/main/res/drawable-mdpi/ic_notification_full_node.png deleted file mode 100644 index 02890154438263739941e580fa861ebc388c20ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmV-D0mA-?P)<h;3K|Lk000e1NJLTq000mG000>X1^@s6IQ*`u00035Nkl<Zc-muN z7zM)_Or1JaV#bUaUel&c^8~Sp)(F%f2-LV_`t<34r%ajhAE^01ko{-o%$bYk&z~=V zS2Iu%7f{35{{H^|-QC?N7$gQX{0z_#4h)T}R;^+NN=5-S@9ORC#n24HAUU9Gs!=re z_4RQAHSe1=X%bjLS63HaJYm9wJ3vDWkpru}r>EzCXJ;o-803=4lP811W)INI{0u<9 zJtkH&G6q=+G-M6~&{;n^Iy(N7gMo&8U;vtSmRtjXdf$T#2x)I`|4$AE8Zs9gkgct) z*==oYpMm&433wkc{16EXXc|AzAa|h2q4+RRV@OL&i^>20{~1R!{9pk9XLEoAr+3aC P00000NkvXXu0mjfw+4as diff --git a/app/src/main/res/drawable-mdpi/ic_notification_new_message.png b/app/src/main/res/drawable-mdpi/ic_notification_new_message.png deleted file mode 100644 index 456614bfb69f6e8024e54d99ae303d66fb5dc3c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273 zcmV+s0q*{ZP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0002kNkl<Zc-muN z7zN~l|Ns9p0kJI*X900K5F1m|a0?*5_8$`*17bC582$~2{{rzdES9_iVkJs~<r>s* z8>m<=7E88~Yk@7)fM-Zz(pW6`2gEYuSdfKeNG{ay8UL{Z4{|I>|4%${A;$t^A`Sly z#3JOz!!aT(cmTw_<XWHxO^SrTVM;7f0wqOgYC#T`2UN2JYLN^mwSXcN6y_kqpoUV< z5-gU|&5{W~yq0E`KrPaxwNKc9SQUsjAX$=6i@IYPk_CmdumBV%`5?oA*l4u6CJ6ul Xf6GlTac*`+00000NkvXXu0mjf2&!d& diff --git a/app/src/main/res/drawable-mdpi/ic_plus_sign.png b/app/src/main/res/drawable-mdpi/ic_plus_sign.png deleted file mode 100755 index 72840771415e2e1392147e5268798aa1286e96e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ^`0({ArbCxr=I3yaujfhXMMPA zLHgo?U;z`(#|E6@W~{x3HZNP?xJ5(x#H)pw>|qO^-<dxvJV4j2PO8#gQ^DuJnJ|ZF zojcyIl*;Oq6PR^QUy8eA#;Ve&GJ$24fClGvi?*|kkt?kk-QCZp&X|$!nUQ8|P|DLi zWsa)t<<A%YPiDGy;K;|l9jxJ=yRYA@?3v^s<iS+$`(WOT?nzngiH9sFefg~W!R*>9 g?&t|jS%Nzly`R=7`X*V_0G-a@>FVdQ&MBb@07>y!jsO4v diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-xhdpi-v11/ic_notification_full_node.png deleted file mode 100644 index 1d59a55d4ddcbbe07c7ea8892afda0c416554dc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 584 zcmV-O0=NB%P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0006HNkl<Zc-rmQ z&r4KM7{>8wG%zwz!7#O2C@6?K^)Cq82uUj!twazLv5jUSA}H9Rjp#zqqI8w++o%w_ z5?qLiD+?u1(L#)=MjbtV54<ZmGtQ5D@7xRLfzM{pnfJZibI*HEC}d+}V`F1u6ACG% zZs8%G;Vs57gU=Yn8#JK;VLug~gF1|08Q<|6{}B^^<+E)pVF<Oz&35rc=*A}g;6mM; z3;Ej(bfPd@#XpeSLtouXm3s95MDPivUKg+L;|1khzM|Z#;_u;$az6Sy^SxNGuarB{ zx717h8%`H}TON;%c#d7=UUpFJRN=X(K`TCB1;5f<u*pV0df#QdPw)i=XvQ3lTrT?h zZM256Oo}f=yF903%KPl;-8|jm@8AV~sO%+*Zy1m@SEk35WQq@?9$PB=ImfYF$a%D( z_@)N#Vlv5bz{|vB9ggV@G*JU}G8XtXQMte+@fsLZ{*Qi2^&rpGz>*3^_EBkS;HL^s zUYi<-1gL>UQv-(qYGB{gz-oXRh?p7}4^RVprUst+a1)D{1ycj}Wd;dCdgIOidW8NL zfPJ7U(J{%ehx30r=`-%c*N#iZGB_@AE2o%>I|~`sL%)o(Q6EgB`c-xUk5hTM$-@&& zV;u*wTYK@Ac7_vq$zMcAQdd5~dqM-c@Lo>iW?iJuhd#<$Y{4U69R}Oj*x1<EIGHQu W#5SAoq^T4D0000<MNUMnLSTa6Lkzh9 diff --git a/app/src/main/res/drawable-xhdpi/ic_action_close.png b/app/src/main/res/drawable-xhdpi/ic_action_close.png deleted file mode 100644 index 2eb9589dd6307b9d20c948744606920c00047669..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=_dQ)4Ln>~)o#oAUSb>N2<i(<; zIz?_`JB3pnm#&yS$!z1ne78?h=Y#G31Tg`X1Hld#&D51!uSUrpi@SZJd-<QZ*DI## zI%j;^*}Hdl%Y{q!eyKu-+W+j{_T&pk+GzosV=~JYK767pEgg1m(ZPpL=jW72{ZyBl z=lAd2vE@_uJ112Atamwd;=huJ-@mSih@Wx?KYWr^JTm>iP*YotJIBYTPnjpwu`aQ1 zy0qW%W#@!1ArV!EDtl$7-4mF2PiXQzk*W8@rrSwOw3C`_FY~k_=5NyxO_sCTMi8eO Z80x=@O59$z(7*sB;OXk;vd$@?2>`T0cToTU diff --git a/app/src/main/res/drawable-xhdpi/ic_action_delete.png b/app/src/main/res/drawable-xhdpi/ic_action_delete.png deleted file mode 100755 index 37e9c219cb4f028f2715714dad4f178f5c726040..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 491 zcmV<H0Tlj;P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00059Nkl<ZcwX(A zu`WhI5QV=#(NL;IB-9FerB|R3ox~H+YIy>QPBdOYsVAW(5m75?BxJb1sL1Y_f8FGs zTko0K`F3{hDR#ubztO<!#{k>`11I1sFz^QO5NvS;7J+SGy0F_`fm7hVP*=2k(g5aw zor>beMFfNs;Hjvpg2lA}7J%75^2CX+3&sD0Z~+92-@a$yp`iB9rL_PyfYs7u6JCD> zoR{V>fKp=JdUt@Yz`$of<<04ztab?Yuae3gWdK;I%$+5>Rsv-JI8ru{pxskq7=S{r z)4Br^ASxkz1r&Oo0SZ0OfG+|bf(c!ugzykl=y?b#^gIMT1pOEAZ_qx#M#xUkRye^+ zC?RZx6?(S93O!q4TVY#aTVY#aTVY#aTj2yRp@jH%D|{$Cw^Ira?K`lq1j(?%2f&Q7 z2|@b;ME>e81HcxrT)hKLcnh2<Au<5W1G~VKvXKhy8*mIfR%pK^82}<!biigq2vK}I zi@i~b4{a8Jh!vf%1}t?Cq%&iW2VOjwz5_1RlOKc2-lTdU&GiP*9C^<Yx&wN4AkDYq h4rq?NX9)-20UuzfMnSPsvF-o>002ovPDHLkV1g#?&=3Fs diff --git a/app/src/main/res/drawable-xhdpi/ic_action_mark_unread.png b/app/src/main/res/drawable-xhdpi/ic_action_mark_unread.png deleted file mode 100644 index 35083d1a58497d7967701dd54f9d11e914ef1b63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU<~whaSW-r^>$WZ)*%NG*S~@; zyj>l;UkR~@uGG+I)jO}$=JMy|yi4L0%&h5bM^CC;EPhazIjOuqBduvJAJ7;OxS`Rt zNc(3-<Ss>4l}Y!fsX8(+2{beWFtJU_?mjhrGoRCauJlRyCzl>Ps&Dmp%_-A^TN;&o zK4zXdI#F=q^$45(pcuUi)6b~dp3W9N8J;4zGp;yk=JMs6zpdWEEfcHWuW~5-PL0QL ziR}mFPaaZ!q;Qf+?EIHG>mG5>)%<03Ks`z4roE=p<>DTBjkwcJD`b*1CE0`?YTHd& zV{<e+mUEs;c7MlQm9H}MUscB(y<XLOOuw~O$xTdf*$)kek8W>SPc5|1J^V*_W2uP$ zmm8P0bK1^LI&16va;sU@nbU9C<|;9X<sU8kk+bh}-~E>Dm2wlp0_LzQaX5hd$iSk& jzzAg4Pk;mjLqqgGhW=3f^X$xzf<Z!_u6{1-oD!M<?k}Ak diff --git a/app/src/main/res/drawable-xhdpi/ic_action_placeholder.png b/app/src/main/res/drawable-xhdpi/ic_action_placeholder.png deleted file mode 100644 index ae7c98f89c4e2d11a04a06b2084b3e63108d3975..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 844 zcmV-S1GD^zP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0009MNkl<Zc-rlo z-AhzK7{-rgVHbg=L{>`FMNovndLfw=NU#WjecFdcAeG)Igp`SRB@r1R-DE+~7h+r8 zlpsaf2UcqRPuugt95@`#%sIPzj`I$^13v`ko!NPw-JLTZ?|NR!rC2PU0#h0EI}EOS z+zSTT2!4as6#Hxj%iy5rLcu_X!1EOQy94fet`rP*6Ziu15p`8yGuTI;5=9GWpt^|o z7WIkR<+)Ta;7i~G*Z{VIUhoin0)I7-X)|De1S_+ifSaCcMG2n=Z@{WSw23+J3)H(N zejAuBQ*;zh!CqIy=fU?B#rx1x;C$3P!Abnp41|XnV`%~{Qr1Ac&p4>&B)-cqiOp3Y z^gVc{sk_2Od=prT7~r&ej8&^gTdCsTf?D+&BN6qhELMi<*&ZceBoH%E&deWZ-=~Qw zfEt_PTMYte_13Q@#^o}<0`$(G1z!y@d7B1It>MI{T`buWUsu69n>KrF7;ub<c%sLK z0RxN-n6zQQJw^t+vSGkuMh3jgN@P7atf&jsZBe^E4m5<uRtws}QK~Oje4pBj`!3UJ z?_8k%A{b@DwZOPK!D48|I8@RG3c)7~AGc1fUjh8x4^}iWgGt3##cW_+FgCz6F@H<6 zgQT^!!9~BL8m0ymV&a;T8er<=2UyS4fccoXonn9(AO?s5Vt^PR28aP-fEXYKhyh}N z7$62j8n8~?>*+dKWPBhp8xyxZ>9N>LLnkwgkHp3eF$;;4DiGMFIToY4zypj8XbhbQ z2RT{lv3Q5d^9v>{v~j-}p=`2HR>^13UCHGLB`h|(b~A+k?Vwv@n$zTJEF%LRFfyQE z!+>5!2Hdul5x&4c{Le%SoQzDM3G%f?@gG6XcA6Zm3UlCefVf2I$xu9TE2hnCDE^S2 zN9bbO?3su*M`GICcPU+73=jjv05L!e5Ci^W1NvjcbZZV_Ol>AyG9YIV@(1kJ>@AiI z?Ycadis4rxHw<kqXxco=>|i591L$=5av*h$au0jrGB#0vTpekxsAF#W+5E<=#`zC3 WyK6wXy9ZYQ0000<MNUMnLSTXd6?Lls diff --git a/app/src/main/res/drawable-xhdpi/ic_action_reply.png b/app/src/main/res/drawable-xhdpi/ic_action_reply.png deleted file mode 100755 index 8b1d03323c480f2b9a19583738875b34ac708a17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 849 zcmV-X1FrmuP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0009RNkl<ZcwX(A z&&y3g7{@=aANjSA;<aEyb{5J;%0l6_uvNAe{unD8$&ZCBQ6#dlAbDehjT8xyUx|bw zzxAz|I^E8>Gjq<&x#!$DQ*XW9GiPR=&+~nrd1mgNA@eqqx%~_P1_fkJU<iTC1u!JR zAq0jH$eh3s0+|b-)m>mc@TzOT+X{g>U=?sKM*uQ@7jQpE05X2J@%p{o09eo4Fv|)+ z(epO!vjH&b`K$nxc|IEeRi4iRz&OuOJ%BxCt8*1qQ<(%+;OzCR?*0Owfp@?g;D!17 z?)iR_ZZ5Bln^eY2`c2082|NKF0S|z0Nvs?*Xx+t!c;45hKfq()wt@1`_r)gt>Hvu0 zdDog19@h*ASKmg@Ljk1l{5S)i1DAlejjW6m0&RG{Y^lG1>%a{_i8N~SRY2RGFEdhV zI1hY`Nx%odq&#0{r?$~E!0T8L`UXID{{YSc4`V>^0ib!h5CTPel^0F}Pn$yU1z;gG zEqt1;xA?x4pm1meAp}4dmGA{PYD(-{a6<ufQ3)!VP5}yw;8q7f7nN`mxEgGI!-Gth zwnv>CjsY)2A=FiXohM{>_zJuS76OZbrNDAvS?HoG`pfv^fDTpOtto(zoKSTkA{Z-y z)xa8H9iXq#^nd93t^l{Zt*<db)!05t2)z+n3v2>50~?HTT()UfI1K!Db+pez36C4s zl@me%6z5t2Yz4Lfvkv1f1M`yr;MyIU0I>Ebe+a}*6C!1HegKa6svxEUiixqTVvGU6 z&anX4ZZ2b`j?t!5<6R#WqySK*gqQ%VZTian0I+eySe3^oCkdczcSr$1^T>%ifPH|J zp{BY($?%g4LMjCm!@*vgrvhN-+5+qcmY5H`l3{89MJ2TH0}0g+hfIO>3^*}q1r(K_ zLZ^+N$S5BiG}WfUK(|Phtqld($yMP+yUKESRuH6wi>}?EO#m)6#(u8EsA}>;R}>K2 zK%encE8NK$z#>;gIL#s8qmba^rq`f->NF^zPu@zg)<FR&sP<{hK>>a8R*JO_3P?e< bPh<WEixPf8$YVo000000NkvXXu0mjfC-iuN diff --git a/app/src/main/res/drawable-xhdpi/ic_action_send.png b/app/src/main/res/drawable-xhdpi/ic_action_send.png deleted file mode 100644 index 04daa14977652e133c72ff22e8599edbd080b3bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 497 zcmV<N0S^9&P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0005FNkl<Zc-rln z%PT}t9Ds+Jkr5Gp02^!B8(An})K&5r#KL1zmTXvBu*Jes$t!u}5k+1rYW}D3-8v`J zl+1AN%suz~zWVm}eBIZ1L?VJ92!bF8f*=S&apZEjW~{O~zyKa#(#jAFx)rm^L+r6C zggaP#$&286g1uO6ON8Bm0NPXR3w;O%nE4@q_6++m8rA^GUjk^)aS&r+{1j$O@Ct`Q z6@tOslo-MF8l6ytU=W*s2%x>i5saH0VCo+M@<WVbjfnv|JqVz^$8oGRDL?{0J&fS` zfN5hwaG}Q@2hcv^bl^j9f$Kg6&_3ZT)&(_y<8y%g5OY``v;gg83ZQ+)d29$ufT+3) zW^ftTa0~bFNPXtseF*W2i`Zz&%QA*7*oGb0g(;j+6RQ>6z-`>e3;ZY|#2YSQQ_us% z3j!onfV2v*ssik&0GCDtP>;ZHg&u)S#UFtRy#yD6^b&N+IPgGz2Ce@Q^7~gfVdG?I z(um2BOozQ$5V^xKQx`<6UlOx-QPlcn@e(&iQeoT}v3_gJ-px_#x5wpx#P<=IZd;GY n*nLP!5ClOG1VIo4!F0X={LZTJ@@%^i00000NkvXXu0mjfpA6hh diff --git a/app/src/main/res/drawable-xhdpi/ic_fab_compose_message.png b/app/src/main/res/drawable-xhdpi/ic_fab_compose_message.png deleted file mode 100644 index af0217a8945cb027787a822dab4d41938fa9226f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1174 zcmV;H1Zn$;P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF000DCNkl<Zc-rNg z`%4s26vt=XR?XBDD-?YFP-109;-jL{9+HtzB>fNtm6eo8+4~7*MJYs98c9Tn1x1PU z0DEa!t-aj;$<Bo{a5&sM?#`~e_l^S}(3v~4^SzIA&pG#wWf^`<O-=cLO5g_Y1{ei~ zfa}0UV2<V01H`2QSAmcNqEhNbMR`s9e4u|C@s!&@wpYX#1H%!Bw|NgNF<txyVBCdx zn{U8!Q^jva0ih`Q2;`bt2`NAsZ~`s9+e)}^gkcoZm})A+Szuojy`n;Q@e6>L*wTGK z6ObV(w+LG_(h9huv-lj~mBNU%0CQP+KX4S7jGzKu=v)D%z<Gyp{#JN^lKXZ6|H7RX zP4V`2qvhrT{sPTF61%4<%nEp_qhP<{d^&I%MROq?+)%>*tzlF^n{MLEfiLLgT@qHB zPgNNBe2|E_MJoYkj|UFoe*^hCiLX?6c!NNdWOf*kl)X%c1N@x_{7_WDA>CSk6>z~} zP8U&MIw_w9^h(O@WzRh#DRWIV)O4tb4B()mH;6dDn0bYK{{JIp5K<-+2(liPs)NvU zjOu(2PEfa`62!Dv#(KgaEAyPTFeXdvd-HS<KM#0>FEs+Rs|*sH-^p4qgyXu5d4iu= znFeyb5oeDzx{2pqS2#Xdl6zK21d7%yBxTkyFK~cdTLv7~L42A*JP%dZE0Q21tgMI= z(j;9ygx+8-u@!z@TVBQ_Ix^hDD*`^TvS-<}H$kq|>g3M$xro+8izZ8kLy)<{CFFXC z%nq0+o+dK`l7NSn=_1!E@LZ4R`lgGw>0}<BSPQtz3}_Z{fp2<AyiE&xem?MvT(6@Q zq(k1WCx+r}tG=53&PA0AoJjG>z?~S24-uy+gZx$ro)1#TDA6rHQ{rt-k>5)aX)Rf1 z$gAQ*IL|MnF@r9t>#jJ6w>h9&-aek<1y6JlA4q_BJ0t9yt>Q%#xH#dNTik&WZ}DRy zwAEqEcP2pmS77OE6+fnfcwZR%alF~>NhQ>Y<)jD~AfsVC;VrMIh-{z+*pBOPLo+14 zT$=!An(GZSAbzDz>8xQ)PwfFVVe4nIX>q`F;!)gI#f!GGa)nXu+0m=p6Jh-h+s$%V z*;2(iemMNps^v*8-pS&3OH$Xl(=1D?#8>KQ?c2k6`D2p%M51CF@XloM0o<$*P75BF zt-RnfoQ6bDOY!*f^$~XCUfJ$T3|-A>dGg{9T^O?OxK%BSJm;~Z<!f{nUlhSsGZ~HK zS!X0Uy|tFk2af4z=JRvpS=FYAPm!#^y4jcSCo{rp?Dl$#cWL}oRFh{`86iFj<0oC! zwM;wUr3+#d>rdcTw18<9&=!NS->k)IjZ4qZO&~Lg%iy&}f1?L4zr{ta?j+r*W%Ln2 z1osTs!9Q3qU3`IJ8#*;=o%pd*lI69-!T5<lX1`~?xKt%Pneh`n;U4b1^jls(i18c6 oZ}AGO*>qs|+Ni}A@&p|J0pSK-ga+l*1^@s607*qoM6N<$f~5>Dd;kCd diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_full_node.png b/app/src/main/res/drawable-xhdpi/ic_notification_full_node.png deleted file mode 100644 index 0b1cf07633c0efcd87500e580117e26f3d2615c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 562 zcmV-20?qx2P)<h;3K|Lk000e1NJLTq001BW001%w1^@s6J!IKr0005`Nkl<Zc-rlj z%S&596vl5U6pD*NS8lo~>c*lgmkKR{_$V%2bS3@=u9{6r;pY8*Ke&h+H`(M52;ByR zLLjzXwxB2_S@lI1NgaPj5=kQX%Hvv?4?gDJ+?n~lIp@qw3`0XhLqkJD<9~x?Sx>Xs z>^k&kGMQc49q7&H^K0^LXu^ohrqk&V?Vn^asbaBM7v(p;{}yAkGXoP=N<CnuuTrVh zmuNKlFAxZ*tB9E<W<JyZ<)Bu+#f0@xD5S0vZ&Ugv5{Vj-*z8N3yiUdA@snUMcw_N4 zWsHo)#PeH;77B%jgzp&x7imj(5$`m?9K~EN_vA+63>ssANyy8%uZP3orr+;Z{g63o zQbX5@ld~nns}!1Q=eu8Vq0GbF(JRFfo}klL3E3b-dl`vD4oT@dNvrItUh#3lU6PJI zpKmB^nUJECnkci_<xT?|g#6dA#CzG=9Y;iC{@egB#ZifOMg-jhihbI^(KoNxt42*s zG%(;WaP-#>148PeA_nSq86XNj4IVz&WdQz=2sk_*&nZ-+CfXH)>vNKqk2V-+kb<}8 zmk&<f*bs$J9Mdl@4kWkRy$!!ZH3A;9{TI59WNxj;d{}{1csfM!ebY2wU%C_8a5|lj z2+tfH-$M%)2EaX+%Vok<NxA-Ouc4u#p>eNp240ev3vem-JOBUy07*qoM6N<$g4OE) Ag#Z8m diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_new_message.png b/app/src/main/res/drawable-xhdpi/ic_notification_new_message.png deleted file mode 100644 index 5621edb8c8bd24d0a5b37aa83f15cbff25d12b0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 455 zcmV;&0XY7NP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0004wNkl<Zc-rll zy-or_6h?Q07QO%w6Dw;H5(_&i3Hku2Z$V*&v9qLvf`m69;T32oq_m)b#KMxq*bp0x z<2T7QStm@G)!n-@$^DYkz!dk)&z*rNl1L;H33rrIN1zQxU=Ciu3QR!{G(hZQ;Rm1t z)@pyALCdG&t6-+m<qPz|p%2Bc)Za4(r6RYDIrAU%*&>C+V^IPfqY!;iD~a!~z{Ww~ z6V!|7Hfv+%D?7&BIj+KlTcB+eFtd!SISOn+nNwh76tJ+2yGU^st~mwfI|{fo#>9GS zczKxVhBdRHJzt}lpQLQ5Lr#JJ^nP0goC4Dv1iG98y&MFZoC1xEi+|U53#UL#ugdWD ze?p=~ugWa)2F{9*=x4YJ{1W;-Pfy~YB})1hrB`Lp+o?{s?{-hO8Bme_Nsvex+|a5F zso<{e^FOUp7w5Hvqo_!Tgh>3BI05IxLy!^)ncHAj!a*R2?R2bvV-xoliDfANcsDHq xuOh%CaB69>VF)DdiKQ+&!BZrWNF<WH@(ulo{osJP^>zRN002ovPDHLkV1n~Z%QgT2 diff --git a/app/src/main/res/drawable-xhdpi/ic_plus_sign.png b/app/src/main/res/drawable-xhdpi/ic_plus_sign.png deleted file mode 100755 index d320ebf7ce139a50fe6d8a85b8d27f57240b6035..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV080zaSW-5dpmP4FOwlp>-=(u z00pZTkxgBk5e=LILdpRFL5CztdJZt~G&txas<f=oW0|7yV#@nlr{~Adlf8XD<Y)TS z+xiR*j8l~xxI_FHni2}d3%>U)@ocDcH{Lxx<*7Y`g!e7ho;kB`ax!o!F)(vA{k?PQ zZXsh5gX4tohV%(G3nUqZ6pb8%60;b)7EXF;c{IRti)Pyjw&)1H{Z$ozd)66E+QPMF z`?QjCJFc5vOYUKFET8Wgm0}Zea_P3TZU46@G8|dQ%=UokNW-h<gz!rWV%l{-?5Awu z>A9a$$EKfF=3T{FXYgkA!WT>&${Nld$;hymV*&3XD69(&PnQyT%awlX`CgvC48It^ zFjq}l#(3T8NL9to*i98YCvqKLRWVHHO1S;}aKpCy39o)8I%qZJ&X2ccmaBUD#c^Iv SelIX!89ZJ6T-G@yGywqmahHJr diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png b/app/src/main/res/drawable-xxhdpi-v11/ic_notification_full_node.png deleted file mode 100644 index 9b736bfbc9a79b9db4edf8551b2937648969c568..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 976 zcmV;>126oEP)<h;3K|Lk000e1NJLTq002k;002k`1^@s6RqeA!000A*Nkl<Zc-rln zTS!z<6owl|D=Q<A9;`HoprogwpofSg0*Op3B!V8Kpy)ynLQxq#R}fSLnH2@ypn8$$ zsf$E*4+9}nG6=e9R+grf&g}lh{DK6X8D~76b7rp}zNf)CXMXoud#$zis8K4FN~Kb% zR4SE9rBbO>|CKQ&1j1km*bYvB``|VB40=E}_yC&t*-@|=ECORdMmicZ7R(0~;5fJm z9?^t0@B{RMe$WSgf=+(+5<CDmz%j5M%mvx$V$5W)6jXs)@C`)F&?SG<0j`5RU>V4j zRTX4|ri0a73B6|cB%$~TtN{5zG$zb&x=eE<bfUv(P?X=<#%ELnA~!J!PXOiM8fZ1{ zOP+BRtODcZ9sF$2I53;Jro)V!+W8NeCGX&8gNnf!<DKN$#eOwsA-Ca=9vUR|(-APw zZ+RyR>;v`2TS@Z12X^tDLVhqP6HEk8Snzo*sl=aivj}@(NH)j=yFm@O%dqM<{?S)Z zIbt=E)EHnAx6%XP5_kq$Xim>C*Ey+5iqczbNiy6vM{0|FPzuh0M)pg7mb4{bz;ck| zwn1~i7WPnnH|AgM1ch#zvj$vX(HAkn(^*;hZdJsEG^fP`S1)Li%#+H%0x&J5yu^eo zU$TiqtRSn8zoC)++I|{Uz%n2+8G~{eTAcw6t3*a65*6#f%w!B&$)2Xqq^BY@NhX!m zoIHevvZYrsc5ToraGvAfoJ0(g*u3Jis@QEwDme&dBx2AG@YvWV=>qmj<sHwU(*fVs zQ_*b}#9=3c>O7w3cqu6dPD&&0WRR?$@5Wxq0$J;1keKtwVuNIX)HoT`mB#f?B9in{ z=VXv{?7uBGNG77KP6jnwo`WQMx}6NVWAQF#n18#R465cTvQ;vwZF4ecn~7aZu~m{@ znw<>Fx3Cu*EIH@D7_Xa`1HcAL4SEVH<Mm@DCU)Ls+ba5HVwWz8;u$38UuBHPh(!jy z2ZupcA~Wf!V1tQWD~VcS&>^tcBr;tzu~QAXYcp`F5l!>Zs%O;X4Rt<UPLtZxG~C3T zw<dNev>@4IAX!Np*{7si;;yGTVUz4t8HStWq*Cr~4FNYwR?R!+oPjg4P^u@b1GhU$ zSSQKN(uZtIq<8CNALF+$>}UHR@ArmvPAzvUNyt;&y6qt+uUo+>Hn>fLO!8fC#rKmd yJ91~J2#hv<z74BVsZ=VJN~Kb%R4SD!1^or;9O1d2CqDrI0000<MNUMnLSTXfX}N^} diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_close.png b/app/src/main/res/drawable-xxhdpi/ic_action_close.png deleted file mode 100644 index 76de2b1303156a1e8ad43ab2f38136a1a1bf77c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V9fP&aSW-r_4bZq-ys7Yhl_bH zA|Le0FuXe=Shd_wOp-J7Rfd@H4ZnNS_qcAG{KqL^uMSWz3@lLlz3+b1o#-{KMRC>V zw!b>Ne_O@Y8&NjVF<eh-Uhd4Z{r;Nc?EgOZXRa^K*2@&F+s+Ydef(K|zIvkXs=3cS z&#e!#diC#4T}OzORmO9jcT>d@{<=D9mXs`-E3|xDP2yCuQ}uFh1!Pmt<^G5@TT`Yb zVzZ~rNd9N$mdS<ReV5Pm>75Z~Eg534w@j&A^(~88M;Vi-qjlr%sbx&1F4m1!i{?7W zUi4egm!Tf#zD0PA@-5E=P`pU_)<y;q7e~26``Ru|luufx^x{P7of)?ZWo1{+-EU(X z@kcc>%zFMGpIe+Zp<5;wG-kH<y?Wzi<#nOsW$6>v-jiFL-8;GGxw*#$FP&>yniAqz z6WCVDBAycC+tkF9<F$>$Z5r#V6Bq1YAqAvAMDAm7T)W16Zr@TNkg%t#pUXO@geCy^ CK(D|6 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_delete.png b/app/src/main/res/drawable-xxhdpi/ic_action_delete.png deleted file mode 100755 index ef8e4bb424fe7f4ac8587f423d15167d4a64fbb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 849 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V3zQ7aSW-5dpmb`m#CwN>-8TV zCW3#M49$dmd3sC*m|6cHu>Wwh<<V>YKg^EIh8J!ea4|Fz+wWj-@rb%=+`;2_e7|2= zciH$&pPI#1|Lak;tKRz*q&LSbWij*Gz1&hT;E#Zd1B-~TM&5kRj)o+rqi>BF1p|B* z?5I^!VCv!&*?ykI<$yiQE$2cu3*NhR!T<Se*yl06eSS2?drrgVxewPTtXHv#<@wv^ zAF<wcF4H;5_p3cCnD)FVJ$(CYq0fQFnGCllUjBUJX4ly)p<5G8zGh|H`^Iirw{LsH z-UFr&9`ww<W1f7rY+B9CjX#tN*ne0hzpbBc!IE=M<U+RpxjAYN(pW46*Prieu=-|i z?e4&`M*TwEp-Oh23a?K>^BA<<FPKF1y*j#|S!VXVt26iZFNozmy>TZ;LBQMzmQDxe z*n9ijXxh7F?(c2cEe+AzcFPOb9KM;>(olVlm*>R3M{fifXXUYExLxbw*zoA;L%{`- zk=jZJy29Jt8`4tNb}(qi$;vU#nz&VoX-!3G#gJBb<0DfZ^V`=t<&UoJQ~tH<^D+0D z*t4MzHWUjVd-MH3Y;xW9{{b7{$>}qw&z)_Mwx2~#@^d?f1yc;$Hw*Km6aU!mu-}v1 zRPWwr(G<10`vB*2H=dj88`|6M*ekhn-r$d6>f334Me{%J2ZkSaH1ki-2xPj&Yr^@w z@iDhu->r?Gr7CQg>Y9y$4!qLLc;BM-P@^D&Q9kgt`&s@2x8pyoP5*a)ae(3h+pE@E o_Do%+u7uK~gJI*NZ}tpTuL}|nY}zsjm{k}&UHx3vIVCg!0Dm)ZegFUf diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_mark_unread.png b/app/src/main/res/drawable-xxhdpi/ic_action_mark_unread.png deleted file mode 100644 index 968b84969b6cfddd35c457f341a77d6883af16a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 620 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U^4b}aSW-r^>)r_ze526Z97G! z6xLM8#hz$#>7CB1SF-T@18Kn^PD2(Up~eY6nDz<trL1aekaev3yYESS=1a@ZTZ3<1 zo^YBGXdntW(*Cq4a=Ojr(pP~T8D4fLtbAD%7#KMm7?=ba7+4e(l$oR^m3Qr5=cs&Y z_e1rQHqEW`U*CDXb48$Fzoeky^__0<KjK|<f+Bvn=5X(Kb<uTAZ>c|W{ZZJHLt1Ud zj~tDB6CN(N`u1yKsl~JF7uG!qnZ*C&{;DT&M&1q&)2*&$9hmdU>U*BdJLalM!k&f+ z(&ooN`gb!HYrKl7F5Dv+&ATd9Ah35L$NmqY&JUxdTl?S6-LSk_SHk6v=RGBjKUwo- zogZ>riR*XvEB%^e?5X)+vzAtpkkdo;UF*FL%<!`MRwp%&wer}tCvt%;@fFu^WbHAr z>#3f&rB})7|CL#P!jd&>ZdV*p|Cjk<wcoW%v+`6Q+kP{=maDsV+1lh&cOJiJ40QR) zuk`28^xjta4RRlJ7iCx|=at1MtNq&Ewec!X#DtsoW}Q>@JFsuAJjmtI+jd%PxSEpY znxnDPL}Y7DhG}2jwF#;x|GG~3CK0dMawt8`w_?fL!-hSl^G%h-p7s~J#pGsN-|AcE zZ1u?af7p@ht>urK&SYyUrVCyAtjzKuP5=@SNO1yWF8Bb9Ax8!#4yH{S4e~i%pV()k d#43nW_L<ZA+K&)D?oW*%uBWS?%Q~loCIAY1{wV+e diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_placeholder.png b/app/src/main/res/drawable-xxhdpi/ic_action_placeholder.png deleted file mode 100644 index 55f33b9dfc75330a1b9a1704384ff16e51620755..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1391 zcma)6`8yPN9R4!n%!F}{bJe<M%dMedNRCZRSXW`Bt;rSEIT*)ODAYv8-B>)tj?6Yi z3{lS68dGxYRIZ_MPm~-_So;U;^X%t&-uLr-etDngec$K#<Y3*LV3I15005YavxCRM z#{CO%(F2|-PB;O8h=q%TomV1vrDFRU?3{dm-8U}RSCv+qU4eh680JqFKLW3<veUb` zZP#-g-Y_qn-P76VuzcOOG_>CFVQA9+@E0^8keNSL@@#@RH<28<ezuI6Ab3?i^eTW| zo?OYhRPRvzMgj|AXp#6vVmspZt)41eh}8;8F)gHI@c5XPs`&aII(_QJ<|$eHHPQTl zzWz?B4Z1=XZr=QzaGnb<`nM`4tg6`{{aF_%+jcuCvGMb;u^}~xLyuKAEfe~hQF$t; z>Y+`lMQQ?^A>%LbK#gSWyiGuJ5#6MHrTQ-NdcWRZ)Wh@snI&%>>x;B@r|NpoRB#OE zkzOLN^98)MH1o7a{>c3?z1>7d`<s3+T1L|sPgxfa=n8@XD;?b3v&I+uc3NNeSfyCo z8xh<?`On*XW<K_VMFX1}&gaNCo>AhgmXBMW)W)l3%=2bV$mSTGp2tN)2s5JVBz0vJ zGj;5W^yVfR34hJld5`;^s;_}Bc%$#HT_%N-9vVLCXFxD;S6xtjahe8e&0ch+XOK>& zish=vMH!opuPl#=DLrnSc9qQCPyfKi%F-Yy-Xn}aJ<HxL2$i78V`kj!8A0?4Bwv`= zAd=g{kZrS`b!7HSD7Y^q6?>-~e=K2b_^whQFCf386l}2PBfn2CFKxDSyz#^f9dpr< zq-KZ4m5C2Tt;uP-zM896kZ~H0s;D6rif|qe!H6YTXZ@{CGcjMOtzKJ*7<-SbjA_W# zl#7ElvjDF@7V8A!M1>NSZCyCsTO_Fk;*73DNSv?{xsE7HV%X~fLzro2AP>03*JS|q zB9z*P0UK6A_2x2NyWyqNQX7Qmj3LKU1u#$YotM#gR%Y&1eRl9sr6hzqv2c@<v~JQy zSybZ}gcXanHj&i`Ayw6CSKbYf)mxI_Z!KNHhz8qOvXc90XU}fFHn`!8(Qw#$)XWIs zhI51R^CLJ-zsp<7Sb;x3MH<Ub^mt^6y8Fa<N}@AoBSQx=ML|&%j;vj-4YNBfIIrXf zsM#SonWK$vPX#`xL7E~cu2wz~rHuet^6oM1B7h~o6aWXp7$6RWegM#PAOnQ|_i+iG zuCM^b)+RRR9SAoSM=jc$07b(Z#id-4-&GFdS}*EQXoG+npm{vA5*IE<ng;lgK5}|o zX-E7h^oEfo2AR@=oK;+w9^ic)AjTQI<krx~L-^Kn6XjRBbvg)R=8Bk>ldIoZ&6_ze zl0`I2iPe#I*q?$ADTRR<3;s`YOyxrN85La}q|_Xn64CIdyYGP$w9xWF7qAMz`D39j z7{o6?aX7}3kD;rvjkdfRf8Hzcc38U>_=d77!jB2M8KL1@LCdnEmL-<jHhJ;C{&;N) z#*TcLuMmM|d^sBpdE%2?2$?ak?M3CU-ca{7JLVdd1CN{ZF&n)HUFmUXw$*1iJ=gZW zN3CG`76nuv;s>-=;f&g>Z=<=Yv*hlYy6(7|kTBW*40a$AMA!gPgIQEwbC$*AWq`VM z{Bs!-eyKoAuC0-yv73Iuw*Qkb1nFhuTk#-G<ADH+f5bX?0(f2Fc%@z;uF9)(p~Mw5 zeQ))SDOcuJ_2z){`<i>ClHo(P-^6LU7?F$HX=1Z6`RKfk5kl8O!J)lsGy(n;V~J`j iv-su1boLxG{MqUxq+4w)#k(&U9E{6pH-{#BT;@OKQFf02 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_reply.png b/app/src/main/res/drawable-xxhdpi/ic_action_reply.png deleted file mode 100755 index 15a3089bef7e8cfb774de2f9521f5eee81acda66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1364 zcmcIk`8(8k0RDXEV6evhgxs{QCn`-uG>$PDW=wWU3XOCPo76aFP4S&_grXR9OqONI zlgT)CBqpC-#Y5XRWx^_qCLL6!B$D0M{s;TZ{_tMU`~LjqGZ|D}ZF6k^fNsdPpokS^ z{|kb~3O~D;?gapTbx4puix>M=^kgP2(711FlgE@WezWl^nw!vW>I8$96y3p{bT{az zx@q@(AScKv&mr3D$Fh?1bI!)c|7bXRe?NcAgVu#Nmq;GnW0y1dGYgHriJul)he!EC zzmIpShggCbx~!9HK~@z)#0%}!-k3IPpakZw@(~#o#U5@X$1hkR#Hwb@xiLQOKMcg$ zBBTo}|GCT2V?h>-_NJ~+_D&XK*QvUtDyP3c!w1Bw8;c*(v~1qvpE+tW^~hZx+)Zz$ zfU=L)byp`(_9HCJx-g%>3L-Rxx7t+TETwqtD9wjhWr)%_eTXU6G}MO(^`UU3`vwUM zSqXql8$AYcklH#+O{OW#fs|@#>F6)vcOp#oG)Klzf7n{zV7H{JHc?X+7qrxTM!n1; zvBS#d{f@zQ^?Q5udbO=O4IM133sFh*saT0|);#Vj#!t^?9NcC^R5dr_QA3U+wcK4W zEU5S`BY?5@MPIoFb8f)eh{~BZQBsso)~j}QSK#EOi-Epb_)4S_i~ITdPHpl`n+Js@ z;92nu7mo}er0;3q)%?Ij?ZhG21NkJJ)!YRgc749}yxgzmkf0CU+8fle?5hbG+kE0j zIs4Mx)vqsBKc3|xZxpUsSBtuV9;PWOze_x^8I(I@DuPQl`^)LK61d=N*`CsN&4lq) zFM#}fg(==3C*q+moFE0nxuFEx_D4}Fi<gT$b6%T9wgm_+#`)Iy?bo$k1xsw5@Xp%C zx(CHLbWOcIeIyC**J8)ih9&lse5F+45-AD{0in5SzvTM6Dh>yPGN{Gaa3K-V0iow` zV|qziS1<*~Bp3;c;1V~1Tua$r8-;PYP{eY{bQP)J!Ackl$Kh?5Ihi#xXIdbhetnOk z@EqOH;w=@m+!daS;)5?(O(YMK2lBAgobIyRngjQ*AikCd+8>>MvazZ|zYdH%=eC1q zhm}0#P2jkvP14&Kbv*4lo}J`9fJj`5B1W2F2+W6ZX44${=OwijH`sGi=a5v_iDXE_ z?6Mp-fE?Hl5f9c07op2Ri=|mxlD$m^UBK3|DWZDLeTTLRP8%r{GG*OUvD;cuTU%oe zetPjG`;3>4SF+t1>hZ!<F$f~WUrA5IoVh&HFfGzIC@4De)<@YZIvKnc2@}XMWaH@K zmWrI!pEeP8`|?xw-C2InNJ5)a6ozPT9X0Bt*YmD*TG91$KGjUmLVrCVr355`?FxH= zs~~>pT4T$dRH(-#e8jMgvSY#F6Su6R@ghm^t1mqjvJ$h8IChNpv=-W1zguAf8(e8K zYp-^eJZ@s?&@6|OE9I+Ep4(FS*cA3J)<(r3^8=qCqmZ}!N~nGrUM{|!lDU^>=J5K| zj0ri6dMItVe*-7oIXQ{BOjo{hnD?Rk_i*KrMmzYkpIgfcaFoVJQ@Us@?$XnYBDCM} z;RutPbG>Kq;ak}0tU3mq6OKrCkmfH-?#48c=}AO(3#%sACFWXg?V5Z2_mQyjgp%r= vV}2L+o1L{F7wp!M5+PY3bhG>){8uFM+ljamTKK+|w*(;+M$nA__NjjWBUxk# diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_send.png b/app/src/main/res/drawable-xxhdpi/ic_action_send.png deleted file mode 100644 index d36f994db6136de671ab12aa242217a6da1b86af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 814 zcmV+}1JV46P)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h0008@Nkl<Zc-rlp z%Wn-)9LKLJsie_Tqzg@GyC61JQVY)o5*3fMiAoS*L4zm~@mN@>4IyD;Ardwv;t?U< zuaqMCKV9F6-$^F!Z9DhgnK@(T^GQB?Js)?@%$YN%R0041000000000000000h)B6y zULj{O68-{QkYjSQY-U8#53o~Cnn8#ygh)3)ZzzECk=!bq7@_0?G|8`3A_P8>{c;W? zm28ANQv?Wy*e>TXTFC@BF+%|7bGeH-BK`oIA_j22kh^6IqZdzr&Kd(aU&;YOG#+5K zd{t)z7h*3V8jEnffdI}oa-W>Xh}It9Pz(W_ujPK(%E(3!uqMU;&bRU)zldl7+U1V~ zA_RVrN924)din^D6ABOxahMQO2e_PY0Ou!pln|8x22u*({9+*%F#3N3tV}t8Ggu5f zE<1`Ipjl3Mir{_`C*`6dN4V!Pfb*Lik&BBM;7kSrILGbZ(^;ed+cFd&{EIjzmlPpD zR|W$(Cu|eJ(!5W^?=l==(w^8a<~D@>uv-}q5dK935071W3$Q_6lGp5g@tCdo2lwmW z<?oDz_+=rM<$Zn#Ufi?>lc8KLSIc#_b7Dvyv&nK)-j?^}GrOdXrx4<&ydryetgf6_ zT4Y;r-Il9lpY1&wv<12m`(J!R-n9(?jf5B_L}h^XP=K{|%R6XG)u%1M%@_mp=J^uT zZI__+a)-T&9JWi)Rl5W|uuIT;yO&R6F8Kd1LHYwW{xb<bw3qf-pM<-)4Nd=fAYZqk zJczLrQQRkB7UNisuC^R~wrI=IypZuc0sDR>d8)9lBzY<CKY*`oDGOP!DW$qG>oXg( zss~7GYhoLtT{-wkZqEJ4=G>VBusz*#fH*(U2)>j#zy}(&1>h4UkAok_M|wkiq*s3c zK2!A^pq>x4_b~AB1>mRx&jF$w*3r*l9mx#-bjQ^^<+z&U1N3lUlivUwS>`#wc@FLK s8-Qag0RR9100000000000002QA0iFHV}!On_5c6?07*qoM6N<$f^KM8v;Y7A diff --git a/app/src/main/res/drawable-xxhdpi/ic_fab_compose_message.png b/app/src/main/res/drawable-xxhdpi/ic_fab_compose_message.png deleted file mode 100644 index 40ae82aa53495add93496912ac01be443b2bd801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1821 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 LNkvXXu0mjfsW5L{ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png b/app/src/main/res/drawable-xxhdpi/ic_notification_full_node.png deleted file mode 100644 index 006b25ee9e3d2b102bd82dca7f94207fae85f736..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmV;s0!00ZP)<h;3K|Lk000e1NJLTq001xm002t}1^@s6Hs=I|0007lNkl<Zc-rlm zOKTHR9L2}?M%-7l-$f8ak%EEhcTi~;LfuIa@|wKgk0N#xh3vXezkplU(pYzD+m%{^ zprN_`{zIZkHI~L?G8g^_4!N1k+<VTw=RT%&9EC!mP$(1%g+ifFC=?3CJke^kPE{(E z>(y#CQ>)cp!w;0d@!5gz``29VxmqX`&ILwzxm-Svut#u@vdQQ3CY#NgbUJNPsgy@4 z%Qg8;?&01~m|Mfhg}I7HNC+W&2pJK46y;udhUa%1jmFKn#D&HS#bVJDluRZkqC6{g zNL}G@_@r;*>GkKiTyA3FhbeV2H#VehKVp-WJWosfunDPqrsC=O+tcj%znJLJ#%$(< ztQh3=<E+FFNISc-vQ0&Fy<T4^l}ZoE=O)~d23*&jDWwezJos<n2yRl*4~a}BlQDjX zACUcG2MuQ?&N;pQSw#CKc+4nVnPgYJPX>E__Vze3-(trVA7bcw^A<iJOZ9is@CIE^ zSzd4abB{pyB0c{lkw};zQIa50?w_IBjLNlfOXL%A9W(=n&B*x)Bae+SCq{ao!&NuP z;)f}x0HT%nWq=iD#TM~HCVDLMOP&GN`B%0X5E7?tPOzsPvek~q<Hk1jm)biHg^rB| zgrukGFdX}~8sLSf?NHdV)qtc&=D-VE4M=)q4%~~yV#ZQ#Ic#E=DRgZ#z$i4my?`(4 zHX0y3%bo$Ubdd{RZ4w`iMz=>^dbqH}4+j>Rxb?@WG!ludV&njtAQSQK*mnaN3Ai6* z@kjmvjFDJ?>paj6kob2nbgb7*Cif*A--R!E_$}=CD7e#a^BL_gbMic^|HM!z6bgkx hp-?Ck3Wee~ege<<9~^<3M?(Mr002ovPDHLkV1jtXN2ve+ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png b/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png deleted file mode 100644 index 2b218dfb6a0474b591659aa72fc3e29f10b88f37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 660 zcmV;F0&D$=P)<h;3K|Lk000e1NJLTq002k;002k`1^@s6RqeA!00078Nkl<Zc-rlo zKTASU7>6y1a1?_a+FZh+u_2oz$fY5$QTzZwMJ_>&(i&7tOA*e|CkTSRfJ95wRt*W! zP!<h!dS5t!NcVcZM?LrT{2usi9`HP!fA2XNiva)t00000B8F0Gj51AGpe$3CC^<@+ zGUBR~3CbGfg3?spJWx(31xnlnF_V-7%7^NnTS{ptMe$UjlqoODoGVIZ*w&aM)mvUE zi^C%4O!bE^Ueu*et0UE4K6%HJDyZz!v4$R5HA<cGV<72~l9ISv{kI^qh$<z~_W8W- zXjqP=3EKDQNY$rJmhv7_&^Kj9(joA}V_GFTwB0ipMKvjc);v0J7NBjxKoFnElM)17 zg#8QCXXXuewG|2H9<SP}9-#fIp`e4P3!3)%^1OrPecFywZVUxoMqSWCfHO<hL(XXr z4vzOx7qlFp6YqMrD(E$5wFe{TkLRchT2gj-in^dV%Yv?>E+}nT(7A*P{=uT4Z3zd( z6N`doB?u~5l5<BHl^`gtFW3i7MbdRg$%3FQNrK|~^<j{3DYGd_QbxZ%3|f+?skW$J zA25<S)MuZN<S!{_cq#9Mq!hnC=nHnNNj{=x^m|yd%R^v=xAG8gXtG99i>M^uAyW?d zCXg??_&$fPEVwG?!jrPY<wNU#bZ}>#kz^pZ0!f`Eg&j#1NWw^hB&<nTlk7;^&_{&U zG$6@e+AgM>xgd8Xbs@-2NnHwZa^dFJ-u1S;1(1|?bxj)4MR6yJ5ClOG1VI+d5_bg! uEO6Zxq$TlBAc`KDiU9xs00000`p7RR(^KE_>(m_p0000<MNUMnLSTX*3m>@v diff --git a/app/src/main/res/drawable-xxhdpi/ic_plus_sign.png b/app/src/main/res/drawable-xxhdpi/ic_plus_sign.png deleted file mode 100755 index 24e567d7429c6eacdeafdbdc6b19bbeb031c599d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VA|*D;uum9_jdNitk6IK*Y6=x z6Q_s?<!-qU^vUy})!c-K;vKDP?@zuLv_U{o;uEu{$=QkBnTKZ@pV?!Wa<}UIyzkel zmhYJU+b3o%%bHT_)2$89<QbU+F8C~{v{h$dJj*icdNBs~fwIA|3XPXLmmHtmX+NoM z#?7hgcbb=``yG&Y+`8oZq<cOS=6Y47r`@l<_-mW;f#WN6S{sbE>`P+#VKH^7KtS8I z@E!*F<(ov9PF?ubCHUZR$TTMh$*6ckrkXjYf;cpsUu(NH^an?BF{WnJ9_9E@sO7D= zfM@M`2}V1=R85vC3%`m|YUpXEyn8`)uU7l}+D`rwcIma}>zzydC)F*>`#yE<?3+vV zf9Z<~ZeaYeVUvpHZ--05p8aaI*C(A*$z8wl{jXmcY4PFH+*oA1er?+HL@u@e?3<LQ zzm*bNui11rST4TA!sPYw+-X6B^pIH&4x0BWEt#J6cnNc89JSWJ-^q}7Bu{+fWa*8z zp0|@5W8>K;^{MQgrE^ctwI;7h<m2fxdSM52?su%6lAiW+ahcj#2XQ8*3r6k&6FiMC zv^FqoIU~rDa*3xzQGr3Xn1yks33Dt5hr?+ZM+Z79?KRx?pMC$7E%uWCbzk0o6@99) ye~M92`n^8CpWFT?ax%?Ys(>r;ASbQ=th?T9D42X`hc7TeGkCiCxvX<aXaWGJQakVf diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_delete.png b/app/src/main/res/drawable-xxxhdpi/ic_action_delete.png deleted file mode 100755 index b66482880bc46ca477f73b47fa90e2b4843e5ed9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1072 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVE*pu;uum9_jayrwrHS;YxtG` zX~C8o9lcAB?VoV&*e$(fLI;lpd2bZ9wq{*D<J_%ddi4i0G7}FRYEXz;(JIfg=!k<4 z+o!a<<>%{i@6FwN{$BPup7kf0eopaASor0O<UED|nI;AShA#Dhnm;)W3;{Ncj~=Hp zvot*76p^==R$vhEU-09{O$NpV6--BupJ(G>I4Z1RS7+wH@It9E_yAWy-lEKH4W$nr zyHpuZewoPZ6Z>zz(XNJsdyE!*G4ns1yI-r$Ct$<4@7x=)xgT4fOExV0v--fL9^+f@ zKg~X{xw17Oj&Z}ae{lw9-abz7VrUcJ^7L=sa^`=3c{WrWc<=XhYwm-;ik-O(%pu1A zeK-%eCX_xC{;IfvZO^QvDGUa#ryiR!9r$r(mz5yHu2sc<3>Xt?(qiXwFvMQ9tee4L zV7GalA4|i!S2FfK3=;Dr!_}D_!b|()mw7WBJ3c|!*gxS^JX5-z56dxi0fuAg*RFOl zTnL@}xSf;XvGMCyF5C<;ueC0IJZvLd|Kx+;7KQ`Oho#f?)|cEak!3jZr9PXTVL$U# zM}~mM-;Xjk+;4oPz_8-u??;RW-aC{EFkJaj`;p;6`2s5rhO9sPDi}Uk2gtH8nAXMH zF#M2R!N=4fwO?P3;SXO3JEKGIeQ|z<I`&n~3=59E?;erHK4sj&FB7Q!;YdAGxlLup z!~4hYGyBa@?6?)*uzOLRFvA_DbF+ZF_YLhJUJT>3vjQ$}c^~Xtai4|ZdBf}jfyNw; zzpX!TRA^`XpPKVfHo;&sQ;bowz?OKX*G=#2C#_}Jz;TDc?^dJFlK+wwJURYLwlDY5 zX5e9KcR!fL$YIBrePCMbzXkV&7(^8^R{arUl(6CLZ$5pXwJ(mRU3}m51KncR{+&B} z`4-!am$?-xI*iX7FCW-w`RnKh_7A*281^ulMMwVJEpM&MeBqGtrfPo11;4xg+b1v} uvWvz%hmRjOGcq}RWa;Xk&r2xB?PsVIX>YU%Fs}xd1Pq?8elF{r5}E*~Z^_^Q diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_reply.png b/app/src/main/res/drawable-xxxhdpi/ic_action_reply.png deleted file mode 100755 index 2f08cc88d29c1b6abd86c457052a15378c8b801f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1929 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 R>XE!y;7n&YG~2V%{sTO)I`#kn diff --git a/app/src/main/res/drawable-xxxhdpi/ic_plus_sign.png b/app/src/main/res/drawable-xxxhdpi/ic_plus_sign.png deleted file mode 100755 index 491d801ec719d0ad6a9327922992ba7e8ff0275f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1043 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV7}$);uum9_jayseu$$;+k8V- z#$~)M>k~XCa2T+(x`s6$_#FL9`Uk5;5PRE_gAE5A3^+Wr7BpxpwB>BtRDOB$c5CDB z=QjW1d48rk?p*D%;_bdaUPd)u`TFjzjnsl-;S2`e3p<(S=HKUBz-Se|;PYE+MiYnU zTqXNz)f?De#5A7U{+{Im!(8nPKfldo0BT^hy1!4@f%!~y<DAc88+c>5-+d9V@Y0lJ zzwWD5^G#Uc`5xvw>}Ab!Z?f|Rd2uVB`F6RPaUD~CL;68IJNCF`Q}(d{6~-|0Z1-mN zIdzKp=(9O<8MLKdvJ^0#dHDb52e~;6tAD=+@+=tHL<E?gC-BNM+A-O%R&ae_cXXB$ zU}`;JY4g&SZQX5G^#<XD*!MokbC)ytGtPsk5qa>NLA}a>LxgELWAW=SO%|?3rv(BU z5K7_+$9;AVu4^efU?O0N1c9KI0}B#-+qjX`YOt^=x^nPfSA3acTCp8_49mIsYWtkd zs6J5qpfck(>j$nMVlm-bfAl|CFa474bLe6|>+?D58Oj@^leum_xYcTzuU{XYpmMhQ zf$|6AO)C=K2!1Gg^WM;goB0m&JSKjwyn7)ZIB$4s?v++<W0P1McXzAQf_49{Zq9Ca zrMn`!fo;X=`mb-K7F2P+5?jFd>Q%h$ZnhQons%{fH0=5netsR}s_hH%7?HI&7_;r* z(|O07ckODj!Ee?FVg=?A_t_tOesoNC$GVUo<qv$lGOy%5jl9FUrB1e{vQqYs#f_Oq zOFv{k$S#Oo`Jw*6`d8NvE-l-gCl}8ZyY|it&JB6VUTZHiNFCVT6kc(F(?F;1J<Bwy z%d^isRzBd~(9a;xSZA1Wg4w8nXTwSLg1`0AY!et$7$hBl)Ssdko#5E}wP4#xt`-Ny z0AL)oLa6374|5X&HmDtj@?pXglvvnUvyEa9DuFTv9GXs~D=t1~ecE1PtF%smyoJPX z_nA$W-R<8$T-~6XARY0?>GX-`0zW1OS@r46ihA>g*I;Ygly%|EQ^NLatdUx<v2BW; eJ1sc>8LwXTnQwAoV;L~BGkCiCxvX<aXaWGuJg27s diff --git a/app/src/main/res/drawable/ic_action_archive.xml b/app/src/main/res/drawable/ic_action_archive.xml new file mode 100644 index 0000000..899700e --- /dev/null +++ b/app/src/main/res/drawable/ic_action_archive.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M3,3H21V7H3V3M4,8H20V21H4V8M9.5,11A0.5,0.5 0,0 0,9 11.5V13H15V11.5A0.5,0.5 0,0 0,14.5 11H9.5Z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_close.xml b/app/src/main/res/drawable/ic_action_close.xml new file mode 100644 index 0000000..a9f3069 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_close.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_compose_message.xml b/app/src/main/res/drawable/ic_action_compose_message.xml new file mode 100644 index 0000000..fc450f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_compose_message.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_mark_unread.xml b/app/src/main/res/drawable/ic_action_mark_unread.xml new file mode 100644 index 0000000..d07b8cd --- /dev/null +++ b/app/src/main/res/drawable/ic_action_mark_unread.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M20,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2zm0,4l-8,5 -8,-5V6l8,5 8,-5v2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_reply.xml b/app/src/main/res/drawable/ic_action_reply.xml new file mode 100644 index 0000000..1cfa52c --- /dev/null +++ b/app/src/main/res/drawable/ic_action_reply.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M10,9V5L3,12L10,19V14.9C15,14.9 18.5,16.5 21,20C20,15 17,10 10,9Z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_send.xml b/app/src/main/res/drawable/ic_action_send.xml new file mode 100644 index 0000000..121d6e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_send.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M2,21L23,12L2,3V10L17,12L2,14V21Z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notification_full_node.xml b/app/src/main/res/drawable/ic_notification_full_node.xml new file mode 100644 index 0000000..4790d6e --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_full_node.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0,0 0,6 20H19A5,5 0,0 0,24 15C24,12.36 21.95,10.22 19.35,10.03Z"/> +</vector> diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index cd73e5b..ba540ad 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -22,7 +22,7 @@ android:id="@+id/fab_compose_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_fab_compose_message" + android:src="@drawable/ic_action_compose_message" app:elevation="8dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" diff --git a/app/src/main/res/layout/fragment_subscribtions.xml b/app/src/main/res/layout/fragment_subscribtions.xml index cd73e5b..ba540ad 100644 --- a/app/src/main/res/layout/fragment_subscribtions.xml +++ b/app/src/main/res/layout/fragment_subscribtions.xml @@ -22,7 +22,7 @@ android:id="@+id/fab_compose_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_fab_compose_message" + android:src="@drawable/ic_action_compose_message" app:elevation="8dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" diff --git a/app/src/main/res/layout/toolbar_layout.xml b/app/src/main/res/layout/toolbar_layout.xml index 049a0b0..ade8b53 100644 --- a/app/src/main/res/layout/toolbar_layout.xml +++ b/app/src/main/res/layout/toolbar_layout.xml @@ -1,34 +1,33 @@ <?xml version="1.0" encoding="utf-8"?> -<android.support.design.widget.CoordinatorLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> +<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_below="@id/toolbar" - android:id="@+id/content" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".MessageListActivity" - tools:layout="@android:layout/list_content" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@id/toolbar" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:context=".MessageListActivity" + tools:layout="@android:layout/list_content" /> <android.support.design.widget.AppBarLayout - android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_width="match_parent" + android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar - android:id="@+id/toolbar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" - android:elevation="4dp" - android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - app:layout_scrollFlags="scroll|enterAlways"/> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + android:elevation="4dp" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.AppBarLayout> diff --git a/app/src/main/res/menu/message.xml b/app/src/main/res/menu/message.xml index aee2121..dc31fc3 100644 --- a/app/src/main/res/menu/message.xml +++ b/app/src/main/res/menu/message.xml @@ -5,21 +5,21 @@ <item android:id="@+id/reply" app:showAsAction="ifRoom" - android:icon="@drawable/ic_action_placeholder" + android:icon="@drawable/ic_action_reply" android:title="@string/reply"/> <item android:id="@+id/delete" app:showAsAction="ifRoom" - android:icon="@drawable/ic_action_placeholder" + android:icon="@drawable/ic_action_delete" android:title="@string/delete"/> <item android:id="@+id/mark_unread" app:showAsAction="ifRoom" - android:icon="@drawable/ic_action_placeholder" + android:icon="@drawable/ic_action_mark_unread" android:title="@string/mark_unread"/> <item android:id="@+id/archive" app:showAsAction="ifRoom" - android:icon="@drawable/ic_action_placeholder" + android:icon="@drawable/ic_action_archive" android:title="@string/archive"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/menu/message_list.xml b/app/src/main/res/menu/message_list.xml index 75f182b..295a7e1 100644 --- a/app/src/main/res/menu/message_list.xml +++ b/app/src/main/res/menu/message_list.xml @@ -5,7 +5,7 @@ <item android:id="@+id/empty_trash" app:showAsAction="never" - android:icon="@drawable/ic_action_placeholder" + android:icon="@drawable/ic_action_delete" android:title="@string/empty_trash" android:visible="false"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8cbeca3..eb1f6e8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,6 +3,7 @@ <resources> <color name="primary">#FFC107</color> <color name="primary_dark">#FFA000</color> + <color name="primary_dark_text">#DEFFFFFF</color> <color name="primary_light">#FFECB3</color> <color name="accent">#607D8B</color> <color name="primary_text">#212121</color> diff --git a/build.gradle b/build.gradle index 37a428b..8eb7947 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:1.4.+' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 32a36e57f69aea1a3e889ecf29d463aa11285617 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 18 Oct 2015 18:20:58 +0200 Subject: [PATCH 007/110] Changed to using vector drawables for action icons --- app/src/main/res/drawable/ic_action_delete.xml | 9 +++++++++ .../main/res/drawable/ic_notification_new_message.xml | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/src/main/res/drawable/ic_action_delete.xml create mode 100644 app/src/main/res/drawable/ic_notification_new_message.xml diff --git a/app/src/main/res/drawable/ic_action_delete.xml b/app/src/main/res/drawable/ic_action_delete.xml new file mode 100644 index 0000000..d555d63 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_delete.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notification_new_message.xml b/app/src/main/res/drawable/ic_notification_new_message.xml new file mode 100644 index 0000000..395fdb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_new_message.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M20.5,0A2.5,2.5 0,0 1,23 2.5V3A1,1 0,0 1,24 4V8A1,1 0,0 1,23 9H18A1,1 0,0 1,17 8V4A1,1 0,0 1,18 3V2.5A2.5,2.5 0,0 1,20.5 0M12,11L4,6V8L12,13L16.18,10.39C16.69,10.77 17.32,11 18,11H22V18A2,2 0,0 1,20 20H4A2,2 0,0 1,2 18V6A2,2 0,0 1,4 4H15V8C15,8.36 15.06,8.7 15.18,9L12,11M20.5,1A1.5,1.5 0,0 0,19 2.5V3H22V2.5A1.5,1.5 0,0 0,20.5 1Z"/> +</vector> From ed5fb69eafef6fadac59b9bf431869c8d1d661f2 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 18 Oct 2015 21:47:07 +0200 Subject: [PATCH 008/110] Bugfixes --- .../dissem/apps/abit/MessageListActivity.java | 41 +++++++++---------- .../dissem/apps/abit/MessageListFragment.java | 24 ++++++----- .../abit/repository/AndroidInventory.java | 2 +- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 1da5bf0..70891ea 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -12,15 +12,6 @@ import android.view.View; import android.widget.AdapterView; import android.widget.CompoundButton; -import ch.dissem.apps.abit.listener.ActionBarListener; -import ch.dissem.apps.abit.listener.ListSelectionListener; -import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.Label; - import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; @@ -42,11 +33,10 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; -import java.util.Timer; -import java.util.TimerTask; -import ch.dissem.apps.abit.listeners.ActionBarListener; -import ch.dissem.apps.abit.listeners.ListSelectionListener; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.listener.ListSelectionListener; +import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; import ch.dissem.bitmessage.BitmessageContext; @@ -277,14 +267,7 @@ public class MessageListActivity extends AppCompatActivity public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem item) { if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); - if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment)) { - MessageListFragment listFragment = new MessageListFragment(getApplicationContext()); - changeList(listFragment); - listFragment.updateList(selectedLabel); - } else { - ((MessageListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(selectedLabel); - } + showSelectedLabel(); return false; } else if (item instanceof Nameable<?>) { Nameable<?> ni = (Nameable<?>) item; @@ -300,6 +283,10 @@ public class MessageListActivity extends AppCompatActivity case R.string.settings: startActivity(new Intent(MessageListActivity.this, SettingsActivity.class)); break; + case R.string.archive: + selectedLabel = null; + showSelectedLabel(); + break; case R.string.full_node: return true; } @@ -311,6 +298,17 @@ public class MessageListActivity extends AppCompatActivity .build(); } + private void showSelectedLabel() { + if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { + ((MessageListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(selectedLabel); + } else { + MessageListFragment listFragment = new MessageListFragment(getApplicationContext()); + changeList(listFragment); + listFragment.updateList(selectedLabel); + } + } + /** * Callback method from {@link ListSelectionListener} * indicating that the item with the given ID was selected. @@ -354,6 +352,7 @@ public class MessageListActivity extends AppCompatActivity @Override public void updateTitle(CharSequence title) { + //noinspection ConstantConditions getSupportActionBar().setTitle(title); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index c31b091..5de7c7e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,25 +1,24 @@ package ch.dissem.apps.abit; -import android.app.Activity; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.ListFragment; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; + import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; - -import ch.dissem.apps.abit.listeners.ActionBarListener; -import ch.dissem.apps.abit.listeners.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; @@ -45,6 +44,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { public MessageListFragment() { } + @SuppressLint("ValidFragment") public MessageListFragment(Context ctx) { bmc = Singleton.getBitmessageContext(ctx); } @@ -98,7 +98,11 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { } }); if (getActivity() instanceof ActionBarListener) { - ((ActionBarListener) getActivity()).updateTitle(label.toString()); + if (label != null) { + ((ActionBarListener) getActivity()).updateTitle(label.toString()); + } else { + ((ActionBarListener) getActivity()).updateTitle(getString(R.string.archive)); + } } if (emptyTrashMenuItem != null) { emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index 32b9ed8..5e5f1c5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -187,7 +187,7 @@ public class AndroidInventory implements Inventory { "hash = X'" + object.getInventoryVector() + "'", null, null, null, null ); - return c.getColumnCount() > 0; + return c.getCount() > 0; } @Override From e149efcfff135fb9ce5ce9087361f73b7ad89417 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 20 Oct 2015 21:17:52 +0200 Subject: [PATCH 009/110] Changed string resource --- .../ch/dissem/apps/abit/notification/NetworkNotification.java | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index b51cc47..99f78d6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -27,7 +27,7 @@ public class NetworkNotification extends AbstractNotification { bmc = Singleton.getBitmessageContext(ctx); builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_full_node) - .setContentTitle(ctx.getString(R.string.bitmessage_active)) + .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 28b28b6..383adde 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -4,7 +4,7 @@ <string name="settings">Einstellungen</string> <string name="wifi_only">Nur WLAN</string> <string name="wifi_only_summary">Nicht mit Mobilfunknetz verbinden</string> - <string name="bitmessage_active">Bitmessage ist aktiv</string> + <string name="bitmessage_full_node">Bitmessage Netzknoten</string> <string name="subject">Betreff</string> <string name="to">An</string> <string name="title_message_detail">Nachricht</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 727b3eb..b493dc2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,7 @@ <string name="app_name">Abit</string> <string name="title_message_detail">Message Detail</string> <string name="title_subscription_detail">Subscription Detail</string> - <string name="bitmessage_active">Bitmessage active</string> + <string name="bitmessage_full_node">Bitmessage Node</string> <string name="wifi_mode">Wi-Fi Connection Mode</string> <string name="settings">Settings</string> <string name="wifi_only">Wi-Fi only</string> From f19996f79c06baafa6d9b3ed1a001f8aa3d4c122 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 21 Oct 2015 16:15:57 +0200 Subject: [PATCH 010/110] Removed unused icons --- .../ic_action_notification_sync.png | Bin 967 -> 0 bytes .../ic_action_notification_sync_disabled.png | Bin 1059 -> 0 bytes .../ic_action_notification_sync.png | Bin 580 -> 0 bytes .../ic_action_notification_sync_disabled.png | Bin 641 -> 0 bytes .../ic_action_notification_sync.png | Bin 1296 -> 0 bytes .../ic_action_notification_sync_disabled.png | Bin 1388 -> 0 bytes .../ic_action_notification_sync.png | Bin 1943 -> 0 bytes .../ic_action_notification_sync_disabled.png | Bin 2048 -> 0 bytes .../ic_action_notification_sync.png | Bin 2779 -> 0 bytes .../ic_action_notification_sync_disabled.png | Bin 2864 -> 0 bytes 10 files changed, 0 insertions(+), 0 deletions(-) delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_notification_sync.png delete mode 100755 app/src/main/res/drawable-hdpi/ic_action_notification_sync_disabled.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_notification_sync.png delete mode 100755 app/src/main/res/drawable-mdpi/ic_action_notification_sync_disabled.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_notification_sync.png delete mode 100755 app/src/main/res/drawable-xhdpi/ic_action_notification_sync_disabled.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_notification_sync.png delete mode 100755 app/src/main/res/drawable-xxhdpi/ic_action_notification_sync_disabled.png delete mode 100755 app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync.png delete mode 100755 app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync_disabled.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-hdpi/ic_action_notification_sync.png deleted file mode 100755 index d938029163421b0b20417517e44df7ed0ef7cff3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 967 zcmV;&133JNP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm000AyNkl<ZXhZFp zPw1s%6vsc`8h`RH$)FgKSSTs8m}X;?hFHwV!pKI#&O%7@FKRGM!(?GeWHHU`W^6E0 z*kFMu5?M?ke<q2cA$9bf&aJ20eSi0Tp5J?a)2;i|?RM|`JkL3w=X=h1&wa0$^PxHS zp9R3FR&b%fLV;zTz)V9ZBCeA3-7<Y#a{0^v>^$&{q`T)1Af4X<TrX+uTmg(u9=O_$ z&lNy<^2Mp&1{|04_bikcEC<F<zHs&@;1A#^@CERJr0<7<84|!Te`gQJeg)nI4oUj9 z$NZJ4LJ@H-up3y@)2U;?i;~{#Ie+4`!4&ZB5pj`ACvZbyZ5?niV8EO5b!J}w4>%}k zUn|6*010s=uzdtkR-uSk1-Pu<3mDA95Kl;Yy9J^NAR;cbVM)JFGFmDzsf<R%1;9>V zL#}r?J=BsDtpJY!mjgQ_{n-I=QVB^9c3d~a(}fZTB<-zLp&B3}E(Kl&9PfvLJ(B+E zQbN)%T~0-W!^rXPoN#q|pti)e0NevSnoc=x9lws_>XA9I3%E5GzaVLU%|<N%&jPok zk!_NW)EFF}8xb3U=kog_lD5_CR|OCeX9KT{`W#?$t>hB`F3V8#rEh@yYub)lfJ=Z^ zGyZLGy`+C@PHY0eX~0MMz1xoamdcy)^|b)LAsN8$9_w1;f8<C+oDO`LpMOpOzxNuo z0B2<gU6Z?SlJr9_OC6IUq6{JTMh>CB`tDf;;7udK3FpM~*edCrzQ!iaiwGA47Y2{n zCUF9kW%MoJ$<{E+GWt|e)mEg8T5{jWRs$!1M<jjRrGi7Gt5QY88CDV41UT+}Bm1#N z(drON6&?q+q#--MQ_^o8A@aK0Y)M~tTt7G4@S~ERuW{aUTLJug_a(qhoyYs`YbHTN z#3#U)*%wXxB^D9q1FIytsjC`I0I9<5j(;02lz0vJw9JVF@DxLIDpBQiF}WEaA>0$) zRX{i=-pkil8P{cgEU82b#IgVq!cQF@F2ZS=A5K=c;h#;L*ysAP0TRMFaR+d7R;OvU z-vD+=ifI$4T^|HsWF+sZHv-oIR{-Y$?w`uxyR8)%<eBN90Hv1^;kM%(pnU4^c|I>4 z#QEkjI!)Fw5SLMpK~H{UV`d5zVP$~9Pkto<4h$Ld<La-30yF)SW99%g`O-3X0nl=4 pL#|mUFyv{r?6pv!<<y2;^FQ(<H9&Q%-n;++002ovPDHLkV1hf%z1#o* diff --git a/app/src/main/res/drawable-hdpi/ic_action_notification_sync_disabled.png b/app/src/main/res/drawable-hdpi/ic_action_notification_sync_disabled.png deleted file mode 100755 index c8bcece5bc975b31264cb7f77d415c01cd302bbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1059 zcmV+;1l;?HP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm000B(Nkl<ZXhZFo zNyr^l5QV>k8k}$;8V3|XP*Biq5JWKIfD2tHiXy={p%_qN9O6RMg#(yuL<9#E6$K%S zECh`hag_wYl?dX(8I229Do#*xLiMB5fA`;gU*|mu^o8*F)3<NcsZ(`sRg1%X=`i=7 z3Bdo&;Kabhz{m(pDKs*GqhFsG7#+fq2P}<&h_J0wfumOO`EOvqq!@~;2tY)f51a?= zl=N@YNJN|lTmXC`>96|ph&Ud25jeBMIq!cN*s%}*>HMR>*=gv_3PePl2y6r{1O5ay zNcyb>z#G6>EdVO#XJ;OA1|s5WU@LGeV1qA7dfkRHx{oD*Z|4B;Jn&W<t9?L9Q^*mv zqfQ5}nc;{y2DleE64)-u`8-<*vW<aHCEeZUXxH;n0N%eI*qjA<5cLsZ=jo^qgKh)x zkC~P9@jM`k08|Dd;s)U1{QE#U^`$_A0l)KO){~F<8Q3K0>rxQ&0@woFR5R9Fgf#$` zQmf5-z~jKWJm(!r&n+B4M4SP<0-Ow(eh0y+vnmq_!0)|Zu@w<^emih<vT(a3voUxT zGY}Cs0}rHyUBKsni?C8BM|XuBMueYV4D7DSuYo@aT$$%RE9u?A-e>em_FFQa2Cm4L zEUO;^*8%s{CAP07YV~wE@JtmDNj`J%Dq<iajsso=&PfZ`O8Oz&1Q+(EJ|>!;WlHYM z|Nj!WL((5Z5tIUO0lo&TO#>HIdY>RxFi~B0T@FQrd!xPi-1)yz(l3hwa7qI3uh+V! z=0`;5W!L6ZM63b!<g@(=z`ui5DFZG<r_i~1`ZbcQ=d+bebY6BnDfAU!CHj4k_lKZ} zACcrDUa=ZXbV_VQSP(1>Ub_}r#I_I-Hv$i(!4KRU)nw7>a24<o;6gMLzN}g$Hd~Ln zjBb<k_R#2iUr`DrfYURLT*z*nH%r==xqKIJ32<B0PDjM`T@srB?$2A@g=+;mC?)&A z6az>Q^;TkEUPe}+4cX(&=7Bzm%`WzdoW!>EIoy0cF8~iUFVy7pd%)9}Z`Kv_K$pbU zS_*fnYb5=;6adM>rCEH<8MkYoaj<U5%dYC3THB5i5OXn58Hfm9Y8N&Pt~QZ9m_>eI zZfH(+C5~5n=K@eGSkJ-D)dj#Q<`MLymUosjQfBH{D(}pKts+_XsSkj^X6y#O18!}b z>`Is@VjyMDdF+2}&fB&N`kt1r5+`Ik{aMxXw_J9<2X-tHz!4cS|8^}$_@^P?c4}>h zFFP~$l%y{f2B75NlD+xK=9^OIZKEiKN}O8O-U-06BHd+YVqjumWCW%Z8X3URuTKn& d4&lfH{sWr?uRudbvR?oI002ovPDHLkV1fpW-(~;+ diff --git a/app/src/main/res/drawable-mdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-mdpi/ic_action_notification_sync.png deleted file mode 100755 index f051d5ec3d3de66def49e39d2461e33039baa43b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xZ*P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80006DNkl<ZSV!%a zJ!=+06osE7f=v`Z3L7C+RP3xoN#{RM15wDAg%&Dku+k<?>@<Z6`2bCbm_QQ$K}r>D ztV9zlAyptjqA4Qil^f=T@g;e8c2*Js?^Mg~?A~Y2oO^b}E)VT;eVPD!TLCk>Ch2t* zA7v}x#eqAL&hI#YnLPw9g~AtsHA#PRKuRlMW><mxQL$BE8TbUuNm?iak^|7|(r;7b zColtyOY-Z&Q5wL%XlB=eo4|g+J<tlY13kb&;IluMv|dDz1Yl+@@rulBAK*%Ok#n^o zW_A`B08RieB|WPQU}h%)H(kG^m8jSzgrpa7kpM9Sn<;yv$P<MMnArp1GB70RRndyV z7WDzQgDD?1dZ!^kka`k04RlKSnh4MaOb0J?H)_!$fWyEna0Iv@X(JKf0Pqg*7VVJq zeY^Kk0KBb^y4xlF+GzkcTFw@@ai@SUKv(t_1VhzK_*Bw(q5@sO-O45m0H=U4z_b3I zq>puNcMBMn^gC+NW55$&P|{*{7epWQ==I=P@}P+!%<Ka&u?56G;gw~=2%rTR3GMj9 zvmSU0d;{JCzWNDC&r`N2o>Le^br2pfR{#gvYK%&9(BzQ}5P`TSJQ>db$3u(O9zQO7 zLMjbV_b)gE%!hk#Bn>7nJxt67h(O!}-lAD&!~X$L>0nwJpseKe^P2!o1^xooe8C~E S!y|M60000<MNUMnLSTY+>F)9X diff --git a/app/src/main/res/drawable-mdpi/ic_action_notification_sync_disabled.png b/app/src/main/res/drawable-mdpi/ic_action_notification_sync_disabled.png deleted file mode 100755 index d8e45269053600a30ca7004c3c5cb9aee6fffa9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 641 zcmV-{0)G98P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80006=Nkl<ZSV!%a z&5Mmu9LGOjk{L;e@seaiENm4kWn=5DB*HWc%8n9$fP^dtMN>m)C`5^cvSe>X>@_T; z$Xmuw-`ja~dS>o@ZqL1SOZU{R+kN?+@A<x+NA!59$NSR<&}ZQPVIU%gfZ3%~O-cIL zDrqelh=^^#3SdIg9}kL%b--5OhNNHVvjN~Xusqvr3b-a|q85ON@M#_-03I|R5%Yji zU>UGS(vJ+lJz!<F*_8yc0le9&Vqh(B7FYu`1_BEJGxJE2Pia=nKn`FM*e&UMRnb+N zFd_`Xr(SGIXF62|z&`j2ECj|RUF`+{Yd!|d2Yx4A$MeA%fQP^<;9~lIMAEz35+r(0 z1G|97z{XsT%$Rd6i=?ONKZk*1mPL{!tnGnWz%5`sus2Qdz*WuUNB}I`UEnovKvL8- zp|!jZECRMj`k6doZ7oS79p&K>VMbm8pHf;rcLKofwgES1xu2GfLXIpIFS-H165Iqf zSc2S8iHRBINM&^gc#|Y(+6C>z@RTzRT$Xe_op2}c1XvABlOrPb0mofrlFrmNVOG74 zq1Opu6R^E#!i<S>SzQ3mNqXB6fCS<wFamr5T*n_uRa|)DUpjyRN#8myq6A>j)e9_1 z1K*bb%mGdUJDQbMtrVoqeJLFQhO;4M07YC4#HV&8cjYp23J#^0-xtf5&Hn5*bSE4H zo|Ve#k|bZ#9W~)nAm7}>T0UqbW1a^L;CXETRR(JHy`LwF>$)cZ+67-(MC}f$`@24X bJ_CON+;#UMEIbiV00000NkvXXu0mjfMFkG{ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-xhdpi/ic_action_notification_sync.png deleted file mode 100755 index 7e1ffedb3e0c682c98bd9b7ee51d8b07c5cf2d1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1296 zcmV+r1@HQaP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF000EnNkl<ZcwX(A zc?f4y6vsbbdrZmN*o!P#iewsVBuT;_{4th>7(2~O#vhbjQll7TFk?-Yk{C&ZKWtgj z$d(zQEXfin$$oUkeRX@@@Auxkci;Q{`n})%_ub{3&pqefbIyID=S@A=pDBQ;1@xT2 z)CGEO0MkQo>H?#50W(`y(%++$VPqEI$^*wrTG2ZIX0{%1J}_6(0lfkccK!n3Bw&W5 z{d)r-Gkf3)U>#SyHvnR@&y?>CfV|n~O#TGmci?A9e{}5*t=|GgW}j2?3*cAa2Vgbu z9<Wl<FI~WB6@Z!f{C?n_5u1Gl#eM^x2cD4heue(EyR-_xr82WqfpdXPYDoDF@FMVl zq#tXHFCEeafDM2-!2D8a26uQ3SR(1u26Ge+Z5=?is+r9N<`r1NEMPNWd%%ODLx4RC zT;^+Fv7}d0Lg*5JTf!;8nK6WX9AIW!0keUVfn9ST9Kw7_pQnP*8Gvve{LdW*1wv-F zU2vNtbIQ5}T$}1lNCv>nb^|_>^jF#L#O^RG5;U_x=f^{mZZ2D=%J(Dy%xoXva^Q8~ zj<OUtD7MBOBpo(P(9rjK;E2c_{C2o3#TA(fz*WHUz(2rsz%vyg%#rk9*-iy_SPbkQ z^<5$9nzFHF-x~ukvl+m1z(!#)hj59cm&+Dko9||J9B^f11s*IN-85oM0pL*JmPnb8 zfJKtNT^j|<`kL9+z)iqjQQrkkuf-Gqys+W-d)DrjbXS=|b-tU~*}!?x*yWOz)|o3C z(+Gf>ZRo)XaBw8VJV`5?2sLP|ne7YQ6McUbxU@+Y*%W{WEpGmML_!@S>ARKyxEnkh z&HXXpZt#8G^_l|M3AhdLpe1XwCH+}fpkd=eQRU63kEhZXOZuwrdQAcB0Nftp_)Lab zWmjNbVa;rP;Eia!D}IrrFYC_P6hM&+_|@dajpQxd1Y_j_9;*5w=owtr&XV+Qa_#@C zE$jy@kLLC?*$+W4snu$YEWlSZvkOCpmkDsEq-C|FDH&vDURXIN>NeVIFaXF0LLY=c zsP@TR#KFvF25a+LLDt415Go3VJzae|r4^Xj;Q_#!_XJ=(Lg8QqR|6iaXKhvRO)p>! zZD!`BvSpIIGCZ&qRF|KW<cU|K*7ytv06ZPO5pXm0<_XbD{sL4MrJ0$h&<_LmR)pYH z=7WG6#wHyO5EcTb<+fu9@aQlIX7)~a7<o*R_b;;;e^}jFlD{D|Zb>X40N4WX{QSV2 zR4)LxN-A`R03aLUwSiK1XsmTKK{5aV!Y&~<*fv_j-Qg495s%{XaeEGc-{+OwAr*wA z00IOraV^T_2idvsA>hHw+kl6$KLKy$lyL}^+#v;o<NzW|SP%}Yx^i~n4y^zP5H<%+ z2fVDkO$`ce3HN#aFUj4gmDU=7OxjQ}J`p%7svA~O#+AMw@X)%oAE{ag5P|TB(UY0I zLjh$=U}Ip%0veU8%T-7Y>(d#4Scp*T-xTnY+EY0J{l9?6tGq8XRo=&s1V9dk2SQo% zo%h3UX_XhINdwTC*~j3<#R57t`*8!{K~bk>KW+g0O|kSXF@?Vu%`pxxFw!5UM-HH^ zovk|W6hKu{liF`;0h7A3RrQ)$Kvhzc+HYzBle)82_4*fLEvZn6@o_@{0000<MNUMn GLSTXw9$41^ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_notification_sync_disabled.png b/app/src/main/res/drawable-xhdpi/ic_action_notification_sync_disabled.png deleted file mode 100755 index 00e51afb9c718c973d5f96824a12309840087a71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1388 zcmV-y1(W)TP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF000FuNkl<ZcwX(9 zS?H%z7{@<fj6KGMB}*d_289cakS*H<H!euF##l3r3$m4MvW2qm=E8+sP!l1M3tN<; zM3x~jqbwK7PCR;a-s$xI-{t(?bLv08e($;Y)$cv$InVPw&vw2?%=plZ+s_EV=m9g1 zz!(BE?f_#F97A9Xff+|&41pPUfUydUA+RBZfSGL!><Vm_sMl}6x03!%oIht;mIs*G zZopzmFZU^6W?KP^fG;Hd*)-qG76P{cd-ll--|q!pkn~UD{Bi)8**?H^z}~<;z|)fc zs_$TC`vIo{#{*vjmq_}l1AqsE%r2XL1s<35<WR$_b%Q*>%(id^00+T`E3^uDDh9#K zb_Z?*_7C$uEdW5{UPFNx3cx17?ZA<+zLLy26*F`B=KvQ2zNQ2Kt=={Sgh2rKMrP&) zxnXAm8%3MXQP9ke0&WG~_OOxUV9gmJ2XuwNvyyHZVrbi%!2kphJZYU3PPvlR77m93 zUrPF}O_>A$z5-TAT3ZT)EC3=;=naPyg%bd9F|PnOOZquQX5XA_03r~)d>vieaH4o^ z0KmQJec<q#+*Ohu=rb<mdno`806aE#u5CI|#;yTSt$ikxTL-twHS2(@C4H1)WY?Th z09+58(y~!<&8`8k-h0e!7vL^nuV|0el2*<;0A{vzh|5hQWxU+EvYpkTu6zLCPT=)= zlVfHl2T!PE{tg@`$$tlrY!5KABY?Xj4L<fH<;ta0hgkCf=E1-_^{&#)b^sh4?<}ja zT#}d8!J`BK-U}>?G<a0fBcV?yQXMMUW_CVsQM8x0!w(I%KI-BI$uKj&wcP<67717; z$usvv%~Fa2W_BR(L^Sse;0j59r>t+9ljQ+swtaB_>Q3N$oFwV{=+u&%r9LA<7`znC z`vka5(hq%W+rH-l;Md#x0MF9ZSW-WcQsEHfyc_N1w@c3hgFC}q0GtoFKad!eUWYW@ z&nLM%w4IijE${>xeRsnzmGsSgc|cPLoFM7PCWS(oOS(I>tltrMaYG6Lul=3`<5u-N zNgs5aV$xN*1%QJB4^e%7D<r{2hAG7>vO*wa`g$3BSkhx10QjEH{Uu$cV>xDaA#g$T z-~5)r0DzZ3=jN*6{rTl_B?$Aj3<S!Xev_Lt9L&t!-y5OD(OzBwyc7-|B|O0Cu~&i3 zgBH%v-X!Vy+UczT+=X4~|2e@)s)Lz1pWq#*hmemy0M9qS47NVE5vn#ev-)M|v)~2m z%*;>TJ2+>K=1^jjRENMUd3Wg`JSWNd#;lR;0pTQ_^z`d+)epEAycTG%Gl4S#*Wbtq zP7)4gb}n#?Ctpb)W?5g#0|E%k0aq>_?*YygR1Y1kq-bh51fq43B&Wx+M-dMQ0Bj3f z3mn^_RUa#yOtfZO9t0EvAb{Y{T3!*nMQ$3M00bHP0yorNhbCf{CIP1?0Kov~0!|zP zK)77F@k<)kCo@a)0zfU>x9QLq0(*pKwmP>M%bSR3?E|BT2L$7nM%lg;0Djr|9Po#g z&Uc4CL)*TW0>GP`(-NMwQ$1k2aOqhMcu!H3S&G5P0>I4tGIV-qa#9Xq4RBS*>+m)q zfv$<k&W>j3nv|#cF2!;I@UrEZKW2ox_VZiY+9B`x4b75!)0H_~fGe5>K+2JqoD(%m zI{_;tIVF}grV4;iX5H%+<h}<~s7)OJS;yV5cm$v{qSGdC^nhtQvC{gE9#9(5X_Gg4 uz_guMX?;fzD2?c}$s0Xj+D@#rzW)ILk~Ux0$Pj@50000<MNUMnLSTaM1bc=6 diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync.png deleted file mode 100755 index 4017dabe97591ca592e6766d3ba181cdee6726c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1943 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 b2kwH&+SJ@hcTJ-`-~tQ}ANN+*NdA8TlG;*a diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync_disabled.png deleted file mode 100755 index 549cf6d8876b9c3e9df0717146cd3380f869ffa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2048 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync.png deleted file mode 100755 index bea081263614e56ec9345eac00e3893eb99a09e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2779 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync_disabled.png b/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync_disabled.png deleted file mode 100755 index d71a99a1a5a5d1a496fe9e073aef42eb494008b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2864 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 From 9b1bf6bdb329a69448f11f2a459196d7a7ef08e3 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 21 Oct 2015 16:43:13 +0200 Subject: [PATCH 011/110] Moving Bitmessage context into a foreground service (work in progress) --- app/src/main/AndroidManifest.xml | 3 +- .../dissem/apps/abit/MessageListActivity.java | 16 ++--- .../notification/AbstractNotification.java | 2 +- .../notification/NetworkNotification.java | 10 +++- .../abit/synchronization/SyncAdapter.java | 19 +----- .../abit/synchronization/SyncService.java | 60 ++++++++++++++++++- 6 files changed, 81 insertions(+), 29 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f43e1e6..f635f00 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -119,8 +119,7 @@ </service> <service android:name=".synchronization.SyncService" - android:exported="true" - android:process=":sync"> + android:exported="true"> <intent-filter> <action android:name="android.content.SyncAdapter"/> </intent-filter> diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 70891ea..f488cdf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -36,9 +36,9 @@ import java.util.ArrayList; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; -import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.apps.abit.synchronization.SyncService; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; @@ -70,7 +70,7 @@ public class MessageListActivity extends AppCompatActivity public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class); - private static final long SYNC_FREQUENCY = 15 * 60; // seconds + private static final long SYNC_FREQUENCY = 15;// FIXME * 60; // seconds private static final int ADD_IDENTITY = 1; /** @@ -253,14 +253,14 @@ public class MessageListActivity extends AppCompatActivity .withOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { - // TODO: warn user, option to restrict to WiFi - if (isChecked && !bmc.isRunning()) { - bmc.startup(); - new NetworkNotification(MessageListActivity.this).show(); - } else if (bmc.isRunning()) bmc.shutdown(); + if (isChecked) { + startService(new Intent(MessageListActivity.this, SyncService.class)); + } else { + stopService(new Intent(MessageListActivity.this, SyncService.class)); + } } }) - .withChecked(bmc.isRunning()) + .withChecked(SyncService.isRunning()) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java index 60bb86d..60eb282 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java @@ -10,7 +10,7 @@ import android.content.Context; public abstract class AbstractNotification { protected final Context ctx; protected final NotificationManager manager; - public Notification notification; + protected Notification notification; public AbstractNotification(Context ctx) { diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 99f78d6..a013602 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -1,6 +1,7 @@ package ch.dissem.apps.abit.notification; import android.annotation.SuppressLint; +import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -19,6 +20,8 @@ import ch.dissem.bitmessage.utils.Property; * Shows the network status (as long as the client is connected as a full node) */ public class NetworkNotification extends AbstractNotification { + public static final int ONGOING_NOTIFICATION_ID = 2; + private final BitmessageContext bmc; private NotificationCompat.Builder builder; @@ -31,6 +34,11 @@ public class NetworkNotification extends AbstractNotification { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } + public Notification getNotification() { + update(); + return notification; + } + @SuppressLint("StringFormatMatches") private boolean update() { boolean running = bmc.isRunning(); @@ -82,6 +90,6 @@ public class NetworkNotification extends AbstractNotification { @Override protected int getNotificationId() { - return 2; + return ONGOING_NOTIFICATION_ID; } } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index c12d30c..efc1333 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -30,22 +30,9 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { /** * Set up the sync adapter */ - public SyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - bmc = Singleton.getBitmessageContext(context); - } - - /** - * Set up the sync adapter. This form of the - * constructor maintains compatibility with Android 3.0 - * and later platform versions - */ - public SyncAdapter( - Context context, - boolean autoInitialize, - boolean allowParallelSyncs) { - super(context, autoInitialize, allowParallelSyncs); - bmc = Singleton.getBitmessageContext(context); + public SyncAdapter(Context context, BitmessageContext bitmessageContext) { + super(context, true); + bmc = bitmessageContext; } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index 4beaa04..f744299 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -1,20 +1,44 @@ package ch.dissem.apps.abit.synchronization; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.IBinder; +import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.listener.MessageListener; +import ch.dissem.apps.abit.notification.NetworkNotification; +import ch.dissem.apps.abit.repository.AndroidAddressRepository; +import ch.dissem.apps.abit.repository.AndroidInventory; +import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.repository.SqlHelper; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.ports.MemoryNodeRegistry; +import ch.dissem.bitmessage.security.sc.SpongySecurity; + +import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; + /** * Define a Service that returns an IBinder for the * sync adapter class, allowing the sync adapter framework to call * onPerformSync(). */ public class SyncService extends Service { + private static MessageListener messageListener = null; + private static BitmessageContext bmc = null; // Storage for an instance of the sync adapter private static SyncAdapter syncAdapter = null; // Object to use as a thread-safe lock private static final Object syncAdapterLock = new Object(); + private static volatile boolean running = false; + + public static boolean isRunning() { + return running; + } + /* * Instantiate the sync adapter object. */ @@ -26,12 +50,46 @@ public class SyncService extends Service { * Disallow parallel syncs */ synchronized (syncAdapterLock) { + final Context ctx = getApplicationContext(); + if (bmc == null) { +// messageListener = new MessageListener(ctx); +// SqlHelper sqlHelper = new SqlHelper(ctx); +// bmc = new BitmessageContext.Builder() +// .security(new SpongySecurity()) +// .nodeRegistry(new MemoryNodeRegistry()) +// .inventory(new AndroidInventory(sqlHelper)) +// .addressRepo(new AndroidAddressRepository(sqlHelper)) +// .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) +// .networkHandler(new DefaultNetworkHandler()) +// .listener(messageListener) +// .build(); + // FIXME: this needs to change once I figured out how to get rid of those singletons + messageListener = Singleton.getMessageListener(ctx); + bmc = Singleton.getBitmessageContext(ctx); + } if (syncAdapter == null) { - syncAdapter = new SyncAdapter(getApplicationContext(), true); + syncAdapter = new SyncAdapter(ctx, bmc); } } } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // TODO: warn user, option to restrict to WiFi + running = true; + NetworkNotification networkNotification = new NetworkNotification(this); + startForeground(ONGOING_NOTIFICATION_ID, networkNotification.getNotification()); + bmc.startup(); + networkNotification.show(); + return Service.START_STICKY; + } + + @Override + public void onDestroy() { + bmc.shutdown(); + running = false; + } + /** * Return an object that allows the system to invoke * the sync adapter. From f5bf5c8bca6fdb665bd2644ef5bbd4a8848d3532 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 23 Oct 2015 22:40:09 +0200 Subject: [PATCH 012/110] Moving Bitmessage context into a foreground service (work in progress) --- app/src/main/AndroidManifest.xml | 1 + .../apps/abit/AbstractItemListFragment.java | 19 +- .../apps/abit/MessageDetailFragment.java | 19 +- .../dissem/apps/abit/MessageListActivity.java | 115 +++++++++-- .../dissem/apps/abit/MessageListFragment.java | 9 +- .../apps/abit/OpenBitmessageLinkActivity.java | 79 +++++++- .../apps/abit/SubscriptionDetailFragment.java | 3 +- .../apps/abit/SubscriptionListFragment.java | 6 +- .../notification/NetworkNotification.java | 6 +- .../notification/NewMessageNotification.java | 3 +- .../repository/AndroidMessageRepository.java | 21 +- .../dissem/apps/abit/service/Singleton.java | 67 +++++-- .../synchronization/BitmessageService.java | 186 ++++++++++++++++++ .../abit/synchronization/SyncService.java | 50 +---- 14 files changed, 441 insertions(+), 143 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f635f00..577db59 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,6 +102,7 @@ <category android:name="android.intent.category.BROWSABLE" /> </intent-filter> </activity> + <service android:name=".synchronization.BitmessageService" /> <!-- Synchronization --> <provider diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 6e25693..7b03a55 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -17,14 +17,13 @@ package ch.dissem.apps.abit; import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; import ch.dissem.apps.abit.listener.ListSelectionListener; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.valueobject.Label; /** @@ -45,7 +44,6 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { public void onItemSelected(Object plaintext) { } }; - protected BitmessageContext bmc; /** * The fragment's current callback object, which is notified of list item * clicks. @@ -59,13 +57,6 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { abstract void updateList(Label label); - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - bmc = Singleton.getBitmessageContext(getActivity()); - } - @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -89,15 +80,15 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { } @Override - public void onAttach(Activity activity) { - super.onAttach(activity); + public void onAttach(Context context) { + super.onAttach(context); // Activities containing this fragment must implement its callbacks. - if (!(activity instanceof ListSelectionListener)) { + if (!(context instanceof ListSelectionListener)) { throw new IllegalStateException("Activity must implement fragment's callbacks."); } - callbacks = (ListSelectionListener) activity; + callbacks = (ListSelectionListener) context; } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 4652f6b..c43ae5f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -6,12 +6,15 @@ import android.support.v4.app.Fragment; import android.view.*; import android.widget.ImageView; import android.widget.TextView; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.ports.MessageRepository; + import com.mikepenz.google_material_typeface_library.GoogleMaterial; import java.util.Iterator; @@ -84,7 +87,7 @@ public class MessageDetailFragment extends Fragment { } } if (removed) { - Singleton.getBitmessageContext(inflater.getContext()).messages().save(item); + Singleton.getMessageRepository(inflater.getContext()).save(item); } return rootView; } @@ -103,7 +106,7 @@ public class MessageDetailFragment extends Fragment { @Override public boolean onOptionsItemSelected(MenuItem menuItem) { - BitmessageContext bmc = Singleton.getBitmessageContext(getActivity()); + MessageRepository messageRepo = Singleton.getMessageRepository(getContext()); switch (menuItem.getItemId()) { case R.id.reply: Intent replyIntent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); @@ -113,21 +116,21 @@ public class MessageDetailFragment extends Fragment { return true; case R.id.delete: if (isInTrash(item)) { - bmc.messages().remove(item); + messageRepo.remove(item); } else { item.getLabels().clear(); - item.addLabels(bmc.messages().getLabels(Label.Type.TRASH)); - bmc.messages().save(item); + item.addLabels(messageRepo.getLabels(Label.Type.TRASH)); + messageRepo.save(item); } getActivity().onBackPressed(); return true; case R.id.mark_unread: - item.addLabels(bmc.messages().getLabels(Label.Type.UNREAD)); - bmc.messages().save(item); + item.addLabels(messageRepo.getLabels(Label.Type.UNREAD)); + messageRepo.save(item); return true; case R.id.archive: item.getLabels().clear(); - bmc.messages().save(item); + messageRepo.save(item); return true; default: return false; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index f488cdf..3899a60 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -2,9 +2,17 @@ package ch.dissem.apps.abit; import android.accounts.Account; import android.accounts.AccountManager; +import android.content.ComponentName; import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -38,12 +46,17 @@ import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.apps.abit.synchronization.BitmessageService; import ch.dissem.apps.abit.synchronization.SyncService; -import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.MessageRepository; +import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_ADDRESS; +import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_START_NODE; +import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_STOP_NODE; import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; @@ -79,15 +92,36 @@ public class MessageListActivity extends AppCompatActivity */ private boolean twoPane; + private Messenger messenger = new Messenger(new IncomingHandler()); + private Messenger service; + private boolean bound; + private ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + MessageListActivity.this.service = new Messenger(service); + MessageListActivity.this.bound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + service = null; + bound = false; + } + }; + private AccountHeader accountHeader; - private BitmessageContext bmc; private Label selectedLabel; + private MessageRepository messageRepo; + private AddressRepository addressRepo; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - bmc = Singleton.getBitmessageContext(this); - selectedLabel = bmc.messages().getLabels().get(0); + messageRepo = Singleton.getMessageRepository(this); + addressRepo = Singleton.getAddressRepository(this); + + selectedLabel = messageRepo.getLabels().get(0); setContentView(R.layout.activity_message_list); @@ -152,7 +186,7 @@ public class MessageListActivity extends AppCompatActivity private void createDrawer(Toolbar toolbar) { final ArrayList<IProfile> profiles = new ArrayList<>(); - for (BitmessageAddress identity : bmc.addresses().getIdentities()) { + for (BitmessageAddress identity : addressRepo.getIdentities()) { LOG.info("Adding identity " + identity.getAddress()); profiles.add(new ProfileDrawerItem() .withIcon(new Identicon(identity)) @@ -183,16 +217,12 @@ public class MessageListActivity extends AppCompatActivity @Override public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { if (profile.getIdentifier() == ADD_IDENTITY) { - BitmessageAddress identity = bmc.createIdentity(false); - IProfile newProfile = new ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - //we know that there are 2 setting elements. set the new profile above them ;) - accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); - } else { - accountHeader.addProfiles(newProfile); + try { + Message message = Message.obtain(null, BitmessageService.MSG_CREATE_IDENTITY); + message.replyTo = messenger; + service.send(message); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); } } // false if it should close the drawer @@ -202,7 +232,7 @@ public class MessageListActivity extends AppCompatActivity .build(); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); - for (Label label : bmc.messages().getLabels()) { + for (Label label : messageRepo.getLabels()) { PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag(label); switch (label.getType()) { case INBOX: @@ -254,13 +284,21 @@ public class MessageListActivity extends AppCompatActivity @Override public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { if (isChecked) { - startService(new Intent(MessageListActivity.this, SyncService.class)); + try { + service.send(Message.obtain(null, MSG_START_NODE)); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } } else { - stopService(new Intent(MessageListActivity.this, SyncService.class)); + try { + service.send(Message.obtain(null, MSG_STOP_NODE)); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } } } }) - .withChecked(SyncService.isRunning()) + .withChecked(BitmessageService.isRunning()) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override @@ -303,7 +341,7 @@ public class MessageListActivity extends AppCompatActivity ((MessageListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(selectedLabel); } else { - MessageListFragment listFragment = new MessageListFragment(getApplicationContext()); + MessageListFragment listFragment = new MessageListFragment(); changeList(listFragment); listFragment.updateList(selectedLabel); } @@ -360,4 +398,41 @@ public class MessageListActivity extends AppCompatActivity return selectedLabel; } + @Override + protected void onStart() { + super.onStart(); + bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + if (bound) { + unbindService(connection); + bound = false; + } + super.onStop(); + } + + private class IncomingHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case BitmessageService.MSG_CREATE_IDENTITY: + BitmessageAddress identity = (BitmessageAddress) msg.getData().getSerializable(DATA_FIELD_ADDRESS); + IProfile newProfile = new ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + //we know that there are 2 setting elements. set the new profile above them ;) + accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); + } else { + accountHeader.addProfiles(newProfile); + } + break; + default: + super.handleMessage(msg); + } + } + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 5de7c7e..802b901 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -44,11 +44,6 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { public MessageListFragment() { } - @SuppressLint("ValidFragment") - public MessageListFragment(Context ctx) { - bmc = Singleton.getBitmessageContext(ctx); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -73,7 +68,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { getActivity(), android.R.layout.simple_list_item_activated_1, android.R.id.text1, - bmc.messages().findMessages(label)) { + Singleton.getMessageRepository(getContext()).findMessages(label)) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { @@ -138,7 +133,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { case R.id.empty_trash: if (currentLabel.getType() != Label.Type.TRASH) return true; - MessageRepository repo = bmc.messages(); + MessageRepository repo = Singleton.getMessageRepository(getContext()); for (Plaintext message : repo.findMessages(currentLabel)) { repo.remove(message); } diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index cff4772..8f87f8a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -17,19 +17,52 @@ package ch.dissem.apps.abit; import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Switch; import android.widget.TextView; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.dissem.apps.abit.synchronization.BitmessageService; import ch.dissem.bitmessage.entity.BitmessageAddress; +import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_ADDRESS; +import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_ADD_CONTACT; +import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_SUBSCRIBE; +import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_SUBSCRIBE_AND_ADD_CONTACT; + public class OpenBitmessageLinkActivity extends AppCompatActivity { + private static final Logger LOG = LoggerFactory.getLogger(OpenBitmessageLinkActivity.class); + + private Messenger service; + private boolean bound; + private ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + OpenBitmessageLinkActivity.this.service = new Messenger(service); + OpenBitmessageLinkActivity.this.bound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + service = null; + bound = false; + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -70,14 +103,29 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { ok.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - BitmessageContext bmc = Singleton.getBitmessageContext(OpenBitmessageLinkActivity.this); BitmessageAddress bmAddress = new BitmessageAddress(address); bmAddress.setAlias(label.getText().toString()); - if (subscribe.isChecked()) { - bmc.addSubscribtion(bmAddress); - } - if (importContact.isChecked()) { - bmc.addContact(bmAddress); + + final int what; + if (subscribe.isChecked() && importContact.isChecked()) + what = MSG_SUBSCRIBE_AND_ADD_CONTACT; + else if (subscribe.isChecked()) + what = MSG_SUBSCRIBE; + else if (importContact.isChecked()) + what = MSG_ADD_CONTACT; + else + what = 0; + + if (what != 0) { + try { + Message message = Message.obtain(null, what); + Bundle bundle = new Bundle(); + bundle.putSerializable(DATA_FIELD_ADDRESS, bmAddress); + message.setData(bundle); + service.send(message); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } } setResult(Activity.RESULT_OK); finish(); @@ -110,4 +158,19 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { return new String[0]; } } + + @Override + protected void onStart() { + super.onStart(); + bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + if (bound) { + unbindService(connection); + bound = false; + } + super.onStop(); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java index fa16aec..0fa31e8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java @@ -27,6 +27,7 @@ import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -115,7 +116,7 @@ public class SubscriptionDetailFragment extends Fragment { @Override public void onPause() { - Singleton.getBitmessageContext(getActivity()).addresses().save(item); + Singleton.getAddressRepository(getContext()).save(item); super.onPause(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java index a3c7b00..18fa320 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java @@ -24,6 +24,8 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; + +import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -32,7 +34,7 @@ import java.util.Comparator; import java.util.List; /** - * Created by chris on 06.09.15. + * Fragment that shows a list of all contacts, the ones we subscribed to first. */ public class SubscriptionListFragment extends AbstractItemListFragment<BitmessageAddress> { @Override @@ -43,7 +45,7 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag } public void updateList() { - List<BitmessageAddress> addresses = bmc.addresses().getContacts(); + List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()).getContacts(); Collections.sort(addresses, new Comparator<BitmessageAddress>() { /** * Yields the following order: diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index a013602..3530c2a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -25,9 +25,9 @@ public class NetworkNotification extends AbstractNotification { private final BitmessageContext bmc; private NotificationCompat.Builder builder; - public NetworkNotification(Context ctx) { - super(ctx); - bmc = Singleton.getBitmessageContext(ctx); + public NetworkNotification(Context ctx, BitmessageContext bmc) { + super(ctx.getApplicationContext()); + this.bmc = bmc; builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_full_node) .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index ee10bf1..2bb63df 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.Plaintext; import static ch.dissem.apps.abit.util.Drawables.toBitmap; public class NewMessageNotification extends AbstractNotification { + public static final int NEW_MESSAGE_NOTIFICATION_ID = 1; private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD); public NewMessageNotification(Context ctx) { @@ -76,6 +77,6 @@ public class NewMessageNotification extends AbstractNotification { @Override protected int getNotificationId() { - return 1; + return NEW_MESSAGE_NOTIFICATION_ID; } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index eba1b0d..83293e7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -23,11 +23,13 @@ import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; @@ -45,7 +47,7 @@ import static ch.dissem.apps.abit.repository.SqlHelper.join; /** * {@link MessageRepository} implementation using the Android SQL API. */ -public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder { +public class AndroidMessageRepository implements MessageRepository { private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class); private static final String TABLE_NAME = "Message"; @@ -71,16 +73,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont private static final String LBL_COLUMN_ORDER = "ord"; private final SqlHelper sql; private final Context ctx; - private InternalContext bmc; + + private final AddressRepository addressRepo; public AndroidMessageRepository(SqlHelper sql, Context ctx) { this.sql = sql; this.ctx = ctx; - } - - @Override - public void setContext(InternalContext context) { - bmc = context; + this.addressRepo = Singleton.getAddressRepository(ctx); } @Override @@ -230,8 +229,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); - builder.from(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER)))); - builder.to(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT)))); + builder.from(addressRepo.getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER)))); + builder.to(addressRepo.getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT)))); builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))); @@ -257,12 +256,12 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress()); + BitmessageAddress savedAddress = addressRepo.getAddress(message.getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); } - bmc.getAddressRepo().save(message.getFrom()); + addressRepo.save(message.getFrom()); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 220dc36..469d1db 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,6 +2,7 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; @@ -9,35 +10,24 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.ports.Security; import ch.dissem.bitmessage.security.sc.SpongySecurity; /** * Provides singleton objects across the application. */ public class Singleton { - private static BitmessageContext bitmessageContext; + private static SqlHelper sqlHelper; + private static Security security; + private static MessageRepository messageRepository; private static MessageListener messageListener; + private static AddressRepository addressRepository; - public static BitmessageContext getBitmessageContext(Context context) { - if (bitmessageContext == null) { - synchronized (Singleton.class) { - if (bitmessageContext == null) { - final Context ctx = context.getApplicationContext(); - SqlHelper sqlHelper = new SqlHelper(ctx); - bitmessageContext = new BitmessageContext.Builder() - .security(new SpongySecurity()) - .nodeRegistry(new MemoryNodeRegistry()) - .inventory(new AndroidInventory(sqlHelper)) - .addressRepo(new AndroidAddressRepository(sqlHelper)) - .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) - .networkHandler(new DefaultNetworkHandler()) - .listener(getMessageListener(ctx)) - .build(); - } - } - } - return bitmessageContext; + static { + ch.dissem.bitmessage.utils.Singleton.initialize(new SpongySecurity()); } public static MessageListener getMessageListener(Context ctx) { @@ -50,4 +40,41 @@ public class Singleton { } return messageListener; } + + public static SqlHelper getSqlHelper(Context ctx) { + if (sqlHelper == null) { + synchronized (Singleton.class) { + if (sqlHelper == null) { + sqlHelper = new SqlHelper(ctx.getApplicationContext()); + } + } + } + return sqlHelper; + } + + public static MessageRepository getMessageRepository(Context ctx) { + if (messageRepository == null) { + ctx = ctx.getApplicationContext(); + getSqlHelper(ctx); + synchronized (Singleton.class) { + if (messageRepository == null) { + messageRepository = new AndroidMessageRepository(sqlHelper, ctx); + } + } + } + return messageRepository; + } + + public static AddressRepository getAddressRepository(Context ctx) { + if (addressRepository == null) { + ctx = ctx.getApplicationContext(); + getSqlHelper(ctx); + synchronized (Singleton.class) { + if (addressRepository == null) { + addressRepository = new AndroidAddressRepository(sqlHelper); + } + } + } + return addressRepository; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java new file mode 100644 index 0000000..76176fb --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java @@ -0,0 +1,186 @@ +package ch.dissem.apps.abit.synchronization; + +import android.app.Service; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.preference.PreferenceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import ch.dissem.apps.abit.listener.MessageListener; +import ch.dissem.apps.abit.notification.NetworkNotification; +import ch.dissem.apps.abit.repository.AndroidInventory; +import ch.dissem.apps.abit.repository.SqlHelper; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.ports.MemoryNodeRegistry; +import ch.dissem.bitmessage.security.sc.SpongySecurity; + +import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; + +/** + * Define a Service that returns an IBinder for the + * sync adapter class, allowing the sync adapter framework to call + * onPerformSync(). + */ +public class BitmessageService extends Service { + public static final Logger LOG = LoggerFactory.getLogger(BitmessageService.class); + + public static final int MSG_SYNC = 2; + public static final int MSG_CREATE_IDENTITY = 10; + public static final int MSG_SUBSCRIBE = 20; + public static final int MSG_ADD_CONTACT = 21; + public static final int MSG_SUBSCRIBE_AND_ADD_CONTACT = 23; + public static final int MSG_START_NODE = 100; + public static final int MSG_STOP_NODE = 101; + + public static final String DATA_FIELD_ADDRESS = "address"; + + // Object to use as a thread-safe lock + private static final Object lock = new Object(); + + private static MessageListener messageListener = null; + private static NetworkNotification notification = null; + private static BitmessageContext bmc = null; + + private static volatile boolean running = false; + + private static Messenger messenger; + + public static boolean isRunning() { + return running; + } + + @Override + public void onCreate() { + synchronized (lock) { + if (bmc == null) { + messageListener = Singleton.getMessageListener(this); + SqlHelper sqlHelper = Singleton.getSqlHelper(this); + bmc = new BitmessageContext.Builder() + .security(new SpongySecurity()) + .nodeRegistry(new MemoryNodeRegistry()) + .inventory(new AndroidInventory(sqlHelper)) + .addressRepo(Singleton.getAddressRepository(this)) + .messageRepo(Singleton.getMessageRepository(this)) + .networkHandler(new DefaultNetworkHandler()) + .listener(messageListener) + .build(); + notification = new NetworkNotification(this, bmc); + messenger = new Messenger(new IncomingHandler()); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return Service.START_STICKY; + } + + @Override + public void onDestroy() { + bmc.shutdown(); + running = false; + } + + /** + * Return an object that allows the system to invoke + * the sync adapter. + */ + @Override + public IBinder onBind(Intent intent) { + return messenger.getBinder(); + } + + private class IncomingHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_CREATE_IDENTITY: + BitmessageAddress identity = bmc.createIdentity(false); + if (msg.replyTo != null) { + try { + Message message = Message.obtain(this, MSG_CREATE_IDENTITY); + Bundle bundle = new Bundle(); + bundle.putSerializable(DATA_FIELD_ADDRESS, identity); + message.setData(bundle); + msg.replyTo.send(message); + } catch (RemoteException e) { + LOG.debug(e.getMessage(), e); + } + } + break; + case MSG_SUBSCRIBE: + BitmessageAddress address = (BitmessageAddress) msg.getData().getSerializable(DATA_FIELD_ADDRESS); + bmc.addSubscribtion(address); + break; + case MSG_SYNC: + LOG.info("Synchronizing Bitmessage"); + // If the Bitmessage context acts as a full node, synchronization isn't necessary + if (bmc.isRunning()) break; + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( + BitmessageService.this); + + String trustedNode = preferences.getString("trusted_node", null); + if (trustedNode == null) break; + trustedNode = trustedNode.trim(); + if (trustedNode.isEmpty()) break; + + int port; + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + String portString = trustedNode.substring(index + 1); + trustedNode = trustedNode.substring(0, index); + try { + port = Integer.parseInt(portString); + } catch (NumberFormatException e) { + LOG.error("Invalid port " + portString); + // TODO: show error as notification + return; + } + } else { + port = 8444; + } + long timeoutInSeconds = preferences.getInt("sync_timeout", 120); + try { + LOG.info("Synchronization started"); + bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); + LOG.info("Synchronization finished"); + } catch (UnknownHostException e) { + LOG.error("Couldn't synchronize", e); + // TODO: show error as notification + } + break; + case MSG_START_NODE: + startService(new Intent(BitmessageService.this, BitmessageService.class)); + // TODO: warn user, option to restrict to WiFi + running = true; + startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); + bmc.startup(); + notification.show(); + break; + case MSG_STOP_NODE: + bmc.shutdown(); + running = false; + stopForeground(false); + stopService(new Intent(BitmessageService.this, BitmessageService.class)); + break; + default: + super.handleMessage(msg); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index f744299..dca34dc 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -1,16 +1,12 @@ package ch.dissem.apps.abit.synchronization; import android.app.Service; -import android.content.Context; import android.content.Intent; import android.os.IBinder; -import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; -import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; @@ -26,20 +22,12 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIF * onPerformSync(). */ public class SyncService extends Service { - private static MessageListener messageListener = null; - private static BitmessageContext bmc = null; // Storage for an instance of the sync adapter private static SyncAdapter syncAdapter = null; // Object to use as a thread-safe lock private static final Object syncAdapterLock = new Object(); - private static volatile boolean running = false; - - public static boolean isRunning() { - return running; - } - - /* + /** * Instantiate the sync adapter object. */ @Override @@ -50,46 +38,12 @@ public class SyncService extends Service { * Disallow parallel syncs */ synchronized (syncAdapterLock) { - final Context ctx = getApplicationContext(); - if (bmc == null) { -// messageListener = new MessageListener(ctx); -// SqlHelper sqlHelper = new SqlHelper(ctx); -// bmc = new BitmessageContext.Builder() -// .security(new SpongySecurity()) -// .nodeRegistry(new MemoryNodeRegistry()) -// .inventory(new AndroidInventory(sqlHelper)) -// .addressRepo(new AndroidAddressRepository(sqlHelper)) -// .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) -// .networkHandler(new DefaultNetworkHandler()) -// .listener(messageListener) -// .build(); - // FIXME: this needs to change once I figured out how to get rid of those singletons - messageListener = Singleton.getMessageListener(ctx); - bmc = Singleton.getBitmessageContext(ctx); - } if (syncAdapter == null) { - syncAdapter = new SyncAdapter(ctx, bmc); + syncAdapter = new SyncAdapter(this, null); // FIXME } } } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // TODO: warn user, option to restrict to WiFi - running = true; - NetworkNotification networkNotification = new NetworkNotification(this); - startForeground(ONGOING_NOTIFICATION_ID, networkNotification.getNotification()); - bmc.startup(); - networkNotification.show(); - return Service.START_STICKY; - } - - @Override - public void onDestroy() { - bmc.shutdown(); - running = false; - } - /** * Return an object that allows the system to invoke * the sync adapter. From 725089c604b0a50d80f1755912c1bf3beea83a78 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 24 Oct 2015 20:59:24 +0200 Subject: [PATCH 013/110] Fixed NullPointerException and minor improvements --- .../dissem/apps/abit/MessageListActivity.java | 26 ++-- .../notification/NewMessageNotification.java | 1 - .../synchronization/BitmessageService.java | 2 +- .../res/layout/fragment_message_detail.xml | 130 +++++++++--------- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 3899a60..1797d17 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -280,25 +280,27 @@ public class MessageListActivity extends AppCompatActivity new SwitchDrawerItem() .withName(R.string.full_node) .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) + .withChecked(BitmessageService.isRunning()) .withOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - try { - service.send(Message.obtain(null, MSG_START_NODE)); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); - } - } else { - try { - service.send(Message.obtain(null, MSG_STOP_NODE)); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); + if (messenger != null) { + if (isChecked) { + try { + service.send(Message.obtain(null, MSG_START_NODE)); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } + } else { + try { + service.send(Message.obtain(null, MSG_STOP_NODE)); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } } } } }) - .withChecked(BitmessageService.isRunning()) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index 2bb63df..4e66ab1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -1,6 +1,5 @@ package ch.dissem.apps.abit.notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java index 76176fb..f611630 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java @@ -91,7 +91,7 @@ public class BitmessageService extends Service { @Override public void onDestroy() { - bmc.shutdown(); + if (bmc.isRunning()) bmc.shutdown(); running = false; } diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index b10bfab..55e0516 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -1,82 +1,82 @@ <?xml version="1.0" encoding="utf-8"?> -<ScrollView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_height="match_parent" - android:layout_width="match_parent"> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> <RelativeLayout - android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true" + android:orientation="vertical"> + + <TextView + android:id="@+id/subject" android:layout_width="match_parent" android:layout_height="wrap_content" - android:fitsSystemWindows="true"> - - <TextView - android:id="@+id/subject" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceLarge" - android:gravity="center_vertical" - android:text="Subject" - android:padding="16dp" - android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:elegantTextHeight="false" - android:enabled="false"/> + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:elegantTextHeight="false" + android:enabled="false" + android:gravity="center_vertical" + android:padding="16dp" + android:text="Subject" + android:textAppearance="?android:attr/textAppearanceLarge" /> <View - android:id="@+id/divider" - android:layout_width="fill_parent" - android:layout_height="2dip" - android:layout_below="@id/subject" - android:background="@color/divider"/> + android:id="@+id/divider" + android:layout_width="fill_parent" + android:layout_height="2dip" + android:layout_below="@id/subject" + android:background="@color/divider" /> <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_below="@+id/divider" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:src="@color/accent" - android:layout_margin="16dp"/> + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/divider" + android:layout_margin="16dp" + android:src="@color/accent" /> <TextView - android:id="@+id/sender" - android:layout_width="wrap_content" - android:layout_height="20dp" - android:text="Sender" - android:layout_alignTop="@+id/avatar" - android:layout_toRightOf="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:gravity="center_vertical" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:textStyle="bold"/> + android:id="@+id/sender" + android:layout_width="wrap_content" + android:layout_height="20dp" + android:layout_alignTop="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:gravity="center_vertical" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:text="Sender" + android:textStyle="bold" /> <TextView - android:id="@+id/recipient" - android:layout_width="wrap_content" - android:layout_height="20dp" - android:text="Recipient" - android:gravity="center_vertical" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:layout_alignBottom="@+id/avatar" - android:layout_toRightOf="@+id/avatar" - android:layout_toEndOf="@+id/avatar"/> + android:id="@+id/recipient" + android:layout_width="wrap_content" + android:layout_height="20dp" + android:layout_alignBottom="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:gravity="center_vertical" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:text="Recipient" /> <TextView - android:id="@+id/text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="New Text" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginTop="32dp" - android:paddingBottom="64dp" - android:layout_below="@+id/avatar" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true"/> + android:id="@+id/text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/avatar" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginTop="32dp" + android:paddingBottom="64dp" + android:text="New Text" + android:textIsSelectable="true" /> </RelativeLayout> </ScrollView> \ No newline at end of file From 7fe7ee42fccfea19381e9ddaf203afeedc5ab3b7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 25 Oct 2015 09:50:40 +0100 Subject: [PATCH 014/110] Fixed initialisation, added message/broadcast sending to service --- .../dissem/apps/abit/MessageListActivity.java | 29 ++++---- .../repository/AndroidMessageRepository.java | 21 +++--- .../dissem/apps/abit/service/Singleton.java | 62 +++++++---------- .../synchronization/BitmessageService.java | 66 ++++++++++++------- .../abit/synchronization/SyncAdapter.java | 6 +- .../abit/synchronization/SyncService.java | 2 +- 6 files changed, 97 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index 1797d17..e3b469f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -47,14 +47,13 @@ import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; import ch.dissem.apps.abit.synchronization.BitmessageService; -import ch.dissem.apps.abit.synchronization.SyncService; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; -import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_ADDRESS; +import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_IDENTITY; import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_START_NODE; import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_STOP_NODE; import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; @@ -419,19 +418,23 @@ public class MessageListActivity extends AppCompatActivity @Override public void handleMessage(Message msg) { switch (msg.what) { - case BitmessageService.MSG_CREATE_IDENTITY: - BitmessageAddress identity = (BitmessageAddress) msg.getData().getSerializable(DATA_FIELD_ADDRESS); - IProfile newProfile = new ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - //we know that there are 2 setting elements. set the new profile above them ;) - accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); - } else { - accountHeader.addProfiles(newProfile); + case BitmessageService.MSG_CREATE_IDENTITY: { + Serializable data = msg.getData().getSerializable(DATA_FIELD_IDENTITY); + if (data instanceof BitmessageAddress) { + BitmessageAddress identity = (BitmessageAddress) data; + IProfile newProfile = new ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + //we know that there are 2 setting elements. set the new profile above them ;) + accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); + } else { + accountHeader.addProfiles(newProfile); + } } break; + } default: super.handleMessage(msg); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 83293e7..eba1b0d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -23,13 +23,11 @@ import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; @@ -47,7 +45,7 @@ import static ch.dissem.apps.abit.repository.SqlHelper.join; /** * {@link MessageRepository} implementation using the Android SQL API. */ -public class AndroidMessageRepository implements MessageRepository { +public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class); private static final String TABLE_NAME = "Message"; @@ -73,13 +71,16 @@ public class AndroidMessageRepository implements MessageRepository { private static final String LBL_COLUMN_ORDER = "ord"; private final SqlHelper sql; private final Context ctx; - - private final AddressRepository addressRepo; + private InternalContext bmc; public AndroidMessageRepository(SqlHelper sql, Context ctx) { this.sql = sql; this.ctx = ctx; - this.addressRepo = Singleton.getAddressRepository(ctx); + } + + @Override + public void setContext(InternalContext context) { + bmc = context; } @Override @@ -229,8 +230,8 @@ public class AndroidMessageRepository implements MessageRepository { long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); - builder.from(addressRepo.getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER)))); - builder.to(addressRepo.getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT)))); + builder.from(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER)))); + builder.to(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT)))); builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))); @@ -256,12 +257,12 @@ public class AndroidMessageRepository implements MessageRepository { // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = addressRepo.getAddress(message.getFrom().getAddress()); + BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); } - addressRepo.save(message.getFrom()); + bmc.getAddressRepo().save(message.getFrom()); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 469d1db..f00d3c7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,6 +2,8 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import java.util.Objects; + import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.repository.AndroidAddressRepository; @@ -20,14 +22,29 @@ import ch.dissem.bitmessage.security.sc.SpongySecurity; * Provides singleton objects across the application. */ public class Singleton { - private static SqlHelper sqlHelper; - private static Security security; - private static MessageRepository messageRepository; + public static final Object lock = new Object(); + private static BitmessageContext bitmessageContext; private static MessageListener messageListener; - private static AddressRepository addressRepository; - static { - ch.dissem.bitmessage.utils.Singleton.initialize(new SpongySecurity()); + public static BitmessageContext getBitmessageContext(Context context) { + if (bitmessageContext == null) { + synchronized (lock) { + if (bitmessageContext == null) { + final Context ctx = context.getApplicationContext(); + SqlHelper sqlHelper = new SqlHelper(ctx); + bitmessageContext = new BitmessageContext.Builder() + .security(new SpongySecurity()) + .nodeRegistry(new MemoryNodeRegistry()) + .inventory(new AndroidInventory(sqlHelper)) + .addressRepo(new AndroidAddressRepository(sqlHelper)) + .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) + .networkHandler(new DefaultNetworkHandler()) + .listener(getMessageListener(ctx)) + .build(); + } + } + } + return bitmessageContext; } public static MessageListener getMessageListener(Context ctx) { @@ -41,40 +58,11 @@ public class Singleton { return messageListener; } - public static SqlHelper getSqlHelper(Context ctx) { - if (sqlHelper == null) { - synchronized (Singleton.class) { - if (sqlHelper == null) { - sqlHelper = new SqlHelper(ctx.getApplicationContext()); - } - } - } - return sqlHelper; - } - public static MessageRepository getMessageRepository(Context ctx) { - if (messageRepository == null) { - ctx = ctx.getApplicationContext(); - getSqlHelper(ctx); - synchronized (Singleton.class) { - if (messageRepository == null) { - messageRepository = new AndroidMessageRepository(sqlHelper, ctx); - } - } - } - return messageRepository; + return getBitmessageContext(ctx).messages(); } public static AddressRepository getAddressRepository(Context ctx) { - if (addressRepository == null) { - ctx = ctx.getApplicationContext(); - getSqlHelper(ctx); - synchronized (Singleton.class) { - if (addressRepository == null) { - addressRepository = new AndroidAddressRepository(sqlHelper); - } - } - } - return addressRepository; + return getBitmessageContext(ctx).addresses(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java index f611630..bb15042 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java @@ -14,19 +14,14 @@ import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.Serializable; import java.net.InetAddress; import java.net.UnknownHostException; -import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.apps.abit.repository.AndroidInventory; -import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.security.sc.SpongySecurity; import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; @@ -43,15 +38,19 @@ public class BitmessageService extends Service { public static final int MSG_SUBSCRIBE = 20; public static final int MSG_ADD_CONTACT = 21; public static final int MSG_SUBSCRIBE_AND_ADD_CONTACT = 23; + public static final int MSG_SEND_MESSAGE = 30; + public static final int MSG_SEND_BROADCAST = 31; public static final int MSG_START_NODE = 100; public static final int MSG_STOP_NODE = 101; + public static final String DATA_FIELD_IDENTITY = "identity"; public static final String DATA_FIELD_ADDRESS = "address"; + public static final String DATA_FIELD_SUBJECT = "subject"; + public static final String DATA_FIELD_MESSAGE = "message"; // Object to use as a thread-safe lock private static final Object lock = new Object(); - private static MessageListener messageListener = null; private static NetworkNotification notification = null; private static BitmessageContext bmc = null; @@ -67,17 +66,7 @@ public class BitmessageService extends Service { public void onCreate() { synchronized (lock) { if (bmc == null) { - messageListener = Singleton.getMessageListener(this); - SqlHelper sqlHelper = Singleton.getSqlHelper(this); - bmc = new BitmessageContext.Builder() - .security(new SpongySecurity()) - .nodeRegistry(new MemoryNodeRegistry()) - .inventory(new AndroidInventory(sqlHelper)) - .addressRepo(Singleton.getAddressRepository(this)) - .messageRepo(Singleton.getMessageRepository(this)) - .networkHandler(new DefaultNetworkHandler()) - .listener(messageListener) - .build(); + bmc = Singleton.getBitmessageContext(this); notification = new NetworkNotification(this, bmc); messenger = new Messenger(new IncomingHandler()); } @@ -108,13 +97,13 @@ public class BitmessageService extends Service { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_CREATE_IDENTITY: + case MSG_CREATE_IDENTITY: { BitmessageAddress identity = bmc.createIdentity(false); if (msg.replyTo != null) { try { Message message = Message.obtain(this, MSG_CREATE_IDENTITY); Bundle bundle = new Bundle(); - bundle.putSerializable(DATA_FIELD_ADDRESS, identity); + bundle.putSerializable(DATA_FIELD_IDENTITY, identity); message.setData(bundle); msg.replyTo.send(message); } catch (RemoteException e) { @@ -122,11 +111,15 @@ public class BitmessageService extends Service { } } break; - case MSG_SUBSCRIBE: - BitmessageAddress address = (BitmessageAddress) msg.getData().getSerializable(DATA_FIELD_ADDRESS); - bmc.addSubscribtion(address); + } + case MSG_SUBSCRIBE: { + Serializable data = msg.getData().getSerializable(DATA_FIELD_ADDRESS); + if (data instanceof BitmessageAddress) { + bmc.addSubscribtion((BitmessageAddress) data); + } break; - case MSG_SYNC: + } + case MSG_SYNC: { LOG.info("Synchronizing Bitmessage"); // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) break; @@ -164,9 +157,32 @@ public class BitmessageService extends Service { // TODO: show error as notification } break; + } + case MSG_SEND_MESSAGE: { + Serializable identity = msg.getData().getSerializable(DATA_FIELD_IDENTITY); + Serializable address = msg.getData().getSerializable(DATA_FIELD_ADDRESS); + if (identity instanceof BitmessageAddress + && address instanceof BitmessageAddress) { + String subject = msg.getData().getString(DATA_FIELD_SUBJECT); + String message = msg.getData().getString(DATA_FIELD_MESSAGE); + bmc.send((BitmessageAddress) identity, (BitmessageAddress) address, + subject, message); + } + break; + } + case MSG_SEND_BROADCAST: { + Serializable data = msg.getData().getSerializable(DATA_FIELD_IDENTITY); + if (data instanceof BitmessageAddress) { + String subject = msg.getData().getString(DATA_FIELD_SUBJECT); + String message = msg.getData().getString(DATA_FIELD_MESSAGE); + bmc.broadcast((BitmessageAddress) data, subject, message); + } + break; + } case MSG_START_NODE: - startService(new Intent(BitmessageService.this, BitmessageService.class)); // TODO: warn user, option to restrict to WiFi + // (I'm not quite sure this can be done here, though) + startService(new Intent(BitmessageService.this, BitmessageService.class)); running = true; startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); bmc.startup(); diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index efc1333..4935e68 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -30,9 +30,9 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { /** * Set up the sync adapter */ - public SyncAdapter(Context context, BitmessageContext bitmessageContext) { - super(context, true); - bmc = bitmessageContext; + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + bmc = Singleton.getBitmessageContext(context); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index dca34dc..cdd0143 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -39,7 +39,7 @@ public class SyncService extends Service { */ synchronized (syncAdapterLock) { if (syncAdapter == null) { - syncAdapter = new SyncAdapter(this, null); // FIXME + syncAdapter = new SyncAdapter(this, true); } } } From 2a17bbe34b92e8fde6b80b4c54a7a87a51b01e2a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 25 Oct 2015 11:29:46 +0100 Subject: [PATCH 015/110] Synchronization works, at least basically --- .../dissem/apps/abit/synchronization/Authenticator.java | 2 +- .../ch/dissem/apps/abit/synchronization/SyncAdapter.java | 9 ++++++--- .../ch/dissem/apps/abit/synchronization/SyncService.java | 4 ++++ app/src/main/res/xml/authenticator.xml | 2 +- app/src/main/res/xml/syncadapter.xml | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java index 5a6d3c3..6d4eec5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java @@ -13,7 +13,7 @@ import android.os.Bundle; */ public class Authenticator extends AbstractAccountAuthenticator { public static final String ACCOUNT_NAME = "Bitmessage"; - public static final String ACCOUNT_TYPE = "bitmessage.dissem.ch"; + public static final String ACCOUNT_TYPE = "ch.dissem.bitmessage"; // Simple constructor public Authenticator(Context context) { diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 4935e68..4bbc627 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -37,9 +37,12 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - LOG.info("Synchronizing Bitmessage"); // If the Bitmessage context acts as a full node, synchronization isn't necessary - if (bmc.isRunning()) return; + if (bmc.isRunning()) { + LOG.info("Synchronization skipped, Abit is acting as a full node"); + return; + } + LOG.info("Synchronizing Bitmessage"); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); @@ -63,7 +66,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } else { port = 8444; } - long timeoutInSeconds = preferences.getInt("sync_timeout", 120); + long timeoutInSeconds = Long.parseLong(preferences.getString("sync_timeout", "120")); try { LOG.info("Synchronization started"); bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index cdd0143..b3abcaf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -4,6 +4,9 @@ import android.app.Service; import android.content.Intent; import android.os.IBinder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.apps.abit.repository.AndroidInventory; @@ -22,6 +25,7 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIF * onPerformSync(). */ public class SyncService extends Service { + private static final Logger LOG = LoggerFactory.getLogger(SyncService.class); // Storage for an instance of the sync adapter private static SyncAdapter syncAdapter = null; // Object to use as a thread-safe lock diff --git a/app/src/main/res/xml/authenticator.xml b/app/src/main/res/xml/authenticator.xml index aa46bf6..17609a0 100644 --- a/app/src/main/res/xml/authenticator.xml +++ b/app/src/main/res/xml/authenticator.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" - android:accountType="bitmessage.dissem.ch" + android:accountType="ch.dissem.bitmessage" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:smallIcon="@mipmap/ic_launcher" /> \ No newline at end of file diff --git a/app/src/main/res/xml/syncadapter.xml b/app/src/main/res/xml/syncadapter.xml index 20cacd2..fabc86f 100644 --- a/app/src/main/res/xml/syncadapter.xml +++ b/app/src/main/res/xml/syncadapter.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" - android:contentAuthority="ch.dissem.bitmessage.provider" + android:contentAuthority="ch.dissem.apps.abit.provider" android:accountType="ch.dissem.bitmessage" android:userVisible="true" android:supportsUploading="true" From e98eefe2cc3b8a220e89e9b7433d9539ea0e10e8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 29 Oct 2015 16:38:50 +0100 Subject: [PATCH 016/110] Added notification for errors and warnings --- .../abit/notification/ErrorNotification.java | 41 +++++++++++++++++++ .../res/drawable/ic_notification_error.xml | 9 ++++ .../ic_notification_proof_of_work.xml | 9 ++++ .../res/drawable/ic_notification_warning.xml | 9 ++++ 4 files changed, 68 insertions(+) create mode 100644 app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java create mode 100644 app/src/main/res/drawable/ic_notification_error.xml create mode 100644 app/src/main/res/drawable/ic_notification_proof_of_work.xml create mode 100644 app/src/main/res/drawable/ic_notification_warning.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java new file mode 100644 index 0000000..d14aa78 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java @@ -0,0 +1,41 @@ +package ch.dissem.apps.abit.notification; + +import android.content.Context; +import android.support.v7.app.NotificationCompat; + +import ch.dissem.apps.abit.R; + +/** + * Created by chrigu on 29.10.15. + */ +public class ErrorNotification extends AbstractNotification { + public static final int ERROR_NOTIFICATION_ID = 4; + + private NotificationCompat.Builder builder; + + public ErrorNotification(Context ctx) { + super(ctx); + builder = new NotificationCompat.Builder(ctx); + builder.setContentTitle(ctx.getString(R.string.app_name)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + } + + public ErrorNotification setWarning(int resId, Object... args) { + builder.setSmallIcon(R.drawable.ic_notification_warning) + .setContentText(ctx.getString(resId, args)); + notification = builder.build(); + return this; + } + + public ErrorNotification setError(int resId, Object... args) { + builder.setSmallIcon(R.drawable.ic_notification_error) + .setContentText(ctx.getString(resId, args)); + notification = builder.build(); + return this; + } + + @Override + protected int getNotificationId() { + return ERROR_NOTIFICATION_ID; + } +} diff --git a/app/src/main/res/drawable/ic_notification_error.xml b/app/src/main/res/drawable/ic_notification_error.xml new file mode 100644 index 0000000..bc132bb --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_error.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-2h2v2zm0,-4h-2V7h2v6z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notification_proof_of_work.xml b/app/src/main/res/drawable/ic_notification_proof_of_work.xml new file mode 100644 index 0000000..c64f014 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_proof_of_work.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M11.71,19C9.93,19 8.5,17.59 8.5,15.86C8.5,14.24 9.53,13.1 11.3,12.74C13.07,12.38 14.9,11.53 15.92,10.16C16.31,11.45 16.5,12.81 16.5,14.2C16.5,16.84 14.36,19 11.71,19M13.5,0.67C13.5,0.67 14.24,3.32 14.24,5.47C14.24,7.53 12.89,9.2 10.83,9.2C8.76,9.2 7.2,7.53 7.2,5.47L7.23,5.1C5.21,7.5 4,10.61 4,14A8,8 0 0,0 12,22A8,8 0 0,0 20,14C20,8.6 17.41,3.8 13.5,0.67Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_notification_warning.xml b/app/src/main/res/drawable/ic_notification_warning.xml new file mode 100644 index 0000000..0510991 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_warning.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M1,21h22L12,2 1,21zm12,-3h-2v-2h2v2zm0,-4h-2v-4h2v4z"/> +</vector> From 54a319638b9ede811aa8cf4bee1be8ff08d29baa Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 31 Oct 2015 07:49:03 +0100 Subject: [PATCH 017/110] Added a service based POW engine, so it shouldn't be killed by the system, at least not that easily. (WIP) --- app/src/main/AndroidManifest.xml | 3 +- .../dissem/apps/abit/MessageListActivity.java | 43 ++++++--- .../apps/abit/OpenBitmessageLinkActivity.java | 10 +-- .../notification/AbstractNotification.java | 6 +- .../abit/notification/ErrorNotification.java | 5 +- .../notification/NetworkNotification.java | 4 +- .../notification/ProofOfWorkNotification.java | 38 ++++++++ .../BitmessageService.java | 66 +++----------- .../apps/abit/service/ProofOfWorkService.java | 88 +++++++++++++++++++ .../apps/abit/service/ServicePowEngine.java | 61 +++++++++++++ .../dissem/apps/abit/service/Singleton.java | 5 +- .../abit/synchronization/SyncAdapter.java | 14 ++- app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 14 files changed, 266 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java rename app/src/main/java/ch/dissem/apps/abit/{synchronization => service}/BitmessageService.java (67%) create mode 100644 app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 577db59..f0fbceb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,7 +102,8 @@ <category android:name="android.intent.category.BROWSABLE" /> </intent-filter> </activity> - <service android:name=".synchronization.BitmessageService" /> + <service android:name=".service.BitmessageService" /> + <service android:name=".service.ProofOfWorkService" /> <!-- Synchronization --> <provider diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index e3b469f..b8a359f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -40,22 +40,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.ArrayList; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; +import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; -import ch.dissem.apps.abit.synchronization.BitmessageService; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; -import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_IDENTITY; -import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_START_NODE; -import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_STOP_NODE; +import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_IDENTITY; +import static ch.dissem.apps.abit.service.BitmessageService.MSG_START_NODE; +import static ch.dissem.apps.abit.service.BitmessageService.MSG_STOP_NODE; import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; @@ -82,7 +83,7 @@ public class MessageListActivity extends AppCompatActivity public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class); - private static final long SYNC_FREQUENCY = 15;// FIXME * 60; // seconds + private static final long SYNC_FREQUENCY = 15 * 60; // seconds private static final int ADD_IDENTITY = 1; /** @@ -91,14 +92,15 @@ public class MessageListActivity extends AppCompatActivity */ private boolean twoPane; - private Messenger messenger = new Messenger(new IncomingHandler()); - private Messenger service; - private boolean bound; - private ServiceConnection connection = new ServiceConnection() { + private static IncomingHandler incomingHandler = new IncomingHandler(); + private static Messenger messenger = new Messenger(incomingHandler); + private static Messenger service; + private static boolean bound; + private static ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - MessageListActivity.this.service = new Messenger(service); - MessageListActivity.this.bound = true; + MessageListActivity.service = new Messenger(service); + MessageListActivity.bound = true; } @Override @@ -108,7 +110,6 @@ public class MessageListActivity extends AppCompatActivity } }; - private AccountHeader accountHeader; private Label selectedLabel; private MessageRepository messageRepo; @@ -208,7 +209,7 @@ public class MessageListActivity extends AppCompatActivity .withIcon(GoogleMaterial.Icon.gmd_settings) ); // Create the AccountHeader - accountHeader = new AccountHeaderBuilder() + AccountHeader accountHeader = new AccountHeaderBuilder() .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) @@ -229,6 +230,7 @@ public class MessageListActivity extends AppCompatActivity } }) .build(); + incomingHandler.updateAccountHeader(accountHeader); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); for (Label label : messageRepo.getLabels()) { @@ -414,11 +416,24 @@ public class MessageListActivity extends AppCompatActivity super.onStop(); } - private class IncomingHandler extends Handler { + private static class IncomingHandler extends Handler { + private WeakReference<AccountHeader> accountHeaderRef; + + private IncomingHandler() { + accountHeaderRef = new WeakReference<>(null); + } + + public void updateAccountHeader(AccountHeader accountHeader){ + accountHeaderRef = new WeakReference<>(accountHeader); + } + @Override public void handleMessage(Message msg) { switch (msg.what) { case BitmessageService.MSG_CREATE_IDENTITY: { + AccountHeader accountHeader = accountHeaderRef.get(); + if (accountHeader == null) break; + Serializable data = msg.getData().getSerializable(DATA_FIELD_IDENTITY); if (data instanceof BitmessageAddress) { BitmessageAddress identity = (BitmessageAddress) data; diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index 8f87f8a..17ffe41 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -37,13 +37,13 @@ import android.widget.TextView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ch.dissem.apps.abit.synchronization.BitmessageService; +import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.bitmessage.entity.BitmessageAddress; -import static ch.dissem.apps.abit.synchronization.BitmessageService.DATA_FIELD_ADDRESS; -import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_ADD_CONTACT; -import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_SUBSCRIBE; -import static ch.dissem.apps.abit.synchronization.BitmessageService.MSG_SUBSCRIBE_AND_ADD_CONTACT; +import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_ADDRESS; +import static ch.dissem.apps.abit.service.BitmessageService.MSG_ADD_CONTACT; +import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE; +import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE_AND_ADD_CONTACT; public class OpenBitmessageLinkActivity extends AppCompatActivity { private static final Logger LOG = LoggerFactory.getLogger(OpenBitmessageLinkActivity.class); diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java index 60eb282..21ff505 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java @@ -14,7 +14,7 @@ public abstract class AbstractNotification { public AbstractNotification(Context ctx) { - this.ctx = ctx; + this.ctx = ctx.getApplicationContext(); this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); } @@ -23,6 +23,10 @@ public abstract class AbstractNotification { */ protected abstract int getNotificationId(); + public Notification getNotification() { + return notification; + } + public void show() { manager.notify(getNotificationId(), notification); } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java index d14aa78..c64185e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java @@ -1,6 +1,7 @@ package ch.dissem.apps.abit.notification; import android.content.Context; +import android.support.annotation.StringRes; import android.support.v7.app.NotificationCompat; import ch.dissem.apps.abit.R; @@ -20,14 +21,14 @@ public class ErrorNotification extends AbstractNotification { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } - public ErrorNotification setWarning(int resId, Object... args) { + public ErrorNotification setWarning(@StringRes int resId, Object... args) { builder.setSmallIcon(R.drawable.ic_notification_warning) .setContentText(ctx.getString(resId, args)); notification = builder.build(); return this; } - public ErrorNotification setError(int resId, Object... args) { + public ErrorNotification setError(@StringRes int resId, Object... args) { builder.setSmallIcon(R.drawable.ic_notification_error) .setContentText(ctx.getString(resId, args)); notification = builder.build(); diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 3530c2a..bff6e92 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -12,7 +12,6 @@ import java.util.TimerTask; import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.utils.Property; @@ -26,7 +25,7 @@ public class NetworkNotification extends AbstractNotification { private NotificationCompat.Builder builder; public NetworkNotification(Context ctx, BitmessageContext bmc) { - super(ctx.getApplicationContext()); + super(ctx); this.bmc = bmc; builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_full_node) @@ -34,6 +33,7 @@ public class NetworkNotification extends AbstractNotification { .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } + @Override public Notification getNotification() { update(); return notification; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java new file mode 100644 index 0000000..f917a83 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java @@ -0,0 +1,38 @@ +package ch.dissem.apps.abit.notification; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v7.app.NotificationCompat; + +import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.R; + +/** + * Ongoing notification while proof of work is in progress. + */ +public class ProofOfWorkNotification extends AbstractNotification { + public static final int ONGOING_NOTIFICATION_ID = 3; + + public ProofOfWorkNotification(Context ctx) { + super(ctx); + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); + + Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setUsesChronometer(true) + .setSmallIcon(R.drawable.ic_notification_proof_of_work) + .setContentTitle(ctx.getString(R.string.proof_of_work_title)) + .setContentText(ctx.getString(R.string.proof_of_work_text)) + .setContentIntent(pendingIntent); + + notification = builder.build(); + } + + @Override + protected int getNotificationId() { + return ONGOING_NOTIFICATION_ID; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java similarity index 67% rename from app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java rename to app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index bb15042..e52eb95 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -1,25 +1,21 @@ -package ch.dissem.apps.abit.synchronization; +package ch.dissem.apps.abit.service; import android.app.Service; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.lang.ref.WeakReference; import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -33,7 +29,6 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIF public class BitmessageService extends Service { public static final Logger LOG = LoggerFactory.getLogger(BitmessageService.class); - public static final int MSG_SYNC = 2; public static final int MSG_CREATE_IDENTITY = 10; public static final int MSG_SUBSCRIBE = 20; public static final int MSG_ADD_CONTACT = 21; @@ -68,7 +63,7 @@ public class BitmessageService extends Service { if (bmc == null) { bmc = Singleton.getBitmessageContext(this); notification = new NetworkNotification(this, bmc); - messenger = new Messenger(new IncomingHandler()); + messenger = new Messenger(new IncomingHandler(this)); } } } @@ -93,7 +88,13 @@ public class BitmessageService extends Service { return messenger.getBinder(); } - private class IncomingHandler extends Handler { + private static class IncomingHandler extends Handler { + private WeakReference<BitmessageService> service; + + private IncomingHandler(BitmessageService service) { + this.service = new WeakReference<>(service); + } + @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -119,45 +120,6 @@ public class BitmessageService extends Service { } break; } - case MSG_SYNC: { - LOG.info("Synchronizing Bitmessage"); - // If the Bitmessage context acts as a full node, synchronization isn't necessary - if (bmc.isRunning()) break; - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( - BitmessageService.this); - - String trustedNode = preferences.getString("trusted_node", null); - if (trustedNode == null) break; - trustedNode = trustedNode.trim(); - if (trustedNode.isEmpty()) break; - - int port; - if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { - int index = trustedNode.lastIndexOf(':'); - String portString = trustedNode.substring(index + 1); - trustedNode = trustedNode.substring(0, index); - try { - port = Integer.parseInt(portString); - } catch (NumberFormatException e) { - LOG.error("Invalid port " + portString); - // TODO: show error as notification - return; - } - } else { - port = 8444; - } - long timeoutInSeconds = preferences.getInt("sync_timeout", 120); - try { - LOG.info("Synchronization started"); - bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); - LOG.info("Synchronization finished"); - } catch (UnknownHostException e) { - LOG.error("Couldn't synchronize", e); - // TODO: show error as notification - } - break; - } case MSG_SEND_MESSAGE: { Serializable identity = msg.getData().getSerializable(DATA_FIELD_IDENTITY); Serializable address = msg.getData().getSerializable(DATA_FIELD_ADDRESS); @@ -182,17 +144,17 @@ public class BitmessageService extends Service { case MSG_START_NODE: // TODO: warn user, option to restrict to WiFi // (I'm not quite sure this can be done here, though) - startService(new Intent(BitmessageService.this, BitmessageService.class)); + service.get().startService(new Intent(service.get(), BitmessageService.class)); running = true; - startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); + service.get().startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); bmc.startup(); notification.show(); break; case MSG_STOP_NODE: bmc.shutdown(); running = false; - stopForeground(false); - stopService(new Intent(BitmessageService.this, BitmessageService.class)); + service.get().stopForeground(false); + service.get().stopSelf(); break; default: super.handleMessage(msg); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java new file mode 100644 index 0000000..7d86ef8 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -0,0 +1,88 @@ +package ch.dissem.apps.abit.service; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.ref.WeakReference; + +import ch.dissem.apps.abit.notification.ProofOfWorkNotification; +import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; + +import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_NOTIFICATION_ID; + +/** + * The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be + * killed by the system before the nonce is found. + */ +public class ProofOfWorkService extends Service { + public static final Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class); + + // Object to use as a thread-safe lock + private static final Object lock = new Object(); + private static ProofOfWorkEngine engine; + + @Override + public void onCreate() { + synchronized (lock) { + if (engine == null) { + engine = new MultiThreadedPOWEngine(); + } + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return new PowBinder(engine, this); + } + + public static class PowBinder extends Binder { + private final ProofOfWorkEngine engine; + + private PowBinder(ProofOfWorkEngine engine, ProofOfWorkService service) { + this.engine = new EngineWrapper(engine, service); + } + + public ProofOfWorkEngine getEngine() { + return engine; + } + } + + private static class EngineWrapper implements ProofOfWorkEngine { + private final ProofOfWorkNotification notification; + private final ProofOfWorkEngine engine; + private final WeakReference<ProofOfWorkService> serviceRef; + + private EngineWrapper(ProofOfWorkEngine engine, ProofOfWorkService service) { + this.engine = engine; + this.serviceRef = new WeakReference<>(service); + this.notification = new ProofOfWorkNotification(service); + } + + @Override + public void calculateNonce(byte[] initialHash, byte[] target, final Callback callback) { + final ProofOfWorkService service = serviceRef.get(); + service.startService(new Intent(service, ProofOfWorkService.class)); + service.startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); + engine.calculateNonce(initialHash, target, new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] nonce) { + try { + callback.onNonceCalculated(nonce); + } finally { + service.stopForeground(true); + service.stopSelf(); + } + } + }); + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java new file mode 100644 index 0000000..16d2aaa --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -0,0 +1,61 @@ +package ch.dissem.apps.abit.service; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import java.util.concurrent.Semaphore; + +import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; + +import static android.content.Context.BIND_AUTO_CREATE; + +/** + * Proof of Work engine that uses the Proof of Work service. + */ +public class ServicePowEngine implements ProofOfWorkEngine, ProofOfWorkEngine.Callback { + private final Semaphore semaphore = new Semaphore(1, true); + private final Context ctx; + + private byte[] initialHash, targetValue; + private Callback callback; + + public ServicePowEngine(Context ctx) { + this.ctx = ctx; + } + + private ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + ((PowBinder) service).getEngine().calculateNonce(initialHash, targetValue, ServicePowEngine.this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + semaphore.release(); + } + }; + + @Override + public void calculateNonce(byte[] initialHash, byte[] targetValue, Callback callback) { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + this.initialHash = initialHash; + this.targetValue = targetValue; + this.callback = callback; + ctx.bindService(new Intent(ctx, ProofOfWorkService.class), connection, BIND_AUTO_CREATE); + } + + @Override + public void onNonceCalculated(byte[] bytes) { + callback.onNonceCalculated(bytes); + ctx.unbindService(connection); + } + +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index f00d3c7..a178519 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,9 +2,6 @@ package ch.dissem.apps.abit.service; import android.content.Context; -import java.util.Objects; - -import ch.dissem.apps.abit.MessageListActivity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; @@ -15,7 +12,6 @@ import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.ports.Security; import ch.dissem.bitmessage.security.sc.SpongySecurity; /** @@ -33,6 +29,7 @@ public class Singleton { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); bitmessageContext = new BitmessageContext.Builder() + .proofOfWorkEngine(new ServicePowEngine(ctx)) .security(new SpongySecurity()) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 4bbc627..7654833 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.UnknownHostException; +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.notification.ErrorNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; @@ -59,8 +61,9 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { try { port = Integer.parseInt(portString); } catch (NumberFormatException e) { - LOG.error("Invalid port " + portString); - // TODO: show error as notification + new ErrorNotification(getContext()) + .setError(R.string.error_invalid_sync_port, portString) + .show(); return; } } else { @@ -72,8 +75,11 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); LOG.info("Synchronization finished"); } catch (UnknownHostException e) { - LOG.error("Couldn't synchronize", e); - // TODO: show error as notification + new ErrorNotification(getContext()) + .setError(R.string.error_invalid_sync_host) + .show(); + } catch (RuntimeException e) { + LOG.error(e.getMessage(), e); } } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 383adde..7020bf7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,4 +42,8 @@ <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> <string name="connection_info_disconnected">Getrennt</string> <string name="connection_info_pending">Verbindung wird aufgebaut…</string> + <string name="proof_of_work_text">Warnung: dies könnte das Gerät erwärmen bis die Batterie leer ist.</string> + <string name="proof_of_work_title">Proof of Work</string> + <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> + <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b493dc2..ed1cd40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,4 +42,8 @@ <string name="send">Send</string> <string name="connection_info_disconnected">Disconnected</string> <string name="connection_info_pending">Connecting…</string> + <string name="proof_of_work_title">Proof of Work</string> + <string name="proof_of_work_text">Warning: This might heat your device until the battery\'s dead.</string> + <string name="error_invalid_sync_port">Invalid port in synchronization settings: %s</string> + <string name="error_invalid_sync_host">Synchronization failed: Trusted node could not be reached.</string> </resources> From 90400269655dced1fdde12f39ee0dfbed1501b1d Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 11 Nov 2015 21:03:03 +0100 Subject: [PATCH 018/110] Select contact and send message --- .../apps/abit/ComposeMessageFragment.java | 71 ++++++++-- .../dissem/apps/abit/MessageListActivity.java | 67 ++++++---- .../dissem/apps/abit/MessageListFragment.java | 4 +- .../apps/abit/SubscriptionDetailFragment.java | 2 +- .../apps/abit/SubscriptionListFragment.java | 2 +- .../apps/abit/adapter/ContactAdapter.java | 124 ++++++++++++++++++ .../apps/abit/service/BitmessageService.java | 5 + app/src/main/res/layout/contact_row.xml | 65 +++++++++ .../res/layout/fragment_compose_message.xml | 66 +++++----- ...detail.xml => fragment_contact_detail.xml} | 2 +- ...ribtions.xml => fragment_contact_list.xml} | 0 app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 13 files changed, 341 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java create mode 100644 app/src/main/res/layout/contact_row.xml rename app/src/main/res/layout/{fragment_subscription_detail.xml => fragment_contact_detail.xml} (98%) rename app/src/main/res/layout/{fragment_subscribtions.xml => fragment_contact_list.xml} (100%) diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 88145fb..e262d29 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -2,11 +2,21 @@ package ch.dissem.apps.abit; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; import android.widget.EditText; -import android.widget.Toast; -import ch.dissem.bitmessage.BitmessageContext; + +import java.util.List; + +import ch.dissem.apps.abit.adapter.ContactAdapter; +import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; @@ -16,9 +26,11 @@ import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; * Compose a new message. */ public class ComposeMessageFragment extends Fragment { - private BitmessageContext bmCtx; private BitmessageAddress identity; private BitmessageAddress recipient; + private AutoCompleteTextView recipientInput; + private EditText subjectInput; + private EditText bodyInput; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -33,10 +45,14 @@ public class ComposeMessageFragment extends Fragment { if (getArguments() != null) { if (getArguments().containsKey(EXTRA_IDENTITY)) { identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY); + } else { + throw new RuntimeException("No identity set for ComposeMessageFragment"); } if (getArguments().containsKey(EXTRA_RECIPIENT)) { recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT); } + } else { + throw new RuntimeException("No identity set for ComposeMessageFragment"); } setHasOptionsMenu(true); } @@ -45,13 +61,32 @@ public class ComposeMessageFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false); + recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient); + final ContactAdapter adapter = new ContactAdapter(getContext()); + recipientInput.setAdapter(adapter); + recipientInput.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + recipient = adapter.getItem(position); + } + }); + recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + recipient = adapter.getItem(position); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); if (recipient != null) { - EditText recipientInput = (EditText) rootView.findViewById(R.id.recipient); recipientInput.setText(recipient.toString()); } - EditText body = (EditText) rootView.findViewById(R.id.body); - body.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); - body.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); + subjectInput = (EditText) rootView.findViewById(R.id.subject); + bodyInput = (EditText) rootView.findViewById(R.id.body); +// bodyInput.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); +// bodyInput.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); return rootView; } @@ -65,7 +100,25 @@ public class ComposeMessageFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.send: - Toast.makeText(getActivity(), "TODO: Send", Toast.LENGTH_SHORT).show(); + String inputString = recipientInput.getText().toString(); + if (recipient == null || !recipient.toString().equals(inputString)) { + try { + recipient = new BitmessageAddress(inputString); + } catch (Exception e) { + List<BitmessageAddress> contacts = Singleton.getAddressRepository(getContext()).getContacts(); + for (BitmessageAddress contact : contacts) { + if (inputString.equalsIgnoreCase(contact.getAlias())) { + recipient = contact; + if (inputString.equals(contact.getAlias())) + break; + } + } + } + } + Singleton.getBitmessageContext(getContext()).send(identity, recipient, + subjectInput.getText().toString(), + bodyInput.getText().toString()); + getActivity().finish(); return true; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java index b8a359f..b21acb1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -114,6 +114,7 @@ public class MessageListActivity extends AppCompatActivity private MessageRepository messageRepo; private AddressRepository addressRepo; + private BitmessageAddress selectedIdentity; @Override protected void onCreate(Bundle savedInstanceState) { @@ -224,38 +225,50 @@ public class MessageListActivity extends AppCompatActivity } catch (RemoteException e) { LOG.error(e.getMessage(), e); } + } else if (profile instanceof ProfileDrawerItem) { + Object tag = ((ProfileDrawerItem) profile).getTag(); + if (tag instanceof BitmessageAddress) { + selectedIdentity = (BitmessageAddress) tag; + } } // false if it should close the drawer return false; } }) .build(); + if (profiles.size() > 0) { + accountHeader.setActiveProfile(profiles.get(0), true); + } incomingHandler.updateAccountHeader(accountHeader); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); for (Label label : messageRepo.getLabels()) { PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag(label); - switch (label.getType()) { - case INBOX: - item.withIcon(GoogleMaterial.Icon.gmd_inbox); - break; - case DRAFT: - item.withIcon(CommunityMaterial.Icon.cmd_file); - break; - case SENT: - item.withIcon(CommunityMaterial.Icon.cmd_send); - break; - case BROADCAST: - item.withIcon(CommunityMaterial.Icon.cmd_rss); - break; - case UNREAD: - item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); - break; - case TRASH: - item.withIcon(GoogleMaterial.Icon.gmd_delete); - break; - default: - item.withIcon(CommunityMaterial.Icon.cmd_label); + if (label.getType() == null) { + item.withIcon(CommunityMaterial.Icon.cmd_label); + } else { + switch (label.getType()) { + case INBOX: + item.withIcon(GoogleMaterial.Icon.gmd_inbox); + break; + case DRAFT: + item.withIcon(CommunityMaterial.Icon.cmd_file); + break; + case SENT: + item.withIcon(CommunityMaterial.Icon.cmd_send); + break; + case BROADCAST: + item.withIcon(CommunityMaterial.Icon.cmd_rss); + break; + case UNREAD: + item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); + break; + case TRASH: + item.withIcon(GoogleMaterial.Icon.gmd_delete); + break; + default: + item.withIcon(CommunityMaterial.Icon.cmd_label); + } } drawerItems.add(item); } @@ -273,8 +286,8 @@ public class MessageListActivity extends AppCompatActivity .withDrawerItems(drawerItems) .addStickyDrawerItems( new PrimaryDrawerItem() - .withName(R.string.subscriptions) - .withIcon(CommunityMaterial.Icon.cmd_rss_box), + .withName(R.string.contacts_and_subscriptions) + .withIcon(GoogleMaterial.Icon.gmd_contacts), new PrimaryDrawerItem() .withName(R.string.settings) .withIcon(GoogleMaterial.Icon.gmd_settings), @@ -313,7 +326,7 @@ public class MessageListActivity extends AppCompatActivity } else if (item instanceof Nameable<?>) { Nameable<?> ni = (Nameable<?>) item; switch (ni.getNameRes()) { - case R.string.subscriptions: + case R.string.contacts_and_subscriptions: if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof SubscriptionListFragment)) { changeList(new SubscriptionListFragment()); } else { @@ -416,6 +429,10 @@ public class MessageListActivity extends AppCompatActivity super.onStop(); } + public BitmessageAddress getSelectedIdentity() { + return selectedIdentity; + } + private static class IncomingHandler extends Handler { private WeakReference<AccountHeader> accountHeaderRef; @@ -423,7 +440,7 @@ public class MessageListActivity extends AppCompatActivity accountHeaderRef = new WeakReference<>(null); } - public void updateAccountHeader(AccountHeader accountHeader){ + public void updateAccountHeader(AccountHeader accountHeader) { accountHeaderRef = new WeakReference<>(accountHeader); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 802b901..9e6d1db 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -113,7 +113,9 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - startActivity(new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class)); + Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, ((MessageListActivity)getActivity()).getSelectedIdentity()); + startActivity(intent); } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java index 0fa31e8..50d7c76 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java @@ -74,7 +74,7 @@ public class SubscriptionDetailFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_subscription_detail, container, false); + View rootView = inflater.inflate(R.layout.fragment_contact_detail, container, false); // Show the dummy content as text in a TextView. if (item != null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java index 18fa320..4da7c4e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java @@ -103,7 +103,7 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_subscribtions, container, false); + View rootView = inflater.inflate(R.layout.fragment_contact_list, container, false); return rootView; } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java new file mode 100644 index 0000000..2d0946e --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java @@ -0,0 +1,124 @@ +package ch.dissem.apps.abit.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import ch.dissem.apps.abit.Identicon; +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.entity.BitmessageAddress; + +/** + * An adapter for contacts. Can be filtered by alias or address. + */ +public class ContactAdapter extends BaseAdapter implements Filterable { + private final LayoutInflater inflater; + private final List<BitmessageAddress> originalData; + private List<BitmessageAddress> data; + + public ContactAdapter(Context ctx) { + inflater = LayoutInflater.from(ctx); + originalData = Singleton.getAddressRepository(ctx).getContacts(); + data = originalData; + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public BitmessageAddress getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.contact_row, parent, false); + } + BitmessageAddress item = getItem(position); + ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); + ((TextView) convertView.findViewById(R.id.name)).setText(item.toString()); + ((TextView) convertView.findViewById(R.id.address)).setText(item.getAddress()); + return convertView; + } + + @Override + public Filter getFilter() { + return new ContactFilter(); + } + + private class ContactFilter extends Filter { + @Override + protected FilterResults performFiltering(CharSequence prefix) { + FilterResults results = new FilterResults(); + + if (prefix == null || prefix.length() == 0) { + results.values = originalData; + results.count = originalData.size(); + } else { + String prefixString = prefix.toString().toLowerCase(); + + final ArrayList<BitmessageAddress> newValues = new ArrayList<>(); + + for (int i = 0; i < originalData.size(); i++) { + final BitmessageAddress value = originalData.get(i); + + // First match against the whole, non-splitted value + if (value.getAlias() != null) { + String alias = value.getAlias().toLowerCase(); + if (alias.startsWith(prefixString)) { + newValues.add(value); + } else { + final String[] words = alias.split(" "); + + for (String word : words) { + if (word.startsWith(prefixString)) { + newValues.add(value); + break; + } + } + } + } else { + String address = value.getAddress().toLowerCase(); + if (address.contains(prefixString)) { + newValues.add(value); + } + } + } + + results.values = newValues; + results.count = newValues.size(); + } + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + //noinspection unchecked + data = (List<BitmessageAddress>) results.values; + if (results.count > 0) { + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index e52eb95..55f54d5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -1,6 +1,7 @@ package ch.dissem.apps.abit.service; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -8,6 +9,7 @@ import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; +import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -129,6 +131,9 @@ public class BitmessageService extends Service { String message = msg.getData().getString(DATA_FIELD_MESSAGE); bmc.send((BitmessageAddress) identity, (BitmessageAddress) address, subject, message); + } else { + Context ctx = service.get(); + Toast.makeText(ctx, "Could not send", Toast.LENGTH_LONG); } break; } diff --git a/app/src/main/res/layout/contact_row.xml b/app/src/main/res/layout/contact_row.xml new file mode 100644 index 0000000..b71f67e --- /dev/null +++ b/app/src/main/res/layout/contact_row.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2015 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:src="@color/accent" + android:layout_margin="16dp"/> + + <TextView + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Name" + android:lines="1" + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_alignTop="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:paddingTop="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingBottom="0dp" + android:textStyle="bold" + /> + + <TextView + android:id="@+id/address" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="BM-2cW0000000000000000000000000000000" + android:lines="1" + android:ellipsize="marquee" + android:textAppearance="?android:attr/textAppearanceSmall" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:layout_alignBottom="@+id/avatar" + android:layout_toRightOf="@+id/avatar" + android:layout_toEndOf="@+id/avatar"/> + +</RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_compose_message.xml b/app/src/main/res/layout/fragment_compose_message.xml index e3e0887..72c23c7 100644 --- a/app/src/main/res/layout/fragment_compose_message.xml +++ b/app/src/main/res/layout/fragment_compose_message.xml @@ -1,51 +1,47 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + android:orientation="vertical"> <android.support.design.widget.TextInputLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="4dp"> + + <AutoCompleteTextView + android:id="@+id/recipient" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="4dp"> - - <EditText - android:id="@+id/recipient" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textNoSuggestions" - android:singleLine="true" - android:hint="@string/to"/> + android:hint="@string/to" + android:inputType="textNoSuggestions" + android:singleLine="true" /> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout - android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_width="match_parent" + android:layout_height="wrap_content"> <EditText - android:id="@+id/subject" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textEmailSubject" - android:textAppearance="?android:attr/textAppearanceLarge" - android:hint="@string/subject"/> + android:id="@+id/subject" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/subject" + android:inputType="textEmailSubject" + android:textAppearance="?android:attr/textAppearanceLarge" /> </android.support.design.widget.TextInputLayout> - <ScrollView - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1"> - - <EditText - android:id="@+id/body" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="textMultiLine"/> - - </ScrollView> + <EditText + android:id="@+id/body" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:hint="@string/compose_body_hint" + android:inputType="textMultiLine|textCapSentences" + android:gravity="top" + android:isScrollContainer="true" /> </LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscription_detail.xml b/app/src/main/res/layout/fragment_contact_detail.xml similarity index 98% rename from app/src/main/res/layout/fragment_subscription_detail.xml rename to app/src/main/res/layout/fragment_contact_detail.xml index 16c3c9b..79bb54a 100644 --- a/app/src/main/res/layout/fragment_subscription_detail.xml +++ b/app/src/main/res/layout/fragment_contact_detail.xml @@ -70,7 +70,7 @@ <Switch android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/enabled" + android:text="@string/subscribed" android:id="@+id/active" android:paddingTop="16dp" android:paddingLeft="16dp" diff --git a/app/src/main/res/layout/fragment_subscribtions.xml b/app/src/main/res/layout/fragment_contact_list.xml similarity index 100% rename from app/src/main/res/layout/fragment_subscribtions.xml rename to app/src/main/res/layout/fragment_contact_list.xml diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7020bf7..da9a684 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -46,4 +46,7 @@ <string name="proof_of_work_title">Proof of Work</string> <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> + <string name="compose_body_hint">Nachricht schreiben</string> + <string name="contacts_and_subscriptions">Kontakte</string> + <string name="subscribed">Abonniert</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed1cd40..4ca446a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,4 +46,7 @@ <string name="proof_of_work_text">Warning: This might heat your device until the battery\'s dead.</string> <string name="error_invalid_sync_port">Invalid port in synchronization settings: %s</string> <string name="error_invalid_sync_host">Synchronization failed: Trusted node could not be reached.</string> + <string name="compose_body_hint">Write message</string> + <string name="contacts_and_subscriptions">Contacts</string> + <string name="subscribed">Subscribed</string> </resources> From 055bd39a42e63c0b9b1d2003a42defda7c16ee16 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 17 Nov 2015 22:19:11 +0100 Subject: [PATCH 019/110] Linkify URLs and Bitmessage addresses --- app/build.gradle | 10 ++--- .../apps/abit/MessageDetailFragment.java | 39 ++++++++++++++++--- .../apps/abit/OpenBitmessageLinkActivity.java | 29 ++++++-------- .../ch/dissem/apps/abit/util/Constants.java | 11 ++++++ .../layout/activity_open_bitmessage_link.xml | 28 ++----------- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 7 files changed, 66 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/Constants.java diff --git a/app/build.gradle b/app/build.gradle index be506a6..4e7094f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion "23.0.1" + buildToolsVersion "23.0.2" defaultConfig { applicationId "ch.dissem.apps.abit" @@ -22,9 +22,9 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.1.0' - compile 'com.android.support:support-v4:23.1.0' - compile 'com.android.support:design:23.1.0' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:design:23.1.1' compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' @@ -38,7 +38,7 @@ dependencies { compile 'com.mikepenz:iconics:1.6.2@aar' compile 'com.mikepenz:community-material-typeface:1.1.71@aar' } - +5 idea.module { downloadJavadoc = true downloadSources = true diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index c43ae5f..6ca9ce0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -3,21 +3,36 @@ package ch.dissem.apps.abit; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.view.*; +import android.text.util.Linkify; +import android.text.util.Linkify.TransformFilter; +import android.util.Patterns; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; + +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; -import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; -import com.mikepenz.google_material_typeface_library.GoogleMaterial; - -import java.util.Iterator; +import static android.text.util.Linkify.ALL; +import static android.text.util.Linkify.EMAIL_ADDRESSES; +import static android.text.util.Linkify.WEB_URLS; +import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; +import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; /** @@ -75,7 +90,19 @@ public class MessageDetailFragment extends Fragment { } else if (item.getType() == Plaintext.Type.BROADCAST) { ((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast); } - ((TextView) rootView.findViewById(R.id.text)).setText(item.getText()); + TextView messageBody = (TextView) rootView.findViewById(R.id.text); + messageBody.setText(item.getText()); + + Linkify.addLinks(messageBody, WEB_URLS); + Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, + new TransformFilter() { + public final String transformUrl(final Matcher match, String url) { + return match.group(); + } + }); + + messageBody.setLinksClickable(true); + messageBody.setTextIsSelectable(true); } boolean removed = false; diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index 17ffe41..732ee2e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -71,7 +71,6 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { final TextView addressView = (TextView) findViewById(R.id.address); final EditText label = (EditText) findViewById(R.id.label); - final Switch importContact = (Switch) findViewById(R.id.import_contact); final Switch subscribe = (Switch) findViewById(R.id.subscribe); Uri uri = getIntent().getData(); @@ -83,7 +82,6 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { label.setText(parameter.substring(parameter.indexOf('=') + 1).trim()); } else if (name.startsWith("action")) { parameter = parameter.toLowerCase(); - importContact.setChecked(parameter.contains("add")); subscribe.setChecked(parameter.contains("subscribe")); } } @@ -107,26 +105,21 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { bmAddress.setAlias(label.getText().toString()); final int what; - if (subscribe.isChecked() && importContact.isChecked()) + if (subscribe.isChecked()) what = MSG_SUBSCRIBE_AND_ADD_CONTACT; - else if (subscribe.isChecked()) - what = MSG_SUBSCRIBE; - else if (importContact.isChecked()) - what = MSG_ADD_CONTACT; else - what = 0; + what = MSG_ADD_CONTACT; - if (what != 0) { - try { - Message message = Message.obtain(null, what); - Bundle bundle = new Bundle(); - bundle.putSerializable(DATA_FIELD_ADDRESS, bmAddress); - message.setData(bundle); - service.send(message); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); - } + try { + Message message = Message.obtain(null, what); + Bundle bundle = new Bundle(); + bundle.putSerializable(DATA_FIELD_ADDRESS, bmAddress); + message.setData(bundle); + service.send(message); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); } + setResult(Activity.RESULT_OK); finish(); } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java new file mode 100644 index 0000000..436dd6c --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java @@ -0,0 +1,11 @@ +package ch.dissem.apps.abit.util; + +import java.util.regex.Pattern; + +/** + * Created by chrigu on 16.11.15. + */ +public class Constants { + public static final String BITMESSAGE_URL_SCHEMA = "bitmessage:"; + public static final Pattern BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b"); +} diff --git a/app/src/main/res/layout/activity_open_bitmessage_link.xml b/app/src/main/res/layout/activity_open_bitmessage_link.xml index fedbd9d..5f973d2 100644 --- a/app/src/main/res/layout/activity_open_bitmessage_link.xml +++ b/app/src/main/res/layout/activity_open_bitmessage_link.xml @@ -26,20 +26,11 @@ </android.support.design.widget.TextInputLayout> - <Switch - android:id="@+id/import_contact" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@+id/label_wrapper" - android:layout_centerHorizontal="true" - android:layout_marginTop="8dp" - android:text="@string/import_contact" /> - <Switch android:id="@+id/subscribe" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@+id/import_contact" + android:layout_below="@+id/label_wrapper" android:layout_centerHorizontal="true" android:layout_marginBottom="8dp" android:layout_marginTop="8dp" @@ -57,25 +48,14 @@ android:layout_marginTop="12dp" android:text="@string/do_import" /> - <Button - android:id="@+id/compose_message" - style="?android:attr/borderlessButtonStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignTop="@+id/do_import" - android:layout_below="@+id/subscribe" - android:layout_toLeftOf="@+id/do_import" - android:layout_toStartOf="@+id/do_import" - android:text="@string/write_message" /> - <Button android:id="@+id/cancel" style="?android:attr/borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/compose_message" - android:layout_toLeftOf="@+id/compose_message" - android:layout_toStartOf="@+id/compose_message" + android:layout_alignTop="@+id/do_import" + android:layout_toLeftOf="@+id/do_import" + android:layout_toStartOf="@+id/do_import" android:text="@string/cancel" /> </RelativeLayout> diff --git a/build.gradle b/build.gradle index 8eb7947..7acef02 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.4.+' + classpath 'com.android.tools.build:gradle:1.5.0-beta1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 179dd79..07fc193 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip From 73383aa2c2035ba8c5b4e75cbda58f25352a8101 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 22 Nov 2015 12:28:31 +0100 Subject: [PATCH 020/110] Renamed MessageListActivity to MainActivity (it is the main activity, after all) --- app/src/main/AndroidManifest.xml | 16 ++++++------- ...ageListActivity.java => MainActivity.java} | 24 +++++++++++-------- .../apps/abit/MessageDetailActivity.java | 5 ++-- .../apps/abit/MessageDetailFragment.java | 6 +---- .../dissem/apps/abit/MessageListFragment.java | 10 ++++---- .../apps/abit/SubscriptionDetailActivity.java | 4 ++-- .../apps/abit/SubscriptionDetailFragment.java | 2 +- .../apps/abit/listener/MessageListener.java | 12 ---------- .../notification/NetworkNotification.java | 4 ++-- .../notification/NewMessageNotification.java | 10 ++++---- .../notification/ProofOfWorkNotification.java | 4 ++-- .../layout/activity_open_bitmessage_link.xml | 16 +++++++++---- 12 files changed, 55 insertions(+), 58 deletions(-) rename app/src/main/java/ch/dissem/apps/abit/{MessageListActivity.java => MainActivity.java} (96%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0fbceb..979915d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ android:label="@string/app_name" android:theme="@style/AppTheme"> <activity - android:name=".MessageListActivity" + android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -29,28 +29,28 @@ <activity android:name=".MessageDetailActivity" android:label="@string/title_message_detail" - android:parentActivityName=".MessageListActivity" + android:parentActivityName=".MainActivity" tools:ignore="UnusedAttribute"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MessageListActivity" /> + android:value=".MainActivity" /> </activity> <activity android:name=".SubscriptionDetailActivity" android:label="@string/title_subscription_detail" - android:parentActivityName=".MessageListActivity" + android:parentActivityName=".MainActivity" tools:ignore="UnusedAttribute"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MessageListActivity" /> + android:value=".MainActivity" /> </activity> <activity android:name=".ComposeMessageActivity" android:label="Compose" - android:parentActivityName=".MessageListActivity"> + android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MessageListActivity" /> + android:value=".MainActivity" /> <intent-filter> <action android:name="android.intent.action.SENDTO" /> @@ -79,7 +79,7 @@ <activity android:name=".SettingsActivity" android:label="@string/settings" - android:parentActivityName=".MessageListActivity"> + android:parentActivityName=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java similarity index 96% rename from app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java rename to app/src/main/java/ch/dissem/apps/abit/MainActivity.java index b21acb1..6f444ad 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -42,6 +42,8 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; @@ -77,12 +79,12 @@ import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; * to listen for item selections. * </p> */ -public class MessageListActivity extends AppCompatActivity +public class MainActivity extends AppCompatActivity implements ListSelectionListener<Serializable>, ActionBarListener { public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; - private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class); + private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final long SYNC_FREQUENCY = 15 * 60; // seconds private static final int ADD_IDENTITY = 1; @@ -99,8 +101,8 @@ public class MessageListActivity extends AppCompatActivity private static ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - MessageListActivity.service = new Messenger(service); - MessageListActivity.bound = true; + MainActivity.service = new Messenger(service); + MainActivity.bound = true; } @Override @@ -121,8 +123,10 @@ public class MessageListActivity extends AppCompatActivity super.onCreate(savedInstanceState); messageRepo = Singleton.getMessageRepository(this); addressRepo = Singleton.getAddressRepository(this); - - selectedLabel = messageRepo.getLabels().get(0); + List<Label> labels = messageRepo.getLabels(); + if (selectedLabel == null) { + selectedLabel = labels.get(0); + } setContentView(R.layout.activity_message_list); @@ -144,7 +148,7 @@ public class MessageListActivity extends AppCompatActivity listFragment.setActivateOnItemClick(true); } - createDrawer(toolbar); + createDrawer(toolbar, labels); Singleton.getMessageListener(this).resetNotification(); @@ -185,7 +189,7 @@ public class MessageListActivity extends AppCompatActivity } } - private void createDrawer(Toolbar toolbar) { + private void createDrawer(Toolbar toolbar, Collection<Label> labels) { final ArrayList<IProfile> profiles = new ArrayList<>(); for (BitmessageAddress identity : addressRepo.getIdentities()) { LOG.info("Adding identity " + identity.getAddress()); @@ -242,7 +246,7 @@ public class MessageListActivity extends AppCompatActivity incomingHandler.updateAccountHeader(accountHeader); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); - for (Label label : messageRepo.getLabels()) { + for (Label label : labels) { PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag(label); if (label.getType() == null) { item.withIcon(CommunityMaterial.Icon.cmd_label); @@ -335,7 +339,7 @@ public class MessageListActivity extends AppCompatActivity } break; case R.string.settings: - startActivity(new Intent(MessageListActivity.this, SettingsActivity.class)); + startActivity(new Intent(MainActivity.this, SettingsActivity.class)); break; case R.string.archive: selectedLabel = null; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java index fae5692..22012c5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java @@ -3,7 +3,6 @@ package ch.dissem.apps.abit; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.NavUtils; -import android.support.v7.app.ActionBarActivity; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.MenuItem; @@ -13,7 +12,7 @@ import android.view.MenuItem; * An activity representing a single Message detail screen. This * activity is only used on handset devices. On tablet-size devices, * item details are presented side-by-side with a list of items - * in a {@link MessageListActivity}. + * in a {@link MainActivity}. * <p/> * This activity is mostly just a 'shell' activity containing nothing * more than a {@link MessageDetailFragment}. @@ -64,7 +63,7 @@ public class MessageDetailActivity extends AppCompatActivity { // // http://developer.android.com/design/patterns/navigation.html#up-vs-back // - NavUtils.navigateUpTo(this, new Intent(this, MessageListActivity.class)); + NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); return true; } return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 6ca9ce0..438e2f2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -5,7 +5,6 @@ import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.util.Linkify; import android.text.util.Linkify.TransformFilter; -import android.util.Patterns; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -19,7 +18,6 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial; import java.util.Iterator; import java.util.regex.Matcher; -import java.util.regex.Pattern; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; @@ -28,8 +26,6 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; -import static android.text.util.Linkify.ALL; -import static android.text.util.Linkify.EMAIL_ADDRESSES; import static android.text.util.Linkify.WEB_URLS; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; @@ -37,7 +33,7 @@ import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; /** * A fragment representing a single Message detail screen. - * This fragment is either contained in a {@link MessageListActivity} + * This fragment is either contained in a {@link MainActivity} * in two-pane mode (on tablets) or a {@link MessageDetailActivity} * on handsets. */ diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 9e6d1db..e844320 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,7 +1,5 @@ package ch.dissem.apps.abit; -import android.annotation.SuppressLint; -import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.os.Bundle; @@ -55,7 +53,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { public void onResume() { super.onResume(); - updateList(((MessageListActivity) getActivity()).getSelectedLabel()); + doUpdateList(((MainActivity) getActivity()).getSelectedLabel()); } @Override @@ -64,6 +62,10 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { if (!isVisible()) return; + doUpdateList(label); + } + + private void doUpdateList(Label label) { setListAdapter(new ArrayAdapter<Plaintext>( getActivity(), android.R.layout.simple_list_item_activated_1, @@ -114,7 +116,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { @Override public void onClick(View view) { Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, ((MessageListActivity)getActivity()).getSelectedIdentity()); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, ((MainActivity) getActivity()).getSelectedIdentity()); startActivity(intent); } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java index 57bf32b..f8c8faf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java @@ -28,7 +28,7 @@ import android.view.MenuItem; * An activity representing a single Subscription detail screen. This * activity is only used on handset devices. On tablet-size devices, * item details are presented side-by-side with a list of items - * in a {@link MessageListActivity}. + * in a {@link MainActivity}. * <p/> * This activity is mostly just a 'shell' activity containing nothing * more than a {@link SubscriptionDetailFragment}. @@ -79,7 +79,7 @@ public class SubscriptionDetailActivity extends AppCompatActivity { // // http://developer.android.com/design/patterns/navigation.html#up-vs-back // - NavUtils.navigateUpTo(this, new Intent(this, MessageListActivity.class)); + NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); return true; } return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java index 50d7c76..6d7c3b4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java @@ -34,7 +34,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; /** * A fragment representing a single Message detail screen. - * This fragment is either contained in a {@link MessageListActivity} + * This fragment is either contained in a {@link MainActivity} * in two-pane mode (on tablets) or a {@link MessageDetailActivity} * on handsets. */ diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index edf17b3..16acfbd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -18,25 +18,13 @@ package ch.dissem.apps.abit.listener; import android.annotation.TargetApi; import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.support.v7.app.NotificationCompat; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.style.StyleSpan; -import ch.dissem.apps.abit.Identicon; -import ch.dissem.apps.abit.MessageListActivity; -import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.notification.NewMessageNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.Plaintext; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index bff6e92..6236546 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -10,7 +10,7 @@ import android.support.v7.app.NotificationCompat; import java.util.Timer; import java.util.TimerTask; -import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.utils.Property; @@ -64,7 +64,7 @@ public class NetworkNotification extends AbstractNotification { } builder.setContentText(info); } - Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); + Intent showMessageIntent = new Intent(ctx, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showMessageIntent, 0); builder.setContentIntent(pendingIntent); notification = builder.build(); diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index 4e66ab1..efe1f8e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -13,7 +13,7 @@ import android.text.style.StyleSpan; import java.util.LinkedList; import ch.dissem.apps.abit.Identicon; -import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; import ch.dissem.bitmessage.entity.Plaintext; @@ -38,8 +38,8 @@ public class NewMessageNotification extends AbstractNotification { .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) .setContentInfo("Info"); - Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); - showMessageIntent.putExtra(MessageListActivity.EXTRA_SHOW_MESSAGE, plaintext); + Intent showMessageIntent = new Intent(ctx, MainActivity.class); + showMessageIntent.putExtra(MainActivity.EXTRA_SHOW_MESSAGE, plaintext); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); @@ -66,8 +66,8 @@ public class NewMessageNotification extends AbstractNotification { } builder.setStyle(inboxStyle); - Intent intent = new Intent(ctx, MessageListActivity.class); - intent.setAction(MessageListActivity.ACTION_SHOW_INBOX); + Intent intent = new Intent(ctx, MainActivity.class); + intent.setAction(MainActivity.ACTION_SHOW_INBOX); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0); builder.setContentIntent(pendingIntent); notification = builder.build(); diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java index f917a83..b0b33b4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java @@ -5,7 +5,7 @@ import android.content.Context; import android.content.Intent; import android.support.v7.app.NotificationCompat; -import ch.dissem.apps.abit.MessageListActivity; +import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; /** @@ -18,7 +18,7 @@ public class ProofOfWorkNotification extends AbstractNotification { super(ctx); NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); - Intent showMessageIntent = new Intent(ctx, MessageListActivity.class); + Intent showMessageIntent = new Intent(ctx, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) diff --git a/app/src/main/res/layout/activity_open_bitmessage_link.xml b/app/src/main/res/layout/activity_open_bitmessage_link.xml index 5f973d2..295dc87 100644 --- a/app/src/main/res/layout/activity_open_bitmessage_link.xml +++ b/app/src/main/res/layout/activity_open_bitmessage_link.xml @@ -1,21 +1,28 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp"> + android:padding="24dp"> <TextView android:id="@+id/address" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:text="BM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - android:textAppearance="?android:attr/textAppearanceSmall" /> + android:textSize="10dp" + tools:ignore="SpUsage" /> <android.support.design.widget.TextInputLayout android:id="@+id/label_wrapper" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@+id/address"> + android:layout_alignLeft="@+id/address" + android:layout_alignStart="@+id/address" + android:layout_below="@+id/address" + android:layout_marginTop="16dp"> <EditText android:id="@+id/label" @@ -30,8 +37,9 @@ android:id="@+id/subscribe" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_alignLeft="@+id/address" + android:layout_alignStart="@+id/address" android:layout_below="@+id/label_wrapper" - android:layout_centerHorizontal="true" android:layout_marginBottom="8dp" android:layout_marginTop="8dp" android:text="@string/subscribe" /> From b0828ec1e54ac036f6a72e9de15c1c7b6d4ee1ca Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 28 Nov 2015 20:28:28 +0100 Subject: [PATCH 021/110] Server POW (work in progress) --- .../apps/abit/adapter/AndroidSecurity.java | 84 +++++ .../dissem/apps/abit/service/Singleton.java | 3 +- .../abit/synchronization/SyncAdapter.java | 7 +- .../ch/dissem/apps/abit/util/Constants.java | 7 +- .../ch/dissem/apps/abit/util/PRNGFixes.java | 341 ++++++++++++++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 7 + 8 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java new file mode 100644 index 0000000..00020b1 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java @@ -0,0 +1,84 @@ +package ch.dissem.apps.abit.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ch.dissem.apps.abit.util.PRNGFixes; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; +import ch.dissem.bitmessage.security.sc.SpongySecurity; +import ch.dissem.bitmessage.utils.UnixTime; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; + +/** + * @author Christian Basler + */ +public class AndroidSecurity extends SpongySecurity { + private final SharedPreferences preferences; + + public AndroidSecurity(Context ctx) { + PRNGFixes.apply(); + preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + } + + @Override + public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, ProofOfWorkEngine.Callback callback) { + if (preferences.getBoolean(PREFERENCE_SERVER_POW, false)) { + object.setNonce(new byte[8]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + object.write(out); + sendAsBroadcast(getContext().getAddressRepo().getIdentities().get(0), out.toByteArray()); + if (object.getPayload() instanceof PlaintextHolder) { + Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); + plaintext.setInventoryVector(object.getInventoryVector()); + plaintext.setStatus(SENT); + plaintext.removeLabel(Label.Type.OUTBOX); + plaintext.addLabels(getContext().getMessageRepository().getLabels(Label.Type.SENT)); + getContext().getMessageRepository().save(plaintext); + + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + super.doProofOfWork(object, nonceTrialsPerByte, extraBytes, callback); + } + } + + private void sendAsBroadcast(BitmessageAddress identity, byte[] data) throws IOException { + Plaintext msg = new Plaintext.Builder(BROADCAST) + .from(identity) + .message(data) + .build(); + Broadcast payload = Factory.getBroadcast(identity, msg); + long expires = UnixTime.now(+2 * DAY); + final ObjectMessage object = new ObjectMessage.Builder() + .stream(identity.getStream()) + .expiresTime(expires) + .payload(payload) + .build(); + object.sign(identity.getPrivateKey()); + payload.encrypt(); + object.setNonce(new byte[8]); + + getContext().getInventory().storeObject(object); + getContext().getNetworkHandler().offer(object.getInventoryVector()); + // TODO: offer to the trusted node only? + // at least make sure it is offered to the trusted node! + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index a178519..943c3e5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,6 +2,7 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import ch.dissem.apps.abit.adapter.AndroidSecurity; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; @@ -30,7 +31,7 @@ public class Singleton { SqlHelper sqlHelper = new SqlHelper(ctx); bitmessageContext = new BitmessageContext.Builder() .proofOfWorkEngine(new ServicePowEngine(ctx)) - .security(new SpongySecurity()) + .security(new AndroidSecurity(ctx)) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 7654833..c1c55c5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -20,6 +20,9 @@ import ch.dissem.apps.abit.notification.ErrorNotification; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; + /** * Sync Adapter to synchronize with the Bitmessage network - fetches * new objects and then disconnects. @@ -48,7 +51,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - String trustedNode = preferences.getString("trusted_node", null); + String trustedNode = preferences.getString(PREFERENCE_TRUSTED_NODE, null); if (trustedNode == null) return; trustedNode = trustedNode.trim(); if (trustedNode.isEmpty()) return; @@ -69,7 +72,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } else { port = 8444; } - long timeoutInSeconds = Long.parseLong(preferences.getString("sync_timeout", "120")); + long timeoutInSeconds = Long.parseLong(preferences.getString(PREFERENCE_SYNC_TIMEOUT, "120")); try { LOG.info("Synchronization started"); bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java index 436dd6c..20fcbee 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java @@ -3,9 +3,14 @@ package ch.dissem.apps.abit.util; import java.util.regex.Pattern; /** - * Created by chrigu on 16.11.15. + * @author Christian Basler */ public class Constants { + public static final String PREFERENCE_WIFI_ONLY = "wifi_only"; + public static final String PREFERENCE_TRUSTED_NODE = "trusted_node"; + public static final String PREFERENCE_SYNC_TIMEOUT = "sync_timeout"; + public static final String PREFERENCE_SERVER_POW = "server_pow"; + public static final String BITMESSAGE_URL_SCHEMA = "bitmessage:"; public static final Pattern BITMESSAGE_ADDRESS_PATTERN = Pattern.compile("\\bBM-[a-zA-Z0-9]+\\b"); } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java new file mode 100644 index 0000000..a17d0a2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java @@ -0,0 +1,341 @@ +package ch.dissem.apps.abit.util; +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + */ + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * <p/> + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + * + * @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html"> + * http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a> + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** + * Hidden constructor to prevent instantiation. + */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index da9a684..19fda0d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -49,4 +49,6 @@ <string name="compose_body_hint">Nachricht schreiben</string> <string name="contacts_and_subscriptions">Kontakte</string> <string name="subscribed">Abonniert</string> + <string name="server_pow">Server POW</string> + <string name="server_pow_summary">Der vertrauenswürdige Knoten macht den Proof of Work</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ca446a..19fd4f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,4 +49,6 @@ <string name="compose_body_hint">Write message</string> <string name="contacts_and_subscriptions">Contacts</string> <string name="subscribed">Subscribed</string> + <string name="server_pow">Server POW</string> + <string name="server_pow_summary">Trusted node does proof of work</string> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d026e3f..05b6937 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -24,4 +24,11 @@ android:key="sync_timeout" android:summary="@string/sync_timeout_summary" android:title="@string/sync_timeout" /> + <SwitchPreference + android:defaultValue="false" + android:key="server_pow" + android:dependency="@string/trusted_node" + android:title="@string/server_pow" + android:summary="@string/server_pow_summary" + /> </PreferenceScreen> \ No newline at end of file From 41f4571bf6b85fec70a9262f94c1225ac49ef081 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 21 Dec 2015 15:31:48 +0100 Subject: [PATCH 022/110] Server POW should work now --- app/build.gradle | 1 + .../migration/V1.2__Create_table_message.sql | 1 + .../migration/V2.0__Update_table_message.sql | 7 + .../db/migration/V2.1__Create_table_POW.sql | 7 + .../ch/dissem/apps/abit/MainActivity.java | 74 ++++---- .../apps/abit/OpenBitmessageLinkActivity.java | 6 +- .../ch/dissem/apps/abit/SettingsActivity.java | 2 +- .../ch/dissem/apps/abit/SettingsFragment.java | 43 ++++- .../apps/abit/SubscriptionListFragment.java | 14 +- .../apps/abit/adapter/AndroidSecurity.java | 52 +----- .../adapter/SwitchingProofOfWorkEngine.java | 52 ++++++ .../dissem/apps/abit/pow/ServerPowEngine.java | 81 +++++++++ .../repository/AndroidAddressRepository.java | 40 +++-- .../abit/repository/AndroidInventory.java | 60 ++++--- .../repository/AndroidMessageRepository.java | 133 ++++++++++----- .../AndroidProofOfWorkRepository.java | 133 +++++++++++++++ .../apps/abit/repository/SqlHelper.java | 7 +- .../apps/abit/service/BitmessageService.java | 8 +- .../apps/abit/service/ProofOfWorkService.java | 4 +- .../apps/abit/service/ServicePowEngine.java | 4 +- .../dissem/apps/abit/service/Singleton.java | 45 ++++- .../abit/synchronization/Authenticator.java | 4 +- .../abit/synchronization/SyncAdapter.java | 158 +++++++++++++----- .../ch/dissem/apps/abit/util/Preferences.java | 90 ++++++++++ app/src/main/res/xml/preferences.xml | 2 +- build.gradle | 2 +- 26 files changed, 804 insertions(+), 226 deletions(-) create mode 100644 app/src/main/assets/db/migration/V2.0__Update_table_message.sql create mode 100644 app/src/main/assets/db/migration/V2.1__Create_table_POW.sql create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/Preferences.java diff --git a/app/build.gradle b/app/build.gradle index 4e7094f..01ba6f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,7 @@ dependencies { compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT' + compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' compile 'org.slf4j:slf4j-android:1.7.12' diff --git a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql index 79e32f2..ae72a6f 100644 --- a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql +++ b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql @@ -8,6 +8,7 @@ CREATE TABLE Message ( sent INTEGER, received INTEGER, status VARCHAR(20) NOT NULL, + initial_hash BINARY(64) UNIQUE, FOREIGN KEY (sender) REFERENCES Address (address), FOREIGN KEY (recipient) REFERENCES Address (address) diff --git a/app/src/main/assets/db/migration/V2.0__Update_table_message.sql b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql new file mode 100644 index 0000000..2b39a6e --- /dev/null +++ b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql @@ -0,0 +1,7 @@ +-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration +-- wasn't really necessary yet. +-- +-- This file is here to reduce confusion regarding to the original migration files. + +--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64); +--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash); \ No newline at end of file diff --git a/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql new file mode 100644 index 0000000..b39c6c5 --- /dev/null +++ b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql @@ -0,0 +1,7 @@ +CREATE TABLE POW ( + initial_hash BINARY(64) PRIMARY KEY, + data BLOB NOT NULL, + version BIGINT NOT NULL, + nonce_trials_per_byte BIGINT NOT NULL, + extra_bytes BIGINT NOT NULL +); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 6f444ad..5d7bee8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -50,6 +50,8 @@ import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.Authenticator; +import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -85,7 +87,6 @@ public class MainActivity extends AppCompatActivity public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); - private static final long SYNC_FREQUENCY = 15 * 60; // seconds private static final int ADD_IDENTITY = 1; /** @@ -134,7 +135,8 @@ public class MainActivity extends AppCompatActivity setSupportActionBar(toolbar); MessageListFragment listFragment = new MessageListFragment(); - getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment).commit(); + getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment) + .commit(); if (findViewById(R.id.message_detail_container) != null) { // The detail container view will be present only in the @@ -157,21 +159,10 @@ public class MainActivity extends AppCompatActivity onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); } - createSyncAccount(); - } - - private void createSyncAccount() { - // Create account, if it's missing. (Either first run, or user has deleted account.) - Account account = new Account(Authenticator.ACCOUNT_NAME, Authenticator.ACCOUNT_TYPE); - - if (AccountManager.get(this).addAccountExplicitly(account, null, null)) { - // Inform the system that this account supports sync - ContentResolver.setIsSyncable(account, AUTHORITY, 1); - // Inform the system that this account is eligible for auto sync when the network is up - ContentResolver.setSyncAutomatically(account, AUTHORITY, true); - // Recommend a schedule for automatic synchronization. The system may modify this based - // on other scheduled syncs and network utilization. - ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + if (Preferences.useTrustedNode(this)) { + SyncAdapter.startSync(this); + } else { + SyncAdapter.stopSync(this); } } @@ -220,10 +211,12 @@ public class MainActivity extends AppCompatActivity .withProfiles(profiles) .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { @Override - public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { + public boolean onProfileChanged(View view, IProfile profile, boolean + currentProfile) { if (profile.getIdentifier() == ADD_IDENTITY) { try { - Message message = Message.obtain(null, BitmessageService.MSG_CREATE_IDENTITY); + Message message = Message.obtain(null, BitmessageService + .MSG_CREATE_IDENTITY); message.replyTo = messenger; service.send(message); } catch (RemoteException e) { @@ -240,14 +233,15 @@ public class MainActivity extends AppCompatActivity } }) .build(); - if (profiles.size() > 0) { + if (profiles.size() > 2) { // There's always the add and manage identity items accountHeader.setActiveProfile(profiles.get(0), true); } incomingHandler.updateAccountHeader(accountHeader); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); for (Label label : labels) { - PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag(label); + PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag + (label); if (label.getType() == null) { item.withIcon(CommunityMaterial.Icon.cmd_label); } else { @@ -301,17 +295,21 @@ public class MainActivity extends AppCompatActivity .withChecked(BitmessageService.isRunning()) .withOnCheckedChangeListener(new OnCheckedChangeListener() { @Override - public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { + public void onCheckedChanged(IDrawerItem drawerItem, + CompoundButton buttonView, + boolean isChecked) { if (messenger != null) { if (isChecked) { try { - service.send(Message.obtain(null, MSG_START_NODE)); + service.send(Message.obtain(null, + MSG_START_NODE)); } catch (RemoteException e) { LOG.error(e.getMessage(), e); } } else { try { - service.send(Message.obtain(null, MSG_STOP_NODE)); + service.send(Message.obtain(null, + MSG_STOP_NODE)); } catch (RemoteException e) { LOG.error(e.getMessage(), e); } @@ -322,7 +320,8 @@ public class MainActivity extends AppCompatActivity ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override - public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem item) { + public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long + l, IDrawerItem item) { if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); showSelectedLabel(); @@ -331,15 +330,18 @@ public class MainActivity extends AppCompatActivity Nameable<?> ni = (Nameable<?>) item; switch (ni.getNameRes()) { case R.string.contacts_and_subscriptions: - if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof SubscriptionListFragment)) { + if (!(getSupportFragmentManager().findFragmentById(R.id + .item_list) instanceof SubscriptionListFragment)) { changeList(new SubscriptionListFragment()); } else { ((SubscriptionListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(); } + break; case R.string.settings: - startActivity(new Intent(MainActivity.this, SettingsActivity.class)); + startActivity(new Intent(MainActivity.this, SettingsActivity + .class)); break; case R.string.archive: selectedLabel = null; @@ -357,7 +359,8 @@ public class MainActivity extends AppCompatActivity } private void showSelectedLabel() { - if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { + if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof + MessageListFragment) { ((MessageListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(selectedLabel); } else { @@ -385,7 +388,8 @@ public class MainActivity extends AppCompatActivity else if (item instanceof BitmessageAddress) fragment = new SubscriptionDetailFragment(); else - throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was " + throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + + "was " + item.getClass().getSimpleName()); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() @@ -400,7 +404,8 @@ public class MainActivity extends AppCompatActivity else if (item instanceof BitmessageAddress) detailIntent = new Intent(this, SubscriptionDetailActivity.class); else - throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was " + throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + + "was " + item.getClass().getSimpleName()); detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); @@ -421,7 +426,8 @@ public class MainActivity extends AppCompatActivity @Override protected void onStart() { super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + bindService(new Intent(this, BitmessageService.class), connection, Context + .BIND_AUTO_CREATE); } @Override @@ -463,8 +469,10 @@ public class MainActivity extends AppCompatActivity .withEmail(identity.getAddress()) .withTag(identity); if (accountHeader.getProfiles() != null) { - //we know that there are 2 setting elements. set the new profile above them ;) - accountHeader.addProfile(newProfile, accountHeader.getProfiles().size() - 2); + //we know that there are 2 setting elements. set the new profile + // above them ;) + accountHeader.addProfile(newProfile, accountHeader.getProfiles().size + () - 2); } else { accountHeader.addProfiles(newProfile); } diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index 732ee2e..138ab77 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -43,7 +43,6 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_ADDRESS; import static ch.dissem.apps.abit.service.BitmessageService.MSG_ADD_CONTACT; import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE_AND_ADD_CONTACT; public class OpenBitmessageLinkActivity extends AppCompatActivity { private static final Logger LOG = LoggerFactory.getLogger(OpenBitmessageLinkActivity.class); @@ -106,7 +105,7 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { final int what; if (subscribe.isChecked()) - what = MSG_SUBSCRIBE_AND_ADD_CONTACT; + what = MSG_SUBSCRIBE; else what = MSG_ADD_CONTACT; @@ -155,7 +154,8 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context.BIND_AUTO_CREATE); + bindService(new Intent(this, BitmessageService.class), connection, Context + .BIND_AUTO_CREATE); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index 670e299..f7f7acb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -5,7 +5,7 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; /** - * Created by chris on 14.07.15. + * @author Christian Basler */ public class SettingsActivity extends AppCompatActivity { @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index ffba5a9..75bd0bc 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -1,13 +1,22 @@ package ch.dissem.apps.abit; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; + +import ch.dissem.apps.abit.synchronization.SyncAdapter; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; /** - * Created by chris on 14.07.15. + * @author Christian Basler */ -public class SettingsFragment extends PreferenceFragment { +public class SettingsFragment + extends PreferenceFragment + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -15,4 +24,32 @@ public class SettingsFragment extends PreferenceFragment { // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); } + + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + PreferenceManager.getDefaultSharedPreferences(ctx) + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + switch (key) { + case PREFERENCE_TRUSTED_NODE: + String node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null); + if (node != null) { + SyncAdapter.startSync(getActivity()); + } else { + SyncAdapter.stopSync(getActivity()); + } + break; + case PREFERENCE_SERVER_POW: + if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { + SyncAdapter.startPowSync(getActivity()); + } else { + SyncAdapter.stopPowSync(getActivity()); + } + break; + } + } } \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java index 4da7c4e..b84aa54 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; @@ -25,6 +26,7 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; +import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -100,12 +102,18 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag }); } + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + if (ctx instanceof ActionBarListener){ + ((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions)); + } + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_contact_list, container, false); - - return rootView; + return inflater.inflate(R.layout.fragment_contact_list, container, false); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java index 00020b1..e495ff1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java @@ -28,57 +28,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; * @author Christian Basler */ public class AndroidSecurity extends SpongySecurity { - private final SharedPreferences preferences; - - public AndroidSecurity(Context ctx) { + public AndroidSecurity() { PRNGFixes.apply(); - preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - } - - @Override - public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, ProofOfWorkEngine.Callback callback) { - if (preferences.getBoolean(PREFERENCE_SERVER_POW, false)) { - object.setNonce(new byte[8]); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - object.write(out); - sendAsBroadcast(getContext().getAddressRepo().getIdentities().get(0), out.toByteArray()); - if (object.getPayload() instanceof PlaintextHolder) { - Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext(); - plaintext.setInventoryVector(object.getInventoryVector()); - plaintext.setStatus(SENT); - plaintext.removeLabel(Label.Type.OUTBOX); - plaintext.addLabels(getContext().getMessageRepository().getLabels(Label.Type.SENT)); - getContext().getMessageRepository().save(plaintext); - - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - super.doProofOfWork(object, nonceTrialsPerByte, extraBytes, callback); - } - } - - private void sendAsBroadcast(BitmessageAddress identity, byte[] data) throws IOException { - Plaintext msg = new Plaintext.Builder(BROADCAST) - .from(identity) - .message(data) - .build(); - Broadcast payload = Factory.getBroadcast(identity, msg); - long expires = UnixTime.now(+2 * DAY); - final ObjectMessage object = new ObjectMessage.Builder() - .stream(identity.getStream()) - .expiresTime(expires) - .payload(payload) - .build(); - object.sign(identity.getPrivateKey()); - payload.encrypt(); - object.setNonce(new byte[8]); - - getContext().getInventory().storeObject(object); - getContext().getNetworkHandler().offer(object.getInventoryVector()); - // TODO: offer to the trusted node only? - // at least make sure it is offered to the trusted node! } } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java new file mode 100644 index 0000000..d129e07 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java @@ -0,0 +1,52 @@ +package ch.dissem.apps.abit.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.util.Arrays; + +import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; + +/** + * Switches between two {@link ProofOfWorkEngine}s depending on the configuration. + * + * @author Christian Basler + */ +public class SwitchingProofOfWorkEngine implements ProofOfWorkEngine, InternalContext.ContextHolder { + private final Context ctx; + private final String preference; + private final ProofOfWorkEngine option; + private final ProofOfWorkEngine fallback; + + public SwitchingProofOfWorkEngine(Context ctx, String preference, + ProofOfWorkEngine option, ProofOfWorkEngine fallback) { + this.ctx = ctx; + this.preference = preference; + this.option = option; + this.fallback = fallback; + } + + @Override + public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + if (preferences.getBoolean(preference, false)) { + option.calculateNonce(initialHash, target, callback); + } else { + fallback.calculateNonce(initialHash, target, callback); + } + } + + @Override + public void setContext(InternalContext context) { + for (ProofOfWorkEngine e : Arrays.asList(option, fallback)) { + if (e instanceof InternalContext.ContextHolder) { + ((InternalContext.ContextHolder) e).setContext(context); + } + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java new file mode 100644 index 0000000..a8b76af --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java @@ -0,0 +1,81 @@ +package ch.dissem.apps.abit.pow; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.extensions.CryptoCustomMessage; +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; +import ch.dissem.bitmessage.ports.ProofOfWorkEngine; + +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class ServerPowEngine implements ProofOfWorkEngine, InternalContext + .ContextHolder { + private static final Logger LOG = LoggerFactory.getLogger(ServerPowEngine.class); + + private final Context ctx; + private InternalContext context; + + private final ExecutorService pool; + + public ServerPowEngine(Context ctx) { + this.ctx = ctx; + pool = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(@NonNull Runnable r) { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + } + }); + } + + @Override + public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) { + pool.execute(new Runnable() { + @Override + public void run() { + BitmessageAddress identity = Singleton.getIdentity(ctx); + if (identity == null) throw new RuntimeException("No Identity for calculating POW"); + + ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, + CALCULATE, target); + SyncAdapter.startPowSync(ctx); + try { + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> + (request); + cryptoMsg.signAndEncrypt( + identity, + security().createPublicKey(identity.getPublicDecryptionKey()) + ); + context.getNetworkHandler().send( + Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), + cryptoMsg); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + }); + } + + @Override + public void setContext(InternalContext context) { + this.context = context; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 9d490df..abf123a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.repository; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; + import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V3Pubkey; @@ -27,6 +28,7 @@ import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.utils.Encode; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -118,27 +120,30 @@ public class AndroidAddressRepository implements AddressRepository { COLUMN_SUBSCRIBED }; + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, null + ); try { - SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, null - ); c.moveToFirst(); while (!c.isAfterLast()) { BitmessageAddress address; byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)); if (privateKeyBytes != null) { - PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream(privateKeyBytes)); + PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream + (privateKeyBytes)); address = new BitmessageAddress(privateKey); } else { address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))); byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY)); if (publicKeyBytes != null) { - Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(), - new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, false); + Pubkey pubkey = Factory.readPubkey(address.getVersion(), address + .getStream(), + new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, + false); if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { pubkey = new V4Pubkey((V3Pubkey) pubkey); } @@ -153,8 +158,9 @@ public class AndroidAddressRepository implements AddressRepository { } } catch (IOException e) { LOG.error(e.getMessage(), e); + } finally { + c.close(); } - return result; } @@ -173,9 +179,14 @@ public class AndroidAddressRepository implements AddressRepository { private boolean exists(BitmessageAddress address) { SQLiteDatabase db = sql.getReadableDatabase(); - Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'", null); - cursor.moveToFirst(); - return cursor.getInt(0) > 0; + Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address + .getAddress() + "'", null); + try { + cursor.moveToFirst(); + return cursor.getInt(0) > 0; + } finally { + cursor.close(); + } } private void update(BitmessageAddress address) throws IOException { @@ -194,7 +205,8 @@ public class AndroidAddressRepository implements AddressRepository { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); - int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + "'", null); + int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + + "'", null); if (update < 0) { LOG.error("Could not update address " + address); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index 5e5f1c5..56ece70 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -74,15 +74,20 @@ public class AndroidInventory implements Inventory { SQLiteDatabase db = sql.getReadableDatabase(); Cursor c = db.query( TABLE_NAME, projection, - (includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")", + (includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join + (streams) + ")", null, null, null, null ); - c.moveToFirst(); List<InventoryVector> result = new LinkedList<>(); - while (!c.isAfterLast()) { - byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); - result.add(new InventoryVector(blob)); - c.moveToNext(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); + result.add(new InventoryVector(blob)); + c.moveToNext(); + } + } finally { + c.close(); } return result; } @@ -108,15 +113,19 @@ public class AndroidInventory implements Inventory { "hash = X'" + vector + "'", null, null, null, null ); - c.moveToFirst(); - if (c.isAfterLast()) { - LOG.info("Object requested that we don't have. IV: " + vector); - return null; - } + try { + c.moveToFirst(); + if (c.isAfterLast()) { + LOG.info("Object requested that we don't have. IV: " + vector); + return null; + } - int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); - byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); - return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length); + int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); + return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length); + } finally { + c.close(); + } } @Override @@ -144,13 +153,18 @@ public class AndroidInventory implements Inventory { where.toString(), null, null, null, null ); - c.moveToFirst(); List<ObjectMessage> result = new LinkedList<>(); - while (!c.isAfterLast()) { - int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); - byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); - result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), blob.length)); - c.moveToNext(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); + result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), + blob.length)); + c.moveToNext(); + } + } finally { + c.close(); } return result; } @@ -187,7 +201,11 @@ public class AndroidInventory implements Inventory { "hash = X'" + object.getInventoryVector() + "'", null, null, null, null ); - return c.getCount() > 0; + try { + return c.getCount() > 0; + } finally { + c.close(); + } } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index eba1b0d..f33c91b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -30,6 +30,7 @@ import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +59,7 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont private static final String COLUMN_SENT = "sent"; private static final String COLUMN_RECEIVED = "received"; private static final String COLUMN_STATUS = "status"; + private static final String COLUMN_INITIAL_HASH = "initial_hash"; private static final String JOIN_TABLE_NAME = "Message_Label"; private static final String JT_COLUMN_MESSAGE = "message_id"; @@ -112,10 +114,14 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont null, null, null, LBL_COLUMN_ORDER ); - c.moveToFirst(); - while (!c.isAfterLast()) { - result.add(getLabel(c)); - c.moveToNext(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + result.add(getLabel(c)); + c.moveToNext(); + } + } finally { + c.close(); } return result; } @@ -124,27 +130,31 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE)); Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName); String text; - switch (type) { - case INBOX: - text = ctx.getString(R.string.inbox); - break; - case DRAFT: - text = ctx.getString(R.string.draft); - break; - case SENT: - text = ctx.getString(R.string.sent); - break; - case UNREAD: - text = ctx.getString(R.string.unread); - break; - case TRASH: - text = ctx.getString(R.string.trash); - break; - case BROADCAST: - text = ctx.getString(R.string.broadcasts); - break; - default: - text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + if (type == null) { + text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + } else { + switch (type) { + case INBOX: + text = ctx.getString(R.string.inbox); + break; + case DRAFT: + text = ctx.getString(R.string.draft); + break; + case SENT: + text = ctx.getString(R.string.sent); + break; + case UNREAD: + text = ctx.getString(R.string.unread); + break; + case TRASH: + text = ctx.getString(R.string.trash); + break; + case BROADCAST: + text = ctx.getString(R.string.broadcasts); + break; + default: + text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); + } } Label label = new Label( text, @@ -158,7 +168,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont public int countUnread(Label label) { String where; if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + + ") AND "; } else { where = ""; } @@ -169,13 +180,32 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))", null, null, null, null ); - return c.getColumnCount(); + try { + return c.getColumnCount(); + } finally { + c.close(); + } + } + + @Override + public Plaintext getMessage(byte[] initialHash) { + List<Plaintext> results = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); + switch (results.size()) { + case 0: + return null; + case 1: + return results.get(0); + default: + throw new RuntimeException("This shouldn't happen, found " + results.size() + + " messages, one or none was expected"); + } } @Override public List<Plaintext> findMessages(Label label) { if (label != null) { - return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); + return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label + .getId() + ")"); } else { return find("id NOT IN (SELECT message_id FROM Message_Label)"); } @@ -183,7 +213,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont @Override public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { - return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'"); + return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + + "'"); } @Override @@ -213,34 +244,41 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont COLUMN_STATUS }; + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, + COLUMN_RECEIVED + " DESC" + ); try { - SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, - COLUMN_RECEIVED + " DESC" - ); c.moveToFirst(); while (!c.isAfterLast()) { byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV)); byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA)); - Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))); - Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new ByteArrayInputStream(data)); + Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex + (COLUMN_TYPE))); + Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new + ByteArrayInputStream(data)); long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); - builder.from(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER)))); - builder.to(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT)))); + builder.from(bmc.getAddressRepository().getAddress(c.getString(c.getColumnIndex + (COLUMN_SENDER)))); + builder.to(bmc.getAddressRepository().getAddress(c.getString(c.getColumnIndex + (COLUMN_RECIPIENT)))); builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); - builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))); + builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex + (COLUMN_STATUS)))); builder.labels(findLabels(id)); result.add(builder.build()); c.moveToNext(); } } catch (IOException e) { LOG.error(e.getMessage(), e); + } finally { + c.close(); } return result; } @@ -257,12 +295,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress()); + BitmessageAddress savedAddress = bmc.getAddressRepository().getAddress(message + .getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); } - bmc.getAddressRepo().save(message.getFrom()); + bmc.getAddressRepository().save(message.getFrom()); } } @@ -295,7 +334,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont private void insert(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); - values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message + .getInventoryVector().getHash()); values.put(COLUMN_TYPE, message.getType().name()); values.put(COLUMN_SENDER, message.getFrom().getAddress()); values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); @@ -303,13 +343,15 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); + values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); long id = db.insertOrThrow(TABLE_NAME, null, values); message.setId(id); } private void update(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); - values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message + .getInventoryVector().getHash()); values.put(COLUMN_TYPE, message.getType().name()); values.put(COLUMN_SENDER, message.getFrom().getAddress()); values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); @@ -317,6 +359,7 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); + values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java new file mode 100644 index 0000000..2273df0 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -0,0 +1,133 @@ +package ch.dissem.apps.abit.repository; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabase; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Strings; + +import static ch.dissem.bitmessage.utils.Singleton.security; + +/** + * @author Christian Basler + */ +public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { + private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); + + private static final String TABLE_NAME = "POW"; + private static final String COLUMN_INITIAL_HASH = "initial_hash"; + private static final String COLUMN_DATA = "data"; + private static final String COLUMN_VERSION = "version"; + private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"; + private static final String COLUMN_EXTRA_BYTES = "extra_bytes"; + + private final SqlHelper sql; + + public AndroidProofOfWorkRepository(SqlHelper sql) { + this.sql = sql; + } + + @Override + public Item getItem(byte[] initialHash) { + // Define a projection that specifies which columns from the database + // you will actually use after this query. + String[] projection = { + COLUMN_DATA, + COLUMN_VERSION, + COLUMN_NONCE_TRIALS_PER_BYTE, + COLUMN_EXTRA_BYTES + }; + + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + "initial_hash = X'" + Strings.hex(initialHash) + "'", + null, null, null, null + ); + try { + c.moveToFirst(); + if (!c.isAfterLast()) { + int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); + return new Item( + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) + ); + } + } finally { + c.close(); + } + throw new RuntimeException("Object requested that we don't have. Initial hash: " + + Strings.hex(initialHash)); + } + + @Override + public List<byte[]> getItems() { + // Define a projection that specifies which columns from the database + // you will actually use after this query. + String[] projection = { + COLUMN_INITIAL_HASH + }; + + SQLiteDatabase db = sql.getReadableDatabase(); + Cursor c = db.query( + TABLE_NAME, projection, + null, null, null, null, null + ); + List<byte[]> result = new LinkedList<>(); + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); + result.add(initialHash); + c.moveToNext(); + } + } finally { + c.close(); + } + return result; + } + + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_INITIAL_HASH, security().getInitialHash(object)); + values.put(COLUMN_DATA, Encode.bytes(object)); + values.put(COLUMN_VERSION, object.getVersion()); + values.put(COLUMN_NONCE_TRIALS_PER_BYTE, nonceTrialsPerByte); + values.put(COLUMN_EXTRA_BYTES, extraBytes); + + db.insertOrThrow(TABLE_NAME, null, values); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + + @Override + public void removeObject(byte[] initialHash) { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, + "initial_hash = X'" + Strings.hex(initialHash) + "'", + null); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index f79bbb2..9b5083a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -26,7 +26,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 1; + public static final int DATABASE_VERSION = 2; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -38,7 +38,7 @@ public class SqlHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { - onUpgrade(db, 0, 1); + onUpgrade(db, 0, 2); } @Override @@ -48,6 +48,9 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V1.0__Create_table_inventory"); executeMigration(db, "V1.1__Create_table_address"); executeMigration(db, "V1.2__Create_table_message"); + case 1: + // executeMigration(db, "V2.0__Update_table_message"); + executeMigration(db, "V2.1__Create_table_POW"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION. } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 55f54d5..d2184fd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -34,7 +34,6 @@ public class BitmessageService extends Service { public static final int MSG_CREATE_IDENTITY = 10; public static final int MSG_SUBSCRIBE = 20; public static final int MSG_ADD_CONTACT = 21; - public static final int MSG_SUBSCRIBE_AND_ADD_CONTACT = 23; public static final int MSG_SEND_MESSAGE = 30; public static final int MSG_SEND_BROADCAST = 31; public static final int MSG_START_NODE = 100; @@ -122,6 +121,13 @@ public class BitmessageService extends Service { } break; } + case MSG_ADD_CONTACT: { + Serializable data = msg.getData().getSerializable(DATA_FIELD_ADDRESS); + if (data instanceof BitmessageAddress) { + bmc.addContact((BitmessageAddress) data); + } + break; + } case MSG_SEND_MESSAGE: { Serializable identity = msg.getData().getSerializable(DATA_FIELD_IDENTITY); Serializable address = msg.getData().getSerializable(DATA_FIELD_ADDRESS); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 7d86ef8..7cb7087 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -73,9 +73,9 @@ public class ProofOfWorkService extends Service { service.startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); engine.calculateNonce(initialHash, target, new ProofOfWorkEngine.Callback() { @Override - public void onNonceCalculated(byte[] nonce) { + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { try { - callback.onNonceCalculated(nonce); + callback.onNonceCalculated(initialHash, nonce); } finally { service.stopForeground(true); service.stopSelf(); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java index 16d2aaa..eaffbdf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -53,8 +53,8 @@ public class ServicePowEngine implements ProofOfWorkEngine, ProofOfWorkEngine.Ca } @Override - public void onNonceCalculated(byte[] bytes) { - callback.onNonceCalculated(bytes); + public void onNonceCalculated(byte[] initialHash, byte[] bytes) { + callback.onNonceCalculated(initialHash, bytes); ctx.unbindService(connection); } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 943c3e5..51d9311 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -2,18 +2,27 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import java.util.List; + import ch.dissem.apps.abit.adapter.AndroidSecurity; +import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; import ch.dissem.apps.abit.listener.MessageListener; +import ch.dissem.apps.abit.pow.ServerPowEngine; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository; import ch.dissem.apps.abit.repository.SqlHelper; +import ch.dissem.apps.abit.util.Constants; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.security.sc.SpongySecurity; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; + +import static ch.dissem.bitmessage.utils.UnixTime.DAY; /** * Provides singleton objects across the application. @@ -22,6 +31,8 @@ public class Singleton { public static final Object lock = new Object(); private static BitmessageContext bitmessageContext; private static MessageListener messageListener; + private static BitmessageAddress identity; + private static AndroidProofOfWorkRepository powRepo; public static BitmessageContext getBitmessageContext(Context context) { if (bitmessageContext == null) { @@ -29,15 +40,23 @@ public class Singleton { if (bitmessageContext == null) { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); + powRepo = new AndroidProofOfWorkRepository(sqlHelper); bitmessageContext = new BitmessageContext.Builder() - .proofOfWorkEngine(new ServicePowEngine(ctx)) - .security(new AndroidSecurity(ctx)) + .proofOfWorkEngine(new SwitchingProofOfWorkEngine( + ctx, Constants.PREFERENCE_SERVER_POW, + new ServerPowEngine(ctx), + new ServicePowEngine(ctx) + )) + .security(new AndroidSecurity()) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) + .powRepo(powRepo) .networkHandler(new DefaultNetworkHandler()) .listener(getMessageListener(ctx)) + .doNotSendPubkeyOnIdentityCreation() + .pubkeyTTL(2 * DAY) .build(); } } @@ -63,4 +82,24 @@ public class Singleton { public static AddressRepository getAddressRepository(Context ctx) { return getBitmessageContext(ctx).addresses(); } + + public static ProofOfWorkRepository getProofOfWorkRepository(Context ctx) { + if (powRepo == null) getBitmessageContext(ctx); + return powRepo; + } + + public static BitmessageAddress getIdentity(Context ctx) { + if (identity == null) { + synchronized (Singleton.class) { + if (identity == null) { + List<BitmessageAddress> identities = getBitmessageContext(ctx).addresses() + .getIdentities(); + if (identities.size() > 0) { + identity = identities.get(0); + } + } + } + } + return identity; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java index 6d4eec5..009b35d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java @@ -12,8 +12,8 @@ import android.os.Bundle; * of its methods */ public class Authenticator extends AbstractAccountAuthenticator { - public static final String ACCOUNT_NAME = "Bitmessage"; - public static final String ACCOUNT_TYPE = "ch.dissem.bitmessage"; + public static final Account ACCOUNT_SYNC = new Account("Bitmessage", "ch.dissem.bitmessage"); + public static final Account ACCOUNT_POW = new Account("Proof of Work ", "ch.dissem.bitmessage"); // Simple constructor public Authenticator(Context context) { diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index c1c55c5..682e165 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -1,27 +1,34 @@ package ch.dissem.apps.abit.synchronization; import android.accounts.Account; +import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; -import android.content.SharedPreferences; import android.content.SyncResult; import android.os.Bundle; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.util.List; -import ch.dissem.apps.abit.R; -import ch.dissem.apps.abit.notification.ErrorNotification; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.extensions.CryptoCustomMessage; +import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; +import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_POW; +import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_SYNC; +import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; +import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE; +import static ch.dissem.bitmessage.utils.Singleton.security; /** * Sync Adapter to synchronize with the Bitmessage network - fetches @@ -30,6 +37,8 @@ import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; public class SyncAdapter extends AbstractThreadedSyncAdapter { private final static Logger LOG = LoggerFactory.getLogger(SyncAdapter.class); + private static final long SYNC_FREQUENCY = 15 * 60; // seconds + private final BitmessageContext bmc; /** @@ -41,7 +50,17 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } @Override - public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + if (account.equals(Authenticator.ACCOUNT_SYNC)) + syncData(); + else if (account.equals(Authenticator.ACCOUNT_POW)) + syncPOW(); + else + throw new RuntimeException("Unknown " + account); + } + + private void syncData() { // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) { LOG.info("Synchronization skipped, Abit is acting as a full node"); @@ -49,40 +68,103 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } LOG.info("Synchronizing Bitmessage"); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - - String trustedNode = preferences.getString(PREFERENCE_TRUSTED_NODE, null); - if (trustedNode == null) return; - trustedNode = trustedNode.trim(); - if (trustedNode.isEmpty()) return; - - int port; - if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { - int index = trustedNode.lastIndexOf(':'); - String portString = trustedNode.substring(index + 1); - trustedNode = trustedNode.substring(0, index); - try { - port = Integer.parseInt(portString); - } catch (NumberFormatException e) { - new ErrorNotification(getContext()) - .setError(R.string.error_invalid_sync_port, portString) - .show(); - return; - } - } else { - port = 8444; - } - long timeoutInSeconds = Long.parseLong(preferences.getString(PREFERENCE_SYNC_TIMEOUT, "120")); try { LOG.info("Synchronization started"); - bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, true); + bmc.synchronize( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + Preferences.getTimeoutInSeconds(getContext()), + true); LOG.info("Synchronization finished"); - } catch (UnknownHostException e) { - new ErrorNotification(getContext()) - .setError(R.string.error_invalid_sync_host) - .show(); } catch (RuntimeException e) { LOG.error(e.getMessage(), e); } } + + private void syncPOW() { + // If the Bitmessage context acts as a full node, synchronization isn't necessary + LOG.info("Looking for completed POW"); + + try { + BitmessageAddress identity = Singleton.getIdentity(getContext()); + byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); + byte[] signingKey = security().createPublicKey(identity.getPublicDecryptionKey()); + ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); + ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); + List<byte[]> items = powRepo.getItems(); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + byte[] target = security().getProofOfWorkTarget(item.object, item + .nonceTrialsPerByte, item.extraBytes); + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( + new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); + cryptoMsg.signAndEncrypt(identity, signingKey); + CustomMessage response = bmc.send( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + cryptoMsg + ); + if (response.isError()) { + LOG.error("Server responded with error: " + new String(response.getData(), + "UTF-8")); + } else { + ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( + response, reader).decrypt(privateKey); + if (decryptedResponse.getRequest() == COMPLETE) { + bmc.internals().getProofOfWorkService().onNonceCalculated( + initialHash, decryptedResponse.getData()); + } + } + } + if (items.size() == 0) { + stopPowSync(getContext()); + } + LOG.info("Synchronization finished"); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + public static void startSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_SYNC); + + // Recommend a schedule for automatic synchronization. The system may modify this based + // on other scheduled syncs and network utilization. + ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + } + + public static void stopSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_SYNC); + + ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle()); + } + + + public static void startPowSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_POW); + + // Recommend a schedule for automatic synchronization. The system may modify this based + // on other scheduled syncs and network utilization. + ContentResolver.addPeriodicSync(account, AUTHORITY, new Bundle(), SYNC_FREQUENCY); + } + + public static void stopPowSync(Context ctx) { + // Create account, if it's missing. (Either first run, or user has deleted account.) + Account account = addAccount(ctx, ACCOUNT_POW); + + ContentResolver.removePeriodicSync(account, AUTHORITY, new Bundle()); + } + + private static Account addAccount(Context ctx, Account account) { + if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) { + // Inform the system that this account supports sync + ContentResolver.setIsSyncable(account, AUTHORITY, 1); + // Inform the system that this account is eligible for auto sync when the network is up + ContentResolver.setSyncAutomatically(account, AUTHORITY, true); + } + return account; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java new file mode 100644 index 0000000..20c5862 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -0,0 +1,90 @@ +package ch.dissem.apps.abit.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.notification.ErrorNotification; + +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; + +/** + * Created by chrig on 01.12.2015. + */ +public class Preferences { + private static Logger LOG = LoggerFactory.getLogger(Preferences.class); + + public static boolean useTrustedNode(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + return trustedNode == null || trustedNode.trim().isEmpty(); + } + + /** + * Warning, this method might do a network call and therefore can't be called from + * the UI thread. + */ + public static InetAddress getTrustedNode(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + if (trustedNode == null) return null; + trustedNode = trustedNode.trim(); + if (trustedNode.isEmpty()) return null; + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + trustedNode = trustedNode.substring(0, index); + } + try { + return InetAddress.getByName(trustedNode); + } catch (UnknownHostException e) { + new ErrorNotification(ctx) + .setError(R.string.error_invalid_sync_host) + .show(); + LOG.error(e.getMessage(), e); + return null; + } + } + + public static int getTrustedNodePort(Context ctx) { + String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); + if (trustedNode == null) return 8444; + trustedNode = trustedNode.trim(); + + if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$")) { + int index = trustedNode.lastIndexOf(':'); + String portString = trustedNode.substring(index + 1); + try { + return Integer.parseInt(portString); + } catch (NumberFormatException e) { + new ErrorNotification(ctx) + .setError(R.string.error_invalid_sync_port, portString) + .show(); + } + } + return 8444; + } + + public static long getTimeoutInSeconds(Context ctx) { + String preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT); + return preference == null ? 120 : Long.parseLong(preference); + } + + public static boolean isServerPOW(Context ctx) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + return preferences.getBoolean(PREFERENCE_SERVER_POW, false); + } + + private static String getPreference(Context ctx, String name) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + + return preferences.getString(name, null); + } +} diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 05b6937..50fea51 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -27,7 +27,7 @@ <SwitchPreference android:defaultValue="false" android:key="server_pow" - android:dependency="@string/trusted_node" + android:dependency="trusted_node" android:title="@string/server_pow" android:summary="@string/server_pow_summary" /> diff --git a/build.gradle b/build.gradle index 7acef02..f0f1e2b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.0-beta1' + classpath 'com.android.tools.build:gradle:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From fdc2277324269f64206aed9033829c5f50ea386e Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 27 Dec 2015 20:04:17 +0100 Subject: [PATCH 023/110] Implemented option to only use WiFi --- app/src/main/AndroidManifest.xml | 8 ++++ .../ch/dissem/apps/abit/MainActivity.java | 45 ++++++++++++++----- .../apps/abit/listener/WifiReceiver.java | 39 ++++++++++++++++ .../apps/abit/service/BitmessageService.java | 11 +++-- .../abit/synchronization/SyncAdapter.java | 11 +++-- .../ch/dissem/apps/abit/util/Preferences.java | 19 +++++--- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 979915d..4f64223 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="ch.dissem.apps.abit"> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> @@ -128,6 +129,13 @@ <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" /> </service> + + <!-- Receive Wi-Fi connection state changes --> + <receiver android:name=".listener.WifiReceiver"> + <intent-filter> + <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> + </intent-filter> + </receiver> </application> </manifest> diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 5d7bee8..1d2a418 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -1,10 +1,9 @@ package ch.dissem.apps.abit; -import android.accounts.Account; -import android.accounts.AccountManager; +import android.app.AlertDialog; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; @@ -49,7 +48,6 @@ import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.apps.abit.synchronization.Authenticator; import ch.dissem.apps.abit.synchronization.SyncAdapter; import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -61,7 +59,6 @@ import ch.dissem.bitmessage.ports.MessageRepository; import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_IDENTITY; import static ch.dissem.apps.abit.service.BitmessageService.MSG_START_NODE; import static ch.dissem.apps.abit.service.BitmessageService.MSG_STOP_NODE; -import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; /** @@ -300,12 +297,7 @@ public class MainActivity extends AppCompatActivity boolean isChecked) { if (messenger != null) { if (isChecked) { - try { - service.send(Message.obtain(null, - MSG_START_NODE)); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); - } + checkAndStartNode(buttonView); } else { try { service.send(Message.obtain(null, @@ -358,6 +350,37 @@ public class MainActivity extends AppCompatActivity .build(); } + private void checkAndStartNode(final CompoundButton buttonView) { + if (Preferences.isConnectionAllowed(MainActivity.this)) { + forceStartNode(); + } else { + new AlertDialog.Builder(MainActivity.this) + .setMessage(R.string.full_node_warning) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + forceStartNode(); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + buttonView.setChecked(false); + } + }) + .show(); + } + } + + private void forceStartNode() { + try { + service.send(Message.obtain(null, + MSG_START_NODE)); + } catch (RemoteException e) { + LOG.error(e.getMessage(), e); + } + } + private void showSelectedLabel() { if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java new file mode 100644 index 0000000..ca34809 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java @@ -0,0 +1,39 @@ +package ch.dissem.apps.abit.listener; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.SupplicantState; +import android.net.wifi.WifiManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.BitmessageContext; + +public class WifiReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context ctx, Intent intent) { + if (Preferences.isWifiOnly(ctx)) { + BitmessageContext bmc = Singleton.getBitmessageContext(ctx); + ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context + .CONNECTIVITY_SERVICE); + NetworkInfo netInfo = conMan.getActiveNetworkInfo(); + + if (netInfo != null && netInfo.getType() != ConnectivityManager.TYPE_WIFI + && !bmc.isRunning()) { + bmc.shutdown(); + } + } + } + + public static boolean isConnectedToWifi(Context ctx) { + WifiManager wifiManager = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE); + SupplicantState state = wifiManager.getConnectionInfo().getSupplicantState(); + return state == SupplicantState.COMPLETED; + } +} \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index d2184fd..5c81472 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -157,12 +157,17 @@ public class BitmessageService extends Service { // (I'm not quite sure this can be done here, though) service.get().startService(new Intent(service.get(), BitmessageService.class)); running = true; - service.get().startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); - bmc.startup(); + service.get().startForeground(ONGOING_NOTIFICATION_ID, notification + .getNotification()); + if (!bmc.isRunning()) { + bmc.startup(); + } notification.show(); break; case MSG_STOP_NODE: - bmc.shutdown(); + if (bmc.isRunning()) { + bmc.shutdown(); + } running = false; service.get().stopForeground(false); service.get().stopSelf(); diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 682e165..7655044 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -52,12 +52,15 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - if (account.equals(Authenticator.ACCOUNT_SYNC)) - syncData(); - else if (account.equals(Authenticator.ACCOUNT_POW)) + if (account.equals(Authenticator.ACCOUNT_SYNC)) { + if (Preferences.isConnectionAllowed(getContext())) { + syncData(); + } + } else if (account.equals(Authenticator.ACCOUNT_POW)) { syncPOW(); - else + } else { throw new RuntimeException("Unknown " + account); + } } private void syncData() { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index 20c5862..df5b1c7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -11,14 +11,15 @@ import java.net.InetAddress; import java.net.UnknownHostException; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.listener.WifiReceiver; import ch.dissem.apps.abit.notification.ErrorNotification; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; +import static ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY; /** - * Created by chrig on 01.12.2015. + * @author Christian Basler */ public class Preferences { private static Logger LOG = LoggerFactory.getLogger(Preferences.class); @@ -77,14 +78,18 @@ public class Preferences { return preference == null ? 120 : Long.parseLong(preference); } - public static boolean isServerPOW(Context ctx) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); - return preferences.getBoolean(PREFERENCE_SERVER_POW, false); - } - private static String getPreference(Context ctx, String name) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); return preferences.getString(name, null); } + + public static boolean isConnectionAllowed(Context ctx) { + return !isWifiOnly(ctx) || WifiReceiver.isConnectedToWifi(ctx); + } + + public static boolean isWifiOnly(Context ctx) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true); + } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 19fda0d..694b211 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -51,4 +51,5 @@ <string name="subscribed">Abonniert</string> <string name="server_pow">Server POW</string> <string name="server_pow_summary">Der vertrauenswürdige Knoten macht den Proof of Work</string> + <string name="full_node_warning">Ein aktiver Bitmessage-Knoten muss viel hoch- und herunterladen, was auf einem mobilen Netzwerk teuer sein kann. Soll tatsächlich ein aktiver Knoten gestartet werden?</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19fd4f8..9ad6b9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,4 +51,5 @@ <string name="subscribed">Subscribed</string> <string name="server_pow">Server POW</string> <string name="server_pow_summary">Trusted node does proof of work</string> + <string name="full_node_warning">Running a full Bitmessage uses a lot of traffic, which could be expensive on a mobile network. Are you sure you want to start a full node?</string> </resources> From 694fa97dce86415eeaf1bcfc1e77c426aaa64a09 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 27 Dec 2015 22:07:44 +0100 Subject: [PATCH 024/110] About Libraries --- app/build.gradle | 3 +++ .../ch/dissem/apps/abit/SettingsFragment.java | 18 +++++++++++++++++ app/src/main/res/values/colors.xml | 5 +++++ .../main/res/values/library_jabit_strings.xml | 20 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 +++ app/src/main/res/xml/preferences.xml | 5 +++++ 6 files changed, 54 insertions(+) create mode 100644 app/src/main/res/values/library_jabit_strings.xml diff --git a/app/build.gradle b/app/build.gradle index 01ba6f9..99299a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,6 +36,9 @@ dependencies { compile('com.mikepenz:materialdrawer:3.1.0@aar') { transitive = true } + compile('com.mikepenz:aboutlibraries:5.3.4@aar') { + transitive = true + } compile 'com.mikepenz:iconics:1.6.2@aar' compile 'com.mikepenz:community-material-typeface:1.1.71@aar' } diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 75bd0bc..d92ac68 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -3,9 +3,13 @@ package ch.dissem.apps.abit; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import com.mikepenz.aboutlibraries.Libs; +import com.mikepenz.aboutlibraries.LibsBuilder; + import ch.dissem.apps.abit.synchronization.SyncAdapter; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; @@ -23,6 +27,20 @@ public class SettingsFragment // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); + + Preference about = findPreference("about"); + about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new LibsBuilder() + .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .withAboutDescription(getString(R.string.about_app)) + .start(getActivity()); + return true; + } + }); } @Override diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index eb1f6e8..2920668 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -10,4 +10,9 @@ <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color> + + <!-- Used for AboutLibraries --> + <color name="colorPrimary">@color/primary</color> + <color name="colorPrimaryDark">@color/primary_dark</color> + <color name="colorAccent">@color/accent</color> </resources> diff --git a/app/src/main/res/values/library_jabit_strings.xml b/app/src/main/res/values/library_jabit_strings.xml new file mode 100644 index 0000000..bd4220c --- /dev/null +++ b/app/src/main/res/values/library_jabit_strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="define_jabit"></string> + <!-- Author section --> + <string name="library_jabit_author">Christian Basler</string> + <string name="library_jabit_authorWebsite">dissem.ch</string> + <!-- Library section --> + <string name="library_jabit_libraryName">Jabit</string> + <string name="library_jabit_libraryDescription">Jabit strives to be an easy to use Bitmessage library for Java developers to quickly implement their own Bitmessage clients.</string> + <string name="library_jabit_libraryWebsite">https://github.com/Dissem/Jabit/wiki</string> + <string name="library_jabit_libraryVersion">1.0.0</string> + <!-- OpenSource section --> + <string name="library_jabit_isOpenSource">true</string> + <string name="library_jabit_repositoryLink">https://github.com/Dissem/Jabit</string> + <!-- ClassPath for autoDetect section --> + <string name="library_jabit_classPath">ch.dissem.bitmessage.BitmessageContext</string> + <!-- License section --> + <string name="library_jabit_licenseId">apache_2_0</string> + <!-- Custom variables section --> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ad6b9f..5746bbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ <resources> <string name="app_name">Abit</string> + <string name="about_app">A Bitmessage client for Android</string> <string name="title_message_detail">Message Detail</string> <string name="title_subscription_detail">Subscription Detail</string> <string name="bitmessage_full_node">Bitmessage Node</string> @@ -52,4 +53,6 @@ <string name="server_pow">Server POW</string> <string name="server_pow_summary">Trusted node does proof of work</string> <string name="full_node_warning">Running a full Bitmessage uses a lot of traffic, which could be expensive on a mobile network. Are you sure you want to start a full node?</string> + <string name="about">About Abit</string> + <string name="about_summary">Open source dependencies.</string> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 50fea51..696d443 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -31,4 +31,9 @@ android:title="@string/server_pow" android:summary="@string/server_pow_summary" /> + <Preference + android:key="about" + android:title="@string/about" + android:summary="@string/about_summary" + /> </PreferenceScreen> \ No newline at end of file From eb322d94a26acab791280dbe5ab8e230f014267a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 1 Jan 2016 15:35:16 +0100 Subject: [PATCH 025/110] Minor improvements and preparations for the release --- app/build.gradle | 11 ++- .../ch/dissem/apps/abit/SettingsFragment.java | 1 + app/src/main/res/layout/contact_row.xml | 2 +- .../res/layout/fragment_contact_detail.xml | 2 +- .../res/layout/fragment_message_detail.xml | 2 +- app/src/main/res/layout/message_row.xml | 2 +- app/src/main/res/layout/subscription_row.xml | 2 +- .../res/values-de/library_jabit_strings.xml | 7 ++ app/src/main/res/values-v21/styles.xml | 6 +- app/src/main/res/values/arrays.xml | 13 --- app/src/main/res/values/colors.xml | 19 ++--- .../main/res/values/library_jabit_strings.xml | 20 ++--- app/src/main/res/values/styles.xml | 20 ++--- gradle.properties | 13 ++- store/function_graphic.png | Bin 0 -> 3297 bytes store/function_graphic.svg | 68 ++++++++++++++++ store/icon.svg | 75 ++++++++++++++++++ 17 files changed, 208 insertions(+), 55 deletions(-) create mode 100644 app/src/main/res/values-de/library_jabit_strings.xml delete mode 100644 app/src/main/res/values/arrays.xml create mode 100644 store/function_graphic.png create mode 100644 store/function_graphic.svg create mode 100644 store/icon.svg diff --git a/app/build.gradle b/app/build.gradle index 99299a0..1b733ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,10 +12,19 @@ android { versionCode 1 versionName "1.0" } + signingConfigs { + release { + storeFile file(keyStoreFile) + storePassword keyStorePassword + keyAlias signingKeyAlias + keyPassword signingKeyPassword + } + } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } } } @@ -42,7 +51,7 @@ dependencies { compile 'com.mikepenz:iconics:1.6.2@aar' compile 'com.mikepenz:community-material-typeface:1.1.71@aar' } -5 + idea.module { downloadJavadoc = true downloadSources = true diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index d92ac68..38ad512 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -33,6 +33,7 @@ public class SettingsFragment @Override public boolean onPreferenceClick(Preference preference) { new LibsBuilder() + .withActivityTitle(getActivity().getString(R.string.about)) .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) .withAboutIconShown(true) .withAboutVersionShown(true) diff --git a/app/src/main/res/layout/contact_row.xml b/app/src/main/res/layout/contact_row.xml index b71f67e..0f6ad65 100644 --- a/app/src/main/res/layout/contact_row.xml +++ b/app/src/main/res/layout/contact_row.xml @@ -27,7 +27,7 @@ android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:src="@color/accent" + android:src="@color/colorAccent" android:layout_margin="16dp"/> <TextView diff --git a/app/src/main/res/layout/fragment_contact_detail.xml b/app/src/main/res/layout/fragment_contact_detail.xml index 79bb54a..4aa33dd 100644 --- a/app/src/main/res/layout/fragment_contact_detail.xml +++ b/app/src/main/res/layout/fragment_contact_detail.xml @@ -26,7 +26,7 @@ android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:src="@color/accent" + android:src="@color/colorAccent" android:layout_margin="16dp"/> <EditText diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 55e0516..044dd43 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -38,7 +38,7 @@ android:layout_alignParentStart="true" android:layout_below="@+id/divider" android:layout_margin="16dp" - android:src="@color/accent" /> + android:src="@color/colorAccent" /> <TextView android:id="@+id/sender" diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index 00d20bd..c3d4f2a 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -25,7 +25,7 @@ android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:src="@color/accent" + android:src="@color/colorAccent" android:layout_margin="16dp"/> <TextView diff --git a/app/src/main/res/layout/subscription_row.xml b/app/src/main/res/layout/subscription_row.xml index 58bbb6f..359edce 100644 --- a/app/src/main/res/layout/subscription_row.xml +++ b/app/src/main/res/layout/subscription_row.xml @@ -27,7 +27,7 @@ android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:src="@color/accent" + android:src="@color/colorAccent" android:layout_margin="16dp"/> <TextView diff --git a/app/src/main/res/values-de/library_jabit_strings.xml b/app/src/main/res/values-de/library_jabit_strings.xml new file mode 100644 index 0000000..e7a54a1 --- /dev/null +++ b/app/src/main/res/values-de/library_jabit_strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="about">Über Abit</string> + <string name="about_app">Ein Bitmessage-Client für Android</string> + <string name="about_summary">Open Source Abhängigkeiten.</string> + <string name="library_jabit_libraryDescription">Jabit hat das Ziel eine einfach zu benutzende Bitmessage-Bibliothek für Java-Entwickler zu sein, um schnell eigene Bitmessage-Anwendungen zu implementieren.</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 9245277..72150b4 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -5,11 +5,11 @@ <!-- Main theme colors --> <!-- your app branding color for the app bar --> - <item name="android:colorPrimary">@color/primary</item> + <item name="android:colorPrimary">@color/colorPrimary</item> <!-- darker variant for the status bar and contextual app bars --> - <item name="android:colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item> <!-- theme UI controls like checkboxes and text fields --> - <item name="android:colorAccent">@color/accent</item> + <item name="android:colorAccent">@color/colorAccent</item> <item name="android:windowContentTransitions">true</item> <item name="android:windowAllowEnterTransitionOverlap">true</item> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml deleted file mode 100644 index 803fa88..0000000 --- a/app/src/main/res/values/arrays.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string-array name="connection_modes"> - <item>full</item> - <item>trusted</item> - <item>offline</item> - </string-array> - <string-array name="connection_mode_values"> - <item>A full node, receiving and relaying objects all the time.</item> - <item>Connect to a trusted node from time to time to get new objects.</item> - <item>Don\'t connect to the network.</item> - </string-array> -</resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2920668..8958204 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,18 +1,13 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Palette generated by Material Palette - materialpalette.com/blue-grey/orange --> <resources> - <color name="primary">#FFC107</color> - <color name="primary_dark">#FFA000</color> - <color name="primary_dark_text">#DEFFFFFF</color> - <color name="primary_light">#FFECB3</color> - <color name="accent">#607D8B</color> - <color name="primary_text">#212121</color> - <color name="secondary_text">#727272</color> + <color name="colorPrimary">#FFC107</color> + <color name="colorPrimaryDark">#FFA000</color> + <color name="colorPrimaryDarkText">#DEFFFFFF</color> + <color name="colorPrimaryLight">#FFECB3</color> + <color name="colorAccent">#607D8B</color> + <color name="colorPrimaryText">#212121</color> + <color name="colorSecondaryText">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color> - - <!-- Used for AboutLibraries --> - <color name="colorPrimary">@color/primary</color> - <color name="colorPrimaryDark">@color/primary_dark</color> - <color name="colorAccent">@color/accent</color> </resources> diff --git a/app/src/main/res/values/library_jabit_strings.xml b/app/src/main/res/values/library_jabit_strings.xml index bd4220c..4bd1769 100644 --- a/app/src/main/res/values/library_jabit_strings.xml +++ b/app/src/main/res/values/library_jabit_strings.xml @@ -1,20 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="define_jabit"></string> + <string name="define_jabit" translatable="false"></string> <!-- Author section --> - <string name="library_jabit_author">Christian Basler</string> - <string name="library_jabit_authorWebsite">dissem.ch</string> + <string name="library_jabit_author" translatable="false">Christian Basler</string> + <string name="library_jabit_authorWebsite" translatable="false">dissem.ch</string> <!-- Library section --> - <string name="library_jabit_libraryName">Jabit</string> + <string name="library_jabit_libraryName" translatable="false">Jabit</string> <string name="library_jabit_libraryDescription">Jabit strives to be an easy to use Bitmessage library for Java developers to quickly implement their own Bitmessage clients.</string> - <string name="library_jabit_libraryWebsite">https://github.com/Dissem/Jabit/wiki</string> - <string name="library_jabit_libraryVersion">1.0.0</string> + <string name="library_jabit_libraryWebsite" translatable="false">https://github.com/Dissem/Jabit/wiki</string> + <string name="library_jabit_libraryVersion" translatable="false">1.0.0</string> <!-- OpenSource section --> - <string name="library_jabit_isOpenSource">true</string> - <string name="library_jabit_repositoryLink">https://github.com/Dissem/Jabit</string> + <string name="library_jabit_isOpenSource" translatable="false">true</string> + <string name="library_jabit_repositoryLink" translatable="false">https://github.com/Dissem/Jabit</string> <!-- ClassPath for autoDetect section --> - <string name="library_jabit_classPath">ch.dissem.bitmessage.BitmessageContext</string> + <string name="library_jabit_classPath" translatable="false">ch.dissem.bitmessage.BitmessageContext</string> <!-- License section --> - <string name="library_jabit_licenseId">apache_2_0</string> + <string name="library_jabit_licenseId" translatable="false">apache_2_0</string> <!-- Custom variables section --> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e0df448..090c4de 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -3,21 +3,21 @@ <!-- Base application theme. --> <style name="AppTheme.Base" parent="MaterialDrawerTheme.Light.DarkToolbar.TranslucentStatus"> - <item name="android:activatedBackgroundIndicator">@color/primary_light</item> - <item name="android:textColor">@color/primary_text</item> - <item name="android:textColorSecondary">@color/secondary_text</item> + <item name="android:activatedBackgroundIndicator">@color/colorPrimaryLight</item> + <item name="android:textColor">@color/colorPrimaryText</item> + <item name="android:textColorSecondary">@color/colorSecondaryText</item> <!-- MaterialDrawer specific values --> - <item name="material_drawer_background">@color/primary_dark</item> - <item name="material_drawer_icons">@color/primary_text</item> + <item name="material_drawer_background">@color/colorPrimaryDark</item> + <item name="material_drawer_icons">@color/colorPrimaryText</item> <item name="material_drawer_primary_icon">@color/icons</item> - <item name="material_drawer_primary_text">@color/primary_text</item> - <item name="material_drawer_secondary_text">@color/secondary_text</item> - <item name="material_drawer_hint_text">@color/secondary_text</item> + <item name="material_drawer_primary_text">@color/colorPrimaryText</item> + <item name="material_drawer_secondary_text">@color/colorSecondaryText</item> + <item name="material_drawer_hint_text">@color/colorSecondaryText</item> <item name="material_drawer_divider">@color/divider</item> <item name="material_drawer_selected">@color/primary</item> - <item name="material_drawer_selected_text">@color/primary_text</item> - <item name="material_drawer_header_selection_text">@color/primary_text</item> + <item name="material_drawer_selected_text">@color/colorPrimaryText</item> + <item name="material_drawer_header_selection_text">@color/colorPrimaryText</item> </style> diff --git a/gradle.properties b/gradle.properties index 1d3591c..ab12df9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,15 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true + + + + +# Signing and encryption settings +# Don't change those parameters - override those properties in the +# gradle.properties file in your home directory instead +keyStoreFile= +keyStorePassword= +signingKeyAlias= +signingKeyPassword= \ No newline at end of file diff --git a/store/function_graphic.png b/store/function_graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..8c281772568a119e18c6346d854f12610df21f16 GIT binary patch literal 3297 zcmeAS@N?(olHy`uVBq!ia0y~yU;#22zi_Ytsl{sX+CYk>*vT`50|;t3QaXTq&H|6f zVxao#Ak65bF}ngNC|TkfQ4*Y=R#Ki=l*&+EUaps!mtCBkSdglhUz9%kosAR&15cc% zi(^Q|oVPa&c^eo64jf?D|1tegpEwh@7zgLx;0rrX+pMeIX8BwKXx@YJYwMYTbi;Q^ zAju$*03sb47(o;x$Z-rTAZ4Qxqd_v77)G<fXvr{ICXUtyqZQ<6(_pkwG}=xakuBr{ Yyn*fPn~rgIB!RMkr>mdKI;Vst0Qj)mv;Y7A literal 0 HcmV?d00001 diff --git a/store/function_graphic.svg b/store/function_graphic.svg new file mode 100644 index 0000000..7b4db20 --- /dev/null +++ b/store/function_graphic.svg @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1024" + height="500" + viewBox="0 0 1024 500.00001" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="function_graphic.svg" + inkscape:export-filename="C:\Users\chrig\Documents\Projekte\Abit\store\function_graphic.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.35" + inkscape:cx="-249.28571" + inkscape:cy="520" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1280" + inkscape:window-height="657" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Ebene 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-552.36216)"> + <rect + style="opacity:1;fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect3336" + width="1024" + height="500" + x="2.8571429" + y="552.36218" /> + </g> +</svg> diff --git a/store/icon.svg b/store/icon.svg new file mode 100644 index 0000000..6e6d83b --- /dev/null +++ b/store/icon.svg @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="512" + height="512" + viewBox="0 0 512.00001 512.00001" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="icon.svg"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.35" + inkscape:cx="-330.71429" + inkscape:cy="520" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1280" + inkscape:window-height="657" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Ebene 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-540.36216)"> + <path + sodipodi:type="star" + style="opacity:1;fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path3343" + sodipodi:sides="7" + sodipodi:cx="254.65251" + sodipodi:cy="795.56248" + sodipodi:r1="231.12856" + sodipodi:r2="76.300064" + sodipodi:arg1="-1.4219063" + sodipodi:arg2="-0.97310738" + inkscape:flatsided="false" + inkscape:rounded="1.39" + inkscape:randomized="0" + d="m 288.93824,566.99107 c 227.8089,34.17135 -181.77145,35.86897 8.65088,165.49887 190.42233,129.6299 41.82424,-252.04738 157.14453,-52.63373 C 570.05394,879.26986 313.35752,560.1055 330.73516,789.8066 348.1128,1019.5077 553.87073,665.35713 469.86388,879.85055 385.85704,1094.344 475.34218,694.65489 306.58941,851.45756 137.83665,1008.2602 543.01068,948.319 322.93556,1016.3743 102.86045,1084.4295 471.14302,905.18976 243.33412,871.01841 15.525224,836.84705 315.01104,1116.2521 124.58871,986.62222 -65.833623,856.99233 303.92209,1033.1731 188.60181,833.75941 73.281523,634.34576 41.560203,1042.6994 24.182553,812.99831 6.804904,583.29722 99.600168,982.23077 183.60702,767.73737 267.61388,553.24396 -71.427781,783.04756 97.324978,626.24488 266.07773,469.4422 12.035822,790.72346 232.11094,722.66821 452.18606,654.61296 61.129346,532.81973 288.93824,566.99107 Z" + inkscape:transform-center-x="-12.36068" + inkscape:transform-center-y="-11.435089" /> + </g> +</svg> From abd25f4010394abc30462c73c08229445aba5525 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 13 Jan 2016 17:28:18 +0100 Subject: [PATCH 026/110] Minor bugfixes & renamed 'security' to 'cryptography --- app/build.gradle | 4 ++-- .../ch/dissem/apps/abit/MainActivity.java | 6 +++-- ...Security.java => AndroidCryptography.java} | 6 ++--- .../apps/abit/listener/WifiReceiver.java | 22 ++++++++----------- .../repository/AndroidAddressRepository.java | 14 ++++-------- .../abit/repository/AndroidInventory.java | 1 + .../repository/AndroidMessageRepository.java | 21 +++++------------- .../AndroidProofOfWorkRepository.java | 16 +++++--------- .../dissem/apps/abit/service/Singleton.java | 4 ++-- .../abit/synchronization/SyncService.java | 12 ---------- .../drawable/ic_notification_full_node.xml | 2 +- .../drawable/ic_notification_new_message.xml | 2 +- .../ic_notification_proof_of_work.xml | 2 +- .../res/drawable/ic_notification_warning.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- store/icon.svg | 7 ++++-- 16 files changed, 46 insertions(+), 77 deletions(-) rename app/src/main/java/ch/dissem/apps/abit/adapter/{AndroidSecurity.java => AndroidCryptography.java} (86%) diff --git a/app/build.gradle b/app/build.gradle index 1b733ab..f66586f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ android { defaultConfig { applicationId "ch.dissem.apps.abit" - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 23 versionCode 1 versionName "1.0" @@ -37,7 +37,7 @@ dependencies { compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT' + compile 'ch.dissem.jabit:jabit-cryptography-spongy:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' compile 'org.slf4j:slf4j-android:1.7.12' diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 1d2a418..5bda190 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -43,6 +43,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; @@ -438,8 +439,9 @@ public class MainActivity extends AppCompatActivity @Override public void updateTitle(CharSequence title) { - //noinspection ConstantConditions - getSupportActionBar().setTitle(title); + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(title); + } } public Label getSelectedLabel() { diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java similarity index 86% rename from app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java rename to app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java index e495ff1..8aed5be 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidSecurity.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java @@ -16,7 +16,7 @@ import ch.dissem.bitmessage.entity.payload.Broadcast; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import ch.dissem.bitmessage.security.sc.SpongySecurity; +import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography; import ch.dissem.bitmessage.utils.UnixTime; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; @@ -27,8 +27,8 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; /** * @author Christian Basler */ -public class AndroidSecurity extends SpongySecurity { - public AndroidSecurity() { +public class AndroidCryptography extends SpongyCryptography { + public AndroidCryptography() { PRNGFixes.apply(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java index ca34809..74df713 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java @@ -5,11 +5,6 @@ import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.wifi.SupplicantState; -import android.net.wifi.WifiManager; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Preferences; @@ -20,20 +15,21 @@ public class WifiReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent intent) { if (Preferences.isWifiOnly(ctx)) { BitmessageContext bmc = Singleton.getBitmessageContext(ctx); - ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context - .CONNECTIVITY_SERVICE); - NetworkInfo netInfo = conMan.getActiveNetworkInfo(); - if (netInfo != null && netInfo.getType() != ConnectivityManager.TYPE_WIFI - && !bmc.isRunning()) { + if (!isConnectedToWifi(ctx) && bmc.isRunning()) { bmc.shutdown(); } } } public static boolean isConnectedToWifi(Context ctx) { - WifiManager wifiManager = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE); - SupplicantState state = wifiManager.getConnectionInfo().getSupplicantState(); - return state == SupplicantState.COMPLETED; + NetworkInfo netInfo = getNetworkInfo(ctx); + return netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI; + } + + private static NetworkInfo getNetworkInfo(Context ctx) { + ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context + .CONNECTIVITY_SERVICE); + return conMan.getActiveNetworkInfo(); } } \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index abf123a..997270b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -121,12 +121,11 @@ public class AndroidAddressRepository implements AddressRepository { }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( TABLE_NAME, projection, where, null, null, null, null - ); - try { + )) { c.moveToFirst(); while (!c.isAfterLast()) { BitmessageAddress address; @@ -158,8 +157,6 @@ public class AndroidAddressRepository implements AddressRepository { } } catch (IOException e) { LOG.error(e.getMessage(), e); - } finally { - c.close(); } return result; } @@ -179,13 +176,10 @@ public class AndroidAddressRepository implements AddressRepository { private boolean exists(BitmessageAddress address) { SQLiteDatabase db = sql.getReadableDatabase(); - Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address - .getAddress() + "'", null); - try { + try (Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address + .getAddress() + "'", null)) { cursor.moveToFirst(); return cursor.getInt(0) > 0; - } finally { - cursor.close(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index 56ece70..86aa1f7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -89,6 +89,7 @@ public class AndroidInventory implements Inventory { } finally { c.close(); } + LOG.info("Inventory size: " + result.size()); return result; } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index f33c91b..a186fd3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -108,20 +108,17 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( LBL_TABLE_NAME, projection, where, null, null, null, LBL_COLUMN_ORDER - ); - try { + )) { c.moveToFirst(); while (!c.isAfterLast()) { result.add(getLabel(c)); c.moveToNext(); } - } finally { - c.close(); } return result; } @@ -174,16 +171,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont where = ""; } SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( TABLE_NAME, new String[]{COLUMN_ID}, where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))", null, null, null, null - ); - try { + )) { return c.getColumnCount(); - } finally { - c.close(); } } @@ -245,13 +239,12 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( TABLE_NAME, projection, where, null, null, null, COLUMN_RECEIVED + " DESC" - ); - try { + )) { c.moveToFirst(); while (!c.isAfterLast()) { byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV)); @@ -277,8 +270,6 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont } } catch (IOException e) { LOG.error(e.getMessage(), e); - } finally { - c.close(); } return result; } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index 2273df0..6a96eee 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -52,12 +52,11 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( TABLE_NAME, projection, "initial_hash = X'" + Strings.hex(initialHash) + "'", null, null, null, null - ); - try { + )) { c.moveToFirst(); if (!c.isAfterLast()) { int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); @@ -69,8 +68,6 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) ); } - } finally { - c.close(); } throw new RuntimeException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); @@ -85,20 +82,17 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + List<byte[]> result = new LinkedList<>(); + try (Cursor c = db.query( TABLE_NAME, projection, null, null, null, null, null - ); - List<byte[]> result = new LinkedList<>(); - try { + )) { c.moveToFirst(); while (!c.isAfterLast()) { byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); result.add(initialHash); c.moveToNext(); } - } finally { - c.close(); } return result; } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 51d9311..9c60ecf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -4,7 +4,7 @@ import android.content.Context; import java.util.List; -import ch.dissem.apps.abit.adapter.AndroidSecurity; +import ch.dissem.apps.abit.adapter.AndroidCryptography; import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; import ch.dissem.apps.abit.listener.MessageListener; import ch.dissem.apps.abit.pow.ServerPowEngine; @@ -47,7 +47,7 @@ public class Singleton { new ServerPowEngine(ctx), new ServicePowEngine(ctx) )) - .security(new AndroidSecurity()) + .cryptography(new AndroidCryptography()) .nodeRegistry(new MemoryNodeRegistry()) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index b3abcaf..1f92159 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -7,18 +7,6 @@ import android.os.IBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ch.dissem.apps.abit.listener.MessageListener; -import ch.dissem.apps.abit.notification.NetworkNotification; -import ch.dissem.apps.abit.repository.AndroidInventory; -import ch.dissem.apps.abit.repository.SqlHelper; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.security.sc.SpongySecurity; - -import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; - /** * Define a Service that returns an IBinder for the * sync adapter class, allowing the sync adapter framework to call diff --git a/app/src/main/res/drawable/ic_notification_full_node.xml b/app/src/main/res/drawable/ic_notification_full_node.xml index 4790d6e..c7dff84 100644 --- a/app/src/main/res/drawable/ic_notification_full_node.xml +++ b/app/src/main/res/drawable/ic_notification_full_node.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0,0 0,6 20H19A5,5 0,0 0,24 15C24,12.36 21.95,10.22 19.35,10.03Z"/> </vector> diff --git a/app/src/main/res/drawable/ic_notification_new_message.xml b/app/src/main/res/drawable/ic_notification_new_message.xml index 395fdb0..1def467 100644 --- a/app/src/main/res/drawable/ic_notification_new_message.xml +++ b/app/src/main/res/drawable/ic_notification_new_message.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M20.5,0A2.5,2.5 0,0 1,23 2.5V3A1,1 0,0 1,24 4V8A1,1 0,0 1,23 9H18A1,1 0,0 1,17 8V4A1,1 0,0 1,18 3V2.5A2.5,2.5 0,0 1,20.5 0M12,11L4,6V8L12,13L16.18,10.39C16.69,10.77 17.32,11 18,11H22V18A2,2 0,0 1,20 20H4A2,2 0,0 1,2 18V6A2,2 0,0 1,4 4H15V8C15,8.36 15.06,8.7 15.18,9L12,11M20.5,1A1.5,1.5 0,0 0,19 2.5V3H22V2.5A1.5,1.5 0,0 0,20.5 1Z"/> </vector> diff --git a/app/src/main/res/drawable/ic_notification_proof_of_work.xml b/app/src/main/res/drawable/ic_notification_proof_of_work.xml index c64f014..fda3f15 100644 --- a/app/src/main/res/drawable/ic_notification_proof_of_work.xml +++ b/app/src/main/res/drawable/ic_notification_proof_of_work.xml @@ -4,6 +4,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M11.71,19C9.93,19 8.5,17.59 8.5,15.86C8.5,14.24 9.53,13.1 11.3,12.74C13.07,12.38 14.9,11.53 15.92,10.16C16.31,11.45 16.5,12.81 16.5,14.2C16.5,16.84 14.36,19 11.71,19M13.5,0.67C13.5,0.67 14.24,3.32 14.24,5.47C14.24,7.53 12.89,9.2 10.83,9.2C8.76,9.2 7.2,7.53 7.2,5.47L7.23,5.1C5.21,7.5 4,10.61 4,14A8,8 0 0,0 12,22A8,8 0 0,0 20,14C20,8.6 17.41,3.8 13.5,0.67Z" /> </vector> diff --git a/app/src/main/res/drawable/ic_notification_warning.xml b/app/src/main/res/drawable/ic_notification_warning.xml index 0510991..3185242 100644 --- a/app/src/main/res/drawable/ic_notification_warning.xml +++ b/app/src/main/res/drawable/ic_notification_warning.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M1,21h22L12,2 1,21zm12,-3h-2v-2h2v2zm0,-4h-2v-4h2v4z"/> </vector> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5746bbd..fa2c9df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,7 +52,7 @@ <string name="subscribed">Subscribed</string> <string name="server_pow">Server POW</string> <string name="server_pow_summary">Trusted node does proof of work</string> - <string name="full_node_warning">Running a full Bitmessage uses a lot of traffic, which could be expensive on a mobile network. Are you sure you want to start a full node?</string> + <string name="full_node_warning">Running a full Bitmessage node uses a lot of traffic, which could be expensive on a mobile network. Are you sure you want to start a full node?</string> <string name="about">About Abit</string> <string name="about_summary">Open source dependencies.</string> </resources> diff --git a/store/icon.svg b/store/icon.svg index 6e6d83b..ef7a4a6 100644 --- a/store/icon.svg +++ b/store/icon.svg @@ -15,7 +15,10 @@ id="svg2" version="1.1" inkscape:version="0.91 r13725" - sodipodi:docname="icon.svg"> + sodipodi:docname="icon.svg" + inkscape:export-filename="C:\Users\chrig\Documents\Projekte\Abit\store\icon.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> <defs id="defs4" /> <sodipodi:namedview @@ -26,7 +29,7 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" - inkscape:cx="-330.71429" + inkscape:cx="-602.14286" inkscape:cy="520" inkscape:document-units="px" inkscape:current-layer="layer1" From 2d2916f4984ae5888aa3318184839009ff283f7c Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 14 Jan 2016 16:41:10 +0100 Subject: [PATCH 027/110] Added some quick & dirty activity to show addresses and the network state --- app/src/main/AndroidManifest.xml | 102 ++++++++++-------- .../ch/dissem/apps/abit/SettingsFragment.java | 10 ++ .../ch/dissem/apps/abit/StatusActivity.java | 36 +++++++ .../apps/abit/service/BitmessageService.java | 3 + app/src/main/res/layout/activity_status.xml | 40 +++++++ app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/preferences.xml | 5 + 8 files changed, 160 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/StatusActivity.java create mode 100644 app/src/main/res/layout/activity_status.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f64223..5738a48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,17 +1,18 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="ch.dissem.apps.abit"> +<manifest + package="ch.dissem.apps.abit" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> - <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> - <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> - <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> - <uses-permission android:name="android.permission.READ_CONTACTS" /> - <uses-permission android:name="android.permission.WRITE_CONTACTS" /> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> + <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> + <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/> + <uses-permission android:name="android.permission.READ_CONTACTS"/> + <uses-permission android:name="android.permission.WRITE_CONTACTS"/> <application android:allowBackup="true" @@ -22,9 +23,9 @@ android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> - <action android:name="android.intent.action.MAIN" /> + <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity @@ -34,7 +35,7 @@ tools:ignore="UnusedAttribute"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MainActivity" /> + android:value=".MainActivity"/> </activity> <activity android:name=".SubscriptionDetailActivity" @@ -43,7 +44,7 @@ tools:ignore="UnusedAttribute"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MainActivity" /> + android:value=".MainActivity"/> </activity> <activity android:name=".ComposeMessageActivity" @@ -51,30 +52,30 @@ android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" - android:value=".MainActivity" /> + android:value=".MainActivity"/> <intent-filter> - <action android:name="android.intent.action.SENDTO" /> + <action android:name="android.intent.action.SENDTO"/> - <data android:scheme="bitmessage" /> - <data android:scheme="bitmsg" /> - <data android:scheme="bm" /> + <data android:scheme="bitmessage"/> + <data android:scheme="bitmsg"/> + <data android:scheme="bm"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <intent-filter> - <action android:name="android.intent.action.SEND" /> + <action android:name="android.intent.action.SEND"/> - <data android:mimeType="text/plain" /> + <data android:mimeType="text/plain"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <intent-filter> - <action android:name="android.intent.action.SEND_MULTIPLE" /> + <action android:name="android.intent.action.SEND_MULTIPLE"/> - <data android:mimeType="text/plain" /> + <data android:mimeType="text/plain"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> <activity @@ -82,43 +83,45 @@ android:label="@string/settings" android:parentActivityName=".MainActivity"> <intent-filter> - <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> + <action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> - <activity android:name=".OpenBitmessageLinkActivity" android:label="@string/title_activity_open_bitmessage_link" android:theme="@style/Theme.AppCompat.Light.Dialog"> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <data android:scheme="bitmessage" /> - <data android:scheme="bitmsg" /> - <data android:scheme="bm" /> + <data android:scheme="bitmessage"/> + <data android:scheme="bitmsg"/> + <data android:scheme="bm"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> </intent-filter> </activity> - <service android:name=".service.BitmessageService" /> - <service android:name=".service.ProofOfWorkService" /> + + <service android:name=".service.BitmessageService"/> + <service android:name=".service.ProofOfWorkService"/> <!-- Synchronization --> <provider android:name=".synchronization.StubProvider" android:authorities="ch.dissem.apps.abit.provider" android:exported="false" - android:syncable="true" /> + android:syncable="true"/> + <service android:name=".synchronization.AuthenticatorService"> <intent-filter> - <action android:name="android.accounts.AccountAuthenticator" /> + <action android:name="android.accounts.AccountAuthenticator"/> </intent-filter> + <meta-data android:name="android.accounts.AccountAuthenticator" - android:resource="@xml/authenticator" /> + android:resource="@xml/authenticator"/> </service> <service android:name=".synchronization.SyncService" @@ -126,16 +129,27 @@ <intent-filter> <action android:name="android.content.SyncAdapter"/> </intent-filter> - <meta-data android:name="android.content.SyncAdapter" - android:resource="@xml/syncadapter" /> + + <meta-data + android:name="android.content.SyncAdapter" + android:resource="@xml/syncadapter"/> </service> <!-- Receive Wi-Fi connection state changes --> <receiver android:name=".listener.WifiReceiver"> <intent-filter> - <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> + <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> </intent-filter> </receiver> + + <activity + android:name=".StatusActivity" + android:label="@string/title_activity_status" + android:parentActivityName=".SettingsActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".SettingsActivity"/> + </activity> </application> </manifest> diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 38ad512..dc638e7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -1,6 +1,7 @@ package ch.dissem.apps.abit; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; @@ -42,6 +43,15 @@ public class SettingsFragment return true; } }); + + Preference status = findPreference("status"); + status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivity(new Intent(getActivity(), StatusActivity.class)); + return true; + } + }); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java new file mode 100644 index 0000000..60886b2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java @@ -0,0 +1,36 @@ +package ch.dissem.apps.abit; + +import android.os.Bundle; +import android.app.Activity; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.widget.TextView; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; + +public class StatusActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_status); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(false); + + BitmessageContext bmc = Singleton.getBitmessageContext(this); + StringBuilder status = new StringBuilder(); + for (BitmessageAddress address : bmc.addresses().getIdentities()) { + status.append(address.getAddress()).append('\n'); + } + status.append('\n'); + status.append(bmc.status()); + ((TextView) findViewById(R.id.content)).setText(status); + } + +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 5c81472..b5c18f6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.ref.WeakReference; +import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -101,6 +102,8 @@ public class BitmessageService extends Service { switch (msg.what) { case MSG_CREATE_IDENTITY: { BitmessageAddress identity = bmc.createIdentity(false); + identity.setAlias(service.get().getString(R.string.alias_default_identity)); + bmc.addresses().save(identity); if (msg.replyTo != null) { try { Message message = Message.obtain(this, MSG_CREATE_IDENTITY); diff --git a/app/src/main/res/layout/activity_status.xml b/app/src/main/res/layout/activity_status.xml new file mode 100644 index 0000000..2e8d317 --- /dev/null +++ b/app/src/main/res/layout/activity_status.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.design.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + tools:context="ch.dissem.apps.abit.StatusActivity"> + + <TextView + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fontFamily="monospace" + android:padding="16dp" + android:text="" + android:textIsSelectable="true" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + </TextView> + + <android.support.design.widget.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + android:elevation="4dp" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + android:title="@string/status" + app:title="@string/status" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> + + </android.support.design.widget.AppBarLayout> + +</android.support.design.widget.CoordinatorLayout> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 694b211..279491a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -52,4 +52,8 @@ <string name="server_pow">Server POW</string> <string name="server_pow_summary">Der vertrauenswürdige Knoten macht den Proof of Work</string> <string name="full_node_warning">Ein aktiver Bitmessage-Knoten muss viel hoch- und herunterladen, was auf einem mobilen Netzwerk teuer sein kann. Soll tatsächlich ein aktiver Knoten gestartet werden?</string> + <string name="status">Debugging</string> + <string name="alias_default_identity">Ich</string> + <string name="title_activity_status">Debugging</string> + <string name="status_summary">Technische Infos</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa2c9df..83c5d60 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,4 +55,8 @@ <string name="full_node_warning">Running a full Bitmessage node uses a lot of traffic, which could be expensive on a mobile network. Are you sure you want to start a full node?</string> <string name="about">About Abit</string> <string name="about_summary">Open source dependencies.</string> + <string name="title_activity_status">Debugging</string> + <string name="status">Debugging</string> + <string name="status_summary">Technical information</string> + <string name="alias_default_identity">Me</string> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 696d443..b04f110 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -36,4 +36,9 @@ android:title="@string/about" android:summary="@string/about_summary" /> + <Preference + android:key="status" + android:title="@string/status" + android:summary="@string/status_summary" + /> </PreferenceScreen> \ No newline at end of file From b4c5d8cc2163c63ab0c7aa2d8ed1da2c97fd1809 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 17 Jan 2016 07:10:44 +0100 Subject: [PATCH 028/110] Jabit Domain was renamed to Jabit Core --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f66586f..4ec935d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ dependencies { compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:design:23.1.1' - compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' + compile 'ch.dissem.jabit:jabit-core:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-cryptography-spongy:0.2.1-SNAPSHOT' compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' From 6ec25cfae735aaef269ed4b068607e67761255b9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 17 Jan 2016 07:11:39 +0100 Subject: [PATCH 029/110] Minor improvements --- app/src/main/java/ch/dissem/apps/abit/MainActivity.java | 1 - .../java/ch/dissem/apps/abit/repository/AndroidInventory.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 5bda190..ca8ec07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -43,7 +43,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Objects; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index 86aa1f7..aa20b3e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -96,6 +96,7 @@ public class AndroidInventory implements Inventory { @Override public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { offer.removeAll(getInventory(true, streams)); + LOG.info(offer.size() + " objects missing."); return offer; } From 6f6a2e4e6cb02625f59882c778e708f078fb39cd Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 19 Jan 2016 07:43:48 +0100 Subject: [PATCH 030/110] Minor bug fixes and improvements, added caching to inventory --- .../ch/dissem/apps/abit/MainActivity.java | 10 ++ .../apps/abit/SubscriptionDetailFragment.java | 11 +- .../abit/repository/AndroidInventory.java | 121 +++++++++++------- .../apps/abit/service/BitmessageService.java | 4 +- .../dissem/apps/abit/service/Singleton.java | 8 +- app/src/main/res/drawable/public_key.xml | 8 ++ .../res/layout/fragment_contact_detail.xml | 121 +++++++++++------- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 186 insertions(+), 101 deletions(-) create mode 100644 app/src/main/res/drawable/public_key.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index ca8ec07..c6e9296 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -188,6 +188,16 @@ public class MainActivity extends AppCompatActivity .withTag(identity) ); } + if (profiles.isEmpty()){ + // Create an initial identity + BitmessageAddress identity = Singleton.getIdentity(this); + profiles.add(new ProfileDrawerItem() + .withIcon(new Identicon(identity)) + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity) + ); + } profiles.add(new ProfileSettingDrawerItem() .withName("Add Identity") .withDescription("Create new identity") diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java index 6d7c3b4..955531f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java @@ -100,7 +100,8 @@ public class SubscriptionDetailFragment extends Fragment { TextView address = (TextView) rootView.findViewById(R.id.address); address.setText(item.getAddress()); address.setSelected(true); - ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity().getString(R.string.stream_number, item.getStream())); + ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity() + .getString(R.string.stream_number, item.getStream())); Switch active = (Switch) rootView.findViewById(R.id.active); active.setChecked(item.isSubscribed()); active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @@ -109,6 +110,14 @@ public class SubscriptionDetailFragment extends Fragment { item.setSubscribed(isChecked); } }); + + ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id.pubkey_available); + if (item.getPubkey() == null) { + pubkeyAvailableImg.setAlpha(0.3f); + TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id + .pubkey_available_desc); + pubkeyAvailableDesc.setText(R.string.pubkey_not_available); + } } return rootView; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index aa20b3e..b7fccaf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -33,10 +33,18 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import static ch.dissem.apps.abit.repository.SqlHelper.join; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; /** @@ -55,47 +63,64 @@ public class AndroidInventory implements Inventory { private final SqlHelper sql; + private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>(); + public AndroidInventory(SqlHelper sql) { this.sql = sql; } @Override public List<InventoryVector> getInventory(long... streams) { - return getInventory(false, streams); + List<InventoryVector> result = new LinkedList<>(); + long now = now(); + for (long stream : streams) { + for (Map.Entry<InventoryVector, Long> e : getCache(stream).entrySet()) { + if (e.getValue() > now) { + result.add(e.getKey()); + } + } + } + return result; } - public List<InventoryVector> getInventory(boolean includeExpired, long... streams) { - // Define a projection that specifies which columns from the database - // you will actually use after this query. - String[] projection = { - COLUMN_HASH - }; + private Map<InventoryVector, Long> getCache(long stream) { + Map<InventoryVector, Long> result = cache.get(stream); + if (result == null) { + synchronized (cache) { + if (cache.get(stream) == null) { + result = new ConcurrentHashMap<>(); + cache.put(stream, result); - SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( - TABLE_NAME, projection, - (includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join - (streams) + ")", - null, null, null, null - ); - List<InventoryVector> result = new LinkedList<>(); - try { - c.moveToFirst(); - while (!c.isAfterLast()) { - byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); - result.add(new InventoryVector(blob)); - c.moveToNext(); + String[] projection = { + COLUMN_HASH, COLUMN_EXPIRES + }; + + SQLiteDatabase db = sql.getReadableDatabase(); + try (Cursor c = db.query( + TABLE_NAME, projection, + "stream = " + stream, + null, null, null, null + )) { + c.moveToFirst(); + while (!c.isAfterLast()) { + byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); + long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES)); + result.put(new InventoryVector(blob), expires); + c.moveToNext(); + } + } + LOG.info("Stream #" + stream + " inventory size: " + result.size()); + } } - } finally { - c.close(); } - LOG.info("Inventory size: " + result.size()); return result; } @Override public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { - offer.removeAll(getInventory(true, streams)); + for (long stream : streams) { + offer.removeAll(getCache(stream).keySet()); + } LOG.info(offer.size() + " objects missing."); return offer; } @@ -110,12 +135,11 @@ public class AndroidInventory implements Inventory { }; SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + try (Cursor c = db.query( TABLE_NAME, projection, "hash = X'" + vector + "'", null, null, null, null - ); - try { + )) { c.moveToFirst(); if (c.isAfterLast()) { LOG.info("Object requested that we don't have. IV: " + vector); @@ -125,8 +149,6 @@ public class AndroidInventory implements Inventory { int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length); - } finally { - c.close(); } } @@ -150,13 +172,12 @@ public class AndroidInventory implements Inventory { } SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( + List<ObjectMessage> result = new LinkedList<>(); + try (Cursor c = db.query( TABLE_NAME, projection, where.toString(), null, null, null, null - ); - List<ObjectMessage> result = new LinkedList<>(); - try { + )) { c.moveToFirst(); while (!c.isAfterLast()) { int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); @@ -165,8 +186,6 @@ public class AndroidInventory implements Inventory { blob.length)); c.moveToNext(); } - } finally { - c.close(); } return result; } @@ -174,6 +193,10 @@ public class AndroidInventory implements Inventory { @Override public void storeObject(ObjectMessage object) { InventoryVector iv = object.getInventoryVector(); + + if (getCache(object.getStream()).containsKey(iv)) + return; + LOG.trace("Storing object " + iv); try { @@ -188,6 +211,8 @@ public class AndroidInventory implements Inventory { values.put(COLUMN_VERSION, object.getVersion()); db.insertOrThrow(TABLE_NAME, null, values); + + getCache(object.getStream()).put(iv, object.getExpiresTime()); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); } catch (IOException e) { @@ -197,22 +222,22 @@ public class AndroidInventory implements Inventory { @Override public boolean contains(ObjectMessage object) { - SQLiteDatabase db = sql.getReadableDatabase(); - Cursor c = db.query( - TABLE_NAME, new String[]{COLUMN_STREAM}, - "hash = X'" + object.getInventoryVector() + "'", - null, null, null, null - ); - try { - return c.getCount() > 0; - } finally { - c.close(); - } + return getCache(object.getStream()).keySet().contains(object.getInventoryVector()); } @Override public void cleanup() { + long fiveMinutesAgo = now() - 5 * MINUTE; SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, "expires < " + (now() - 300), null); + db.delete(TABLE_NAME, "expires < " + fiveMinutesAgo, null); + + for (Map<InventoryVector, Long> c : cache.values()) { + Iterator<Map.Entry<InventoryVector, Long>> iterator = c.entrySet().iterator(); + while (iterator.hasNext()) { + if (iterator.next().getValue() < fiveMinutesAgo) { + iterator.remove(); + } + } + } } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index b5c18f6..691db2c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -56,7 +56,7 @@ public class BitmessageService extends Service { private static Messenger messenger; public static boolean isRunning() { - return running; + return running && bmc.isRunning(); } @Override @@ -102,8 +102,6 @@ public class BitmessageService extends Service { switch (msg.what) { case MSG_CREATE_IDENTITY: { BitmessageAddress identity = bmc.createIdentity(false); - identity.setAlias(service.get().getString(R.string.alias_default_identity)); - bmc.addresses().save(identity); if (msg.replyTo != null) { try { Message message = Message.obtain(this, MSG_CREATE_IDENTITY); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 9c60ecf..50c95da 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -4,6 +4,7 @@ import android.content.Context; import java.util.List; +import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.adapter.AndroidCryptography; import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; import ch.dissem.apps.abit.listener.MessageListener; @@ -92,10 +93,15 @@ public class Singleton { if (identity == null) { synchronized (Singleton.class) { if (identity == null) { - List<BitmessageAddress> identities = getBitmessageContext(ctx).addresses() + BitmessageContext bmc = getBitmessageContext(ctx); + List<BitmessageAddress> identities = bmc.addresses() .getIdentities(); if (identities.size() > 0) { identity = identities.get(0); + } else { + identity = bmc.createIdentity(false); + identity.setAlias(ctx.getString(R.string.alias_default_identity)); + bmc.addresses().save(identity); } } } diff --git a/app/src/main/res/drawable/public_key.xml b/app/src/main/res/drawable/public_key.xml new file mode 100644 index 0000000..d3cea1a --- /dev/null +++ b/app/src/main/res/drawable/public_key.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contact_detail.xml b/app/src/main/res/layout/fragment_contact_detail.xml index 4aa33dd..f8b4bda 100644 --- a/app/src/main/res/layout/fragment_contact_detail.xml +++ b/app/src/main/res/layout/fragment_contact_detail.xml @@ -16,65 +16,90 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" - android:src="@color/colorAccent" - android:layout_margin="16dp"/> + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorAccent" + tools:ignore="ContentDescription"/> <EditText - android:id="@+id/name" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:text="Name" - android:layout_alignTop="@+id/avatar" - android:layout_toRightOf="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:layout_marginRight="16dp" - android:layout_marginEnd="16dp" - android:layout_alignParentRight="true" - android:layout_alignParentEnd="true"/> + android:id="@+id/name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignTop="@+id/avatar" + android:layout_marginEnd="16dp" + android:layout_toEndOf="@+id/avatar" + android:text="" + android:inputType="textPersonName" + tools:ignore="LabelFor"/> <TextView - android:id="@+id/address" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Address" - android:lines="1" - android:textAppearance="?android:attr/textAppearanceSmall" - android:layout_below="@+id/avatar" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:textStyle="bold" - /> + android:id="@+id/address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/avatar" + android:lines="1" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:text="BM-XyYxXyYxXyYxXyYxXyYx" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textStyle="bold" + tools:ignore="HardcodedText"/> <TextView - android:id="@+id/stream_number" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Stream #" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:layout_below="@+id/address"/> + android:id="@+id/stream_number" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/address" + android:ellipsize="end" + android:lines="1" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:text="Stream #" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:ignore="HardcodedText"/> <Switch - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/subscribed" - android:id="@+id/active" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:layout_below="@+id/stream_number"/> + android:id="@+id/active" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/stream_number" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="16dp" + android:text="@string/subscribed"/> + + <ImageView + android:id="@+id/pubkey_available" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_below="@+id/active" + android:paddingStart="16dp" + android:paddingEnd="4dp" + android:paddingTop="16dp" + android:src="@drawable/public_key" + tools:ignore="ContentDescription"/> + + <TextView + android:id="@+id/pubkey_available_desc" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingStart="0dp" + android:paddingEnd="16dp" + android:layout_alignBottom="@id/pubkey_available" + android:layout_toEndOf="@id/pubkey_available" + android:layout_alignParentEnd="true" + android:text="@string/pubkey_available" + android:textAppearance="?android:attr/textAppearanceSmall"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 279491a..bc0d062 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -56,4 +56,6 @@ <string name="alias_default_identity">Ich</string> <string name="title_activity_status">Debugging</string> <string name="status_summary">Technische Infos</string> + <string name="pubkey_available">Öffentlicher Schlüssel verfügbar</string> + <string name="pubkey_not_available">Öffentlicher Schlüssel noch nicht verfügbar</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83c5d60..7a4e21c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,4 +59,6 @@ <string name="status">Debugging</string> <string name="status_summary">Technical information</string> <string name="alias_default_identity">Me</string> + <string name="pubkey_available">Public key available</string> + <string name="pubkey_not_available">Public key not yet available</string> </resources> From c4d76fc8ce6022f20dbde368968b596190db166c Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 19 Jan 2016 20:50:58 +0100 Subject: [PATCH 031/110] Code cleanup - most notably BitmessageService was cleaned up and doesn't use messaging anymore --- app/src/main/AndroidManifest.xml | 9 +- .../apps/abit/AbstractItemListFragment.java | 13 +- .../apps/abit/ComposeMessageActivity.java | 18 ++- .../apps/abit/ComposeMessageFragment.java | 17 +- .../java/ch/dissem/apps/abit/Identicon.java | 6 +- .../ch/dissem/apps/abit/MainActivity.java | 144 ++++++----------- .../apps/abit/MessageDetailActivity.java | 1 + .../apps/abit/MessageDetailFragment.java | 45 ++++-- .../dissem/apps/abit/MessageListFragment.java | 16 ++ .../apps/abit/OpenBitmessageLinkActivity.java | 71 +-------- .../ch/dissem/apps/abit/SettingsActivity.java | 1 + .../ch/dissem/apps/abit/SettingsFragment.java | 16 ++ .../ch/dissem/apps/abit/StatusActivity.java | 18 ++- .../apps/abit/SubscriptionDetailActivity.java | 1 + .../abit/adapter/AndroidCryptography.java | 37 ++--- .../apps/abit/adapter/ContactAdapter.java | 16 ++ .../adapter/SwitchingProofOfWorkEngine.java | 19 ++- .../apps/abit/listener/ActionBarListener.java | 2 +- .../abit/listener/ListSelectionListener.java | 2 +- .../apps/abit/listener/MessageListener.java | 41 +---- .../apps/abit/listener/WifiReceiver.java | 16 ++ .../notification/AbstractNotification.java | 16 ++ .../abit/notification/ErrorNotification.java | 21 ++- .../notification/NetworkNotification.java | 16 ++ .../notification/NewMessageNotification.java | 45 +++++- .../notification/ProofOfWorkNotification.java | 16 ++ .../dissem/apps/abit/pow/ServerPowEngine.java | 16 ++ .../abit/repository/AndroidInventory.java | 26 ++-- .../AndroidProofOfWorkRepository.java | 16 ++ .../apps/abit/service/BitmessageService.java | 146 ++++-------------- .../apps/abit/service/ProofOfWorkService.java | 16 ++ .../apps/abit/service/ServicePowEngine.java | 16 ++ .../dissem/apps/abit/service/Singleton.java | 16 ++ .../abit/synchronization/Authenticator.java | 16 ++ .../synchronization/AuthenticatorService.java | 16 ++ .../abit/synchronization/StubProvider.java | 27 +++- .../abit/synchronization/SyncAdapter.java | 16 ++ .../abit/synchronization/SyncService.java | 20 ++- .../java/ch/dissem/apps/abit/util/Assets.java | 9 -- .../ch/dissem/apps/abit/util/Constants.java | 16 ++ .../ch/dissem/apps/abit/util/Drawables.java | 2 +- .../ch/dissem/apps/abit/util/PRNGFixes.java | 7 +- .../ch/dissem/apps/abit/util/Preferences.java | 16 ++ .../layout-w720dp/activity_message_list.xml | 4 +- .../main/res/layout/activity_message_list.xml | 4 +- .../layout/activity_open_bitmessage_link.xml | 7 +- app/src/main/res/layout/activity_status.xml | 3 +- app/src/main/res/layout/contact_row.xml | 15 +- .../res/layout/fragment_contact_detail.xml | 10 +- .../main/res/layout/fragment_contact_list.xml | 2 - .../res/layout/fragment_message_detail.xml | 20 ++- .../main/res/layout/fragment_message_list.xml | 2 - app/src/main/res/layout/message_row.xml | 14 +- app/src/main/res/layout/subscription_row.xml | 14 +- app/src/main/res/layout/toolbar_layout.xml | 6 +- app/src/main/res/menu/compose.xml | 2 +- app/src/main/res/menu/main.xml | 4 - .../main/res/values/library_jabit_strings.xml | 2 +- 58 files changed, 661 insertions(+), 468 deletions(-) delete mode 100644 app/src/main/res/menu/main.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5738a48..1eb392b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ <uses-permission android:name="android.permission.WRITE_CONTACTS"/> <application - android:allowBackup="true" + android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> @@ -114,7 +114,9 @@ android:exported="false" android:syncable="true"/> - <service android:name=".synchronization.AuthenticatorService"> + <service + android:name=".synchronization.AuthenticatorService" + tools:ignore="ExportedService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator"/> </intent-filter> @@ -125,7 +127,8 @@ </service> <service android:name=".synchronization.SyncService" - android:exported="true"> + android:exported="true" + tools:ignore="ExportedService"> <intent-filter> <action android:name="android.content.SyncAdapter"/> </intent-filter> diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 7b03a55..5895277 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -16,7 +16,6 @@ package ch.dissem.apps.abit; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v4.app.ListFragment; @@ -27,7 +26,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.bitmessage.entity.valueobject.Label; /** - * Created by chris on 07.09.15. + * @author Christian Basler */ public abstract class AbstractItemListFragment<T> extends ListFragment { /** @@ -39,7 +38,8 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { * A dummy implementation of the {@link ListSelectionListener} interface that does * nothing. Used only when this fragment is not attached to an activity. */ - private static ListSelectionListener<Object> dummyCallbacks = new ListSelectionListener<Object>() { + private static ListSelectionListener<Object> dummyCallbacks = new + ListSelectionListener<Object>() { @Override public void onItemSelected(Object plaintext) { } @@ -84,11 +84,13 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { super.onAttach(context); // Activities containing this fragment must implement its callbacks. - if (!(context instanceof ListSelectionListener)) { + if (context instanceof ListSelectionListener) { + //noinspection unchecked + callbacks = (ListSelectionListener) context; + } else { throw new IllegalStateException("Activity must implement fragment's callbacks."); } - callbacks = (ListSelectionListener) context; } @Override @@ -105,6 +107,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { // Notify the active callbacks interface (the activity, if the // fragment is attached to one) that an item has been selected. + //noinspection unchecked callbacks.onItemSelected((T) listView.getItemAtPosition(position)); } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index a8f35fc..f6a2e38 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -1,9 +1,24 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import ch.dissem.bitmessage.entity.BitmessageAddress; /** * Compose a new message. @@ -20,6 +35,7 @@ public class ComposeMessageActivity extends AppCompatActivity { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + //noinspection ConstantConditions getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(false); diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index e262d29..9184996 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.os.Bundle; @@ -8,7 +24,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.EditText; diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index 08ad67e..288e27e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -16,13 +16,12 @@ package ch.dissem.apps.abit; - import android.graphics.*; import android.graphics.drawable.Drawable; import ch.dissem.bitmessage.entity.BitmessageAddress; /** - * + * @author Christian Basler */ public class Identicon extends Drawable { private static final int SIZE = 9; @@ -34,7 +33,6 @@ public class Identicon extends Drawable { private float cellWidth; private float cellHeight; - private byte[] hash; private int color; private int background; private boolean[][] fields; @@ -44,7 +42,7 @@ public class Identicon extends Drawable { paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); - hash = input.getRipe(); + byte[] hash = input.getRipe(); fields = new boolean[SIZE][SIZE]; color = Color.HSVToColor(new float[]{Math.abs(hash[0] * hash[1] + hash[2]) % 360, 0.8f, 1.0f}); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index c6e9296..f493cb3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.app.AlertDialog; @@ -7,11 +23,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -39,7 +51,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -47,18 +58,16 @@ import java.util.List; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; +import ch.dissem.apps.abit.service.BitmessageService.BitmessageBinder; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_IDENTITY; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_START_NODE; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_STOP_NODE; +import static ch.dissem.apps.abit.service.BitmessageService.isRunning; /** @@ -92,14 +101,12 @@ public class MainActivity extends AppCompatActivity */ private boolean twoPane; - private static IncomingHandler incomingHandler = new IncomingHandler(); - private static Messenger messenger = new Messenger(incomingHandler); - private static Messenger service; + private static BitmessageBinder service; private static boolean bound; private static ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - MainActivity.service = new Messenger(service); + MainActivity.service = (BitmessageBinder) service; MainActivity.bound = true; } @@ -112,16 +119,15 @@ public class MainActivity extends AppCompatActivity private Label selectedLabel; - private MessageRepository messageRepo; - private AddressRepository addressRepo; + private BitmessageContext bmc; private BitmessageAddress selectedIdentity; + private AccountHeader accountHeader; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - messageRepo = Singleton.getMessageRepository(this); - addressRepo = Singleton.getAddressRepository(this); - List<Label> labels = messageRepo.getLabels(); + bmc = Singleton.getBitmessageContext(this); + List<Label> labels = bmc.messages().getLabels(); if (selectedLabel == null) { selectedLabel = labels.get(0); } @@ -179,7 +185,7 @@ public class MainActivity extends AppCompatActivity private void createDrawer(Toolbar toolbar, Collection<Label> labels) { final ArrayList<IProfile> profiles = new ArrayList<>(); - for (BitmessageAddress identity : addressRepo.getIdentities()) { + for (BitmessageAddress identity : bmc.addresses().getIdentities()) { LOG.info("Adding identity " + identity.getAddress()); profiles.add(new ProfileDrawerItem() .withIcon(new Identicon(identity)) @@ -188,7 +194,7 @@ public class MainActivity extends AppCompatActivity .withTag(identity) ); } - if (profiles.isEmpty()){ + if (profiles.isEmpty()) { // Create an initial identity BitmessageAddress identity = Singleton.getIdentity(this); profiles.add(new ProfileDrawerItem() @@ -212,7 +218,7 @@ public class MainActivity extends AppCompatActivity .withIcon(GoogleMaterial.Icon.gmd_settings) ); // Create the AccountHeader - AccountHeader accountHeader = new AccountHeaderBuilder() + accountHeader = new AccountHeaderBuilder() .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) @@ -221,13 +227,18 @@ public class MainActivity extends AppCompatActivity public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { if (profile.getIdentifier() == ADD_IDENTITY) { - try { - Message message = Message.obtain(null, BitmessageService - .MSG_CREATE_IDENTITY); - message.replyTo = messenger; - service.send(message); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); + BitmessageAddress identity = bmc.createIdentity(false); + IProfile newProfile = new ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + // we know that there are 2 setting elements. + // Set the new profile above them ;) + accountHeader.addProfile( + newProfile, accountHeader.getProfiles().size() - 2); + } else { + accountHeader.addProfiles(newProfile); } } else if (profile instanceof ProfileDrawerItem) { Object tag = ((ProfileDrawerItem) profile).getTag(); @@ -243,7 +254,6 @@ public class MainActivity extends AppCompatActivity if (profiles.size() > 2) { // There's always the add and manage identity items accountHeader.setActiveProfile(profiles.get(0), true); } - incomingHandler.updateAccountHeader(accountHeader); ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); for (Label label : labels) { @@ -299,23 +309,16 @@ public class MainActivity extends AppCompatActivity new SwitchDrawerItem() .withName(R.string.full_node) .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) - .withChecked(BitmessageService.isRunning()) + .withChecked(isRunning()) .withOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { - if (messenger != null) { - if (isChecked) { - checkAndStartNode(buttonView); - } else { - try { - service.send(Message.obtain(null, - MSG_STOP_NODE)); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); - } - } + if (isChecked) { + checkAndStartNode(buttonView); + } else { + service.shutdownNode(); } } }) @@ -361,15 +364,17 @@ public class MainActivity extends AppCompatActivity } private void checkAndStartNode(final CompoundButton buttonView) { + if (service == null) return; + if (Preferences.isConnectionAllowed(MainActivity.this)) { - forceStartNode(); + service.startupNode(); } else { new AlertDialog.Builder(MainActivity.this) .setMessage(R.string.full_node_warning) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - forceStartNode(); + service.startupNode(); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { @@ -382,15 +387,6 @@ public class MainActivity extends AppCompatActivity } } - private void forceStartNode() { - try { - service.send(Message.obtain(null, - MSG_START_NODE)); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); - } - } - private void showSelectedLabel() { if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { @@ -476,46 +472,4 @@ public class MainActivity extends AppCompatActivity public BitmessageAddress getSelectedIdentity() { return selectedIdentity; } - - private static class IncomingHandler extends Handler { - private WeakReference<AccountHeader> accountHeaderRef; - - private IncomingHandler() { - accountHeaderRef = new WeakReference<>(null); - } - - public void updateAccountHeader(AccountHeader accountHeader) { - accountHeaderRef = new WeakReference<>(accountHeader); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case BitmessageService.MSG_CREATE_IDENTITY: { - AccountHeader accountHeader = accountHeaderRef.get(); - if (accountHeader == null) break; - - Serializable data = msg.getData().getSerializable(DATA_FIELD_IDENTITY); - if (data instanceof BitmessageAddress) { - BitmessageAddress identity = (BitmessageAddress) data; - IProfile newProfile = new ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - //we know that there are 2 setting elements. set the new profile - // above them ;) - accountHeader.addProfile(newProfile, accountHeader.getProfiles().size - () - 2); - } else { - accountHeader.addProfiles(newProfile); - } - } - break; - } - default: - super.handleMessage(msg); - } - } - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java index 22012c5..77be840 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java @@ -27,6 +27,7 @@ public class MessageDetailActivity extends AppCompatActivity { final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // Show the Up button in the action bar. + //noinspection ConstantConditions getSupportActionBar().setDisplayHomeAsUpEnabled(true); // savedInstanceState is non-null when there is fragment state diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 438e2f2..21a9d3f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.content.Intent; @@ -79,7 +95,8 @@ public class MessageDetailFragment extends Fragment { if (item != null) { ((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); BitmessageAddress sender = item.getFrom(); - ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(sender)); + ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon + (sender)); ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString()); if (item.getTo() != null) { ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); @@ -99,18 +116,18 @@ public class MessageDetailFragment extends Fragment { messageBody.setLinksClickable(true); messageBody.setTextIsSelectable(true); - } - boolean removed = false; - Iterator<Label> labels = item.getLabels().iterator(); - while (labels.hasNext()) { - if (labels.next().getType() == Label.Type.UNREAD) { - labels.remove(); - removed = true; + boolean removed = false; + Iterator<Label> labels = item.getLabels().iterator(); + while (labels.hasNext()) { + if (labels.next().getType() == Label.Type.UNREAD) { + labels.remove(); + removed = true; + } + } + if (removed) { + Singleton.getMessageRepository(inflater.getContext()).save(item); } - } - if (removed) { - Singleton.getMessageRepository(inflater.getContext()).save(item); } return rootView; } @@ -121,7 +138,8 @@ public class MessageDetailFragment extends Fragment { Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply); Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); - Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon.gmd_markunread); + Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon + .gmd_markunread); Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive); super.onCreateOptionsMenu(menu, inflater); @@ -132,7 +150,8 @@ public class MessageDetailFragment extends Fragment { MessageRepository messageRepo = Singleton.getMessageRepository(getContext()); switch (menuItem.getItemId()) { case R.id.reply: - Intent replyIntent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); + Intent replyIntent = new Intent(getActivity().getApplicationContext(), + ComposeMessageActivity.class); replyIntent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item.getFrom()); replyIntent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, item.getTo()); startActivity(replyIntent); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index e844320..c4b0d1f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.content.Intent; diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java index 138ab77..9f2fd4c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java @@ -17,16 +17,8 @@ package ch.dissem.apps.abit; import android.app.Activity; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; @@ -34,35 +26,11 @@ import android.widget.EditText; import android.widget.Switch; import android.widget.TextView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ch.dissem.apps.abit.service.BitmessageService; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; -import static ch.dissem.apps.abit.service.BitmessageService.DATA_FIELD_ADDRESS; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_ADD_CONTACT; -import static ch.dissem.apps.abit.service.BitmessageService.MSG_SUBSCRIBE; - public class OpenBitmessageLinkActivity extends AppCompatActivity { - private static final Logger LOG = LoggerFactory.getLogger(OpenBitmessageLinkActivity.class); - - private Messenger service; - private boolean bound; - private ServiceConnection connection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - OpenBitmessageLinkActivity.this.service = new Messenger(service); - OpenBitmessageLinkActivity.this.bound = true; - } - - @Override - public void onServiceDisconnected(ComponentName name) { - service = null; - bound = false; - } - }; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -103,20 +71,11 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { BitmessageAddress bmAddress = new BitmessageAddress(address); bmAddress.setAlias(label.getText().toString()); - final int what; - if (subscribe.isChecked()) - what = MSG_SUBSCRIBE; - else - what = MSG_ADD_CONTACT; - - try { - Message message = Message.obtain(null, what); - Bundle bundle = new Bundle(); - bundle.putSerializable(DATA_FIELD_ADDRESS, bmAddress); - message.setData(bundle); - service.send(message); - } catch (RemoteException e) { - LOG.error(e.getMessage(), e); + BitmessageContext bmc = Singleton.getBitmessageContext(OpenBitmessageLinkActivity + .this); + bmc.addContact(bmAddress); + if (subscribe.isChecked()) { + bmc.addSubscribtion(bmAddress); } setResult(Activity.RESULT_OK); @@ -150,20 +109,4 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { return new String[0]; } } - - @Override - protected void onStart() { - super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context - .BIND_AUTO_CREATE); - } - - @Override - protected void onStop() { - if (bound) { - unbindService(connection); - bound = false; - } - super.onStop(); - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index f7f7acb..9199c42 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -16,6 +16,7 @@ public class SettingsActivity extends AppCompatActivity { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + //noinspection ConstantConditions getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(false); diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index dc638e7..60ecbeb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.content.Context; diff --git a/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java index 60886b2..1babcf6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java @@ -1,7 +1,22 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; import android.os.Bundle; -import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.widget.TextView; @@ -20,6 +35,7 @@ public class StatusActivity extends AppCompatActivity { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + //noinspection ConstantConditions getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(false); diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java index f8c8faf..cc83451 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java @@ -43,6 +43,7 @@ public class SubscriptionDetailActivity extends AppCompatActivity { final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // Show the Up button in the action bar. + //noinspection ConstantConditions getSupportActionBar().setDisplayHomeAsUpEnabled(true); // savedInstanceState is non-null when there is fragment state diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java index 8aed5be..07c7e61 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AndroidCryptography.java @@ -1,28 +1,23 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.adapter; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import ch.dissem.apps.abit.util.PRNGFixes; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.PlaintextHolder; -import ch.dissem.bitmessage.entity.payload.Broadcast; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography; -import ch.dissem.bitmessage.utils.UnixTime; - -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; -import static ch.dissem.bitmessage.entity.Plaintext.Status.SENT; -import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; /** * @author Christian Basler diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java index 2d0946e..1951985 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.adapter; import android.content.Context; diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java index d129e07..3e0d545 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwitchingProofOfWorkEngine.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.adapter; import android.content.Context; @@ -6,12 +22,9 @@ import android.preference.PreferenceManager; import java.util.Arrays; -import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; -import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; - /** * Switches between two {@link ProofOfWorkEngine}s depending on the configuration. * diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java index 7dfcdf2..d1e1ea4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java @@ -17,7 +17,7 @@ package ch.dissem.apps.abit.listener; /** - * Created by chris on 06.09.15. + * @author Christian Basler */ public interface ActionBarListener { void updateTitle(CharSequence title); diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java index 03fdc19..42e4732 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/ListSelectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2016 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index 16acfbd..b25fdfc 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -16,60 +16,32 @@ package ch.dissem.apps.abit.listener; -import android.annotation.TargetApi; -import android.app.NotificationManager; import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.ContactsContract; -import android.support.v7.app.NotificationCompat; + +import java.util.Deque; +import java.util.LinkedList; import ch.dissem.apps.abit.notification.NewMessageNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.Plaintext; -import java.util.LinkedList; - /** * Listens for decrypted Bitmessage messages. Does show a notification. * <p> - * Should show a notification when the app isn't running, but update the message list when it is. Also, + * Should show a notification when the app isn't running, but update the message list when it is. + * Also, * notifications should be combined. * </p> */ public class MessageListener implements BitmessageContext.Listener { - private final Context ctx; - private final NotificationManager manager; - private final LinkedList<Plaintext> unacknowledged = new LinkedList<>(); + private final Deque<Plaintext> unacknowledged = new LinkedList<>(); private int numberOfUnacknowledgedMessages = 0; private final NewMessageNotification notification; public MessageListener(Context ctx) { - this.ctx = ctx.getApplicationContext(); - this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); - this.notification = new NewMessageNotification(ctx); } - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - public static int getMaxContactPhotoSize(final Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - // Note that this URI is safe to call on the UI thread. - final Uri uri = ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI; - final String[] projection = new String[]{ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM}; - final Cursor c = context.getContentResolver().query(uri, projection, null, null, null); - try { - c.moveToFirst(); - return c.getInt(0); - } finally { - c.close(); - } - } - // fallback: 96x96 is the max contact photo size for pre-ICS versions - return 96; - } - @Override public void receive(final Plaintext plaintext) { synchronized (unacknowledged) { @@ -80,7 +52,6 @@ public class MessageListener implements BitmessageContext.Listener { } } - NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); if (numberOfUnacknowledgedMessages == 1) { notification.singleNotification(plaintext); } else { diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java index 74df713..e4ea8e0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.listener; import android.content.BroadcastReceiver; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java index 21ff505..7573dc9 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.notification; import android.app.Notification; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java index c64185e..0efdc66 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.notification; import android.content.Context; @@ -7,7 +23,10 @@ import android.support.v7.app.NotificationCompat; import ch.dissem.apps.abit.R; /** - * Created by chrigu on 29.10.15. + * Easily create notifications with error messages. Use carefully, users probably won't like them. + * (But they are useful during development/testing) + * + * @author Christian Basler */ public class ErrorNotification extends AbstractNotification { public static final int ERROR_NOTIFICATION_ID = 4; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 6236546..d11c0c9 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.notification; import android.annotation.SuppressLint; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index efe1f8e..246ea6d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.notification; import android.app.PendingIntent; @@ -10,7 +26,7 @@ import android.text.SpannableString; import android.text.Spanned; import android.text.style.StyleSpan; -import java.util.LinkedList; +import java.util.Collection; import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.MainActivity; @@ -29,8 +45,10 @@ public class NewMessageNotification extends AbstractNotification { public NewMessageNotification singleNotification(Plaintext plaintext) { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); - Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText()); - bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText + ()); + bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned + .SPAN_INCLUSIVE_EXCLUSIVE); builder.setSmallIcon(R.drawable.ic_notification_new_message) .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192)) .setContentTitle(plaintext.getFrom().toString()) @@ -40,27 +58,38 @@ public class NewMessageNotification extends AbstractNotification { Intent showMessageIntent = new Intent(ctx, MainActivity.class); showMessageIntent.putExtra(MainActivity.EXTRA_SHOW_MESSAGE, plaintext); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, + PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent); - builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), pendingIntent); + builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), + pendingIntent); notification = builder.build(); return this; } - public NewMessageNotification multiNotification(LinkedList<Plaintext> unacknowledged, int numberOfUnacknowledgedMessages) { + /** + * @param unacknowledged will be accessed from different threads, so make sure wherever it's + * accessed it will be in a <code>synchronized(unacknowledged) + * {}</code> block + */ + public NewMessageNotification multiNotification(Collection<Plaintext> unacknowledged, int + numberOfUnacknowledgedMessages) { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_new_message) .setContentTitle(ctx.getString(R.string.n_new_messages, unacknowledged.size())) .setContentText(ctx.getString(R.string.app_name)); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (unacknowledged) { - inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)); + inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, + numberOfUnacknowledgedMessages)); for (Plaintext msg : unacknowledged) { Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); - sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable + .SPAN_INCLUSIVE_EXCLUSIVE); inboxStyle.addLine(sb); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java index b0b33b4..f677130 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.notification; import android.app.PendingIntent; diff --git a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java index a8b76af..1b94aa5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.pow; import android.content.Context; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index b7fccaf..c88ca2a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -21,6 +21,17 @@ import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; @@ -28,21 +39,6 @@ import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.Inventory; import ch.dissem.bitmessage.utils.Encode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import static ch.dissem.apps.abit.repository.SqlHelper.join; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index 6a96eee..6ef5cd0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.repository; import android.content.ContentValues; diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 691db2c..d2e04d8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -1,26 +1,31 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.service; import android.app.Service; -import android.content.Context; import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; +import android.os.Binder; import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.Serializable; -import java.lang.ref.WeakReference; - -import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; @@ -32,19 +37,6 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIF public class BitmessageService extends Service { public static final Logger LOG = LoggerFactory.getLogger(BitmessageService.class); - public static final int MSG_CREATE_IDENTITY = 10; - public static final int MSG_SUBSCRIBE = 20; - public static final int MSG_ADD_CONTACT = 21; - public static final int MSG_SEND_MESSAGE = 30; - public static final int MSG_SEND_BROADCAST = 31; - public static final int MSG_START_NODE = 100; - public static final int MSG_STOP_NODE = 101; - - public static final String DATA_FIELD_IDENTITY = "identity"; - public static final String DATA_FIELD_ADDRESS = "address"; - public static final String DATA_FIELD_SUBJECT = "subject"; - public static final String DATA_FIELD_MESSAGE = "message"; - // Object to use as a thread-safe lock private static final Object lock = new Object(); @@ -53,8 +45,6 @@ public class BitmessageService extends Service { private static volatile boolean running = false; - private static Messenger messenger; - public static boolean isRunning() { return running && bmc.isRunning(); } @@ -65,7 +55,6 @@ public class BitmessageService extends Service { if (bmc == null) { bmc = Singleton.getBitmessageContext(this); notification = new NetworkNotification(this, bmc); - messenger = new Messenger(new IncomingHandler(this)); } } } @@ -81,101 +70,34 @@ public class BitmessageService extends Service { running = false; } + /** * Return an object that allows the system to invoke * the sync adapter. */ @Override public IBinder onBind(Intent intent) { - return messenger.getBinder(); + return new BitmessageBinder(); } - private static class IncomingHandler extends Handler { - private WeakReference<BitmessageService> service; - - private IncomingHandler(BitmessageService service) { - this.service = new WeakReference<>(service); + public class BitmessageBinder extends Binder { + public void startupNode() { + startService(new Intent(BitmessageService.this, BitmessageService.class)); + running = true; + startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); + if (!bmc.isRunning()) { + bmc.startup(); + } + notification.show(); } - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_CREATE_IDENTITY: { - BitmessageAddress identity = bmc.createIdentity(false); - if (msg.replyTo != null) { - try { - Message message = Message.obtain(this, MSG_CREATE_IDENTITY); - Bundle bundle = new Bundle(); - bundle.putSerializable(DATA_FIELD_IDENTITY, identity); - message.setData(bundle); - msg.replyTo.send(message); - } catch (RemoteException e) { - LOG.debug(e.getMessage(), e); - } - } - break; - } - case MSG_SUBSCRIBE: { - Serializable data = msg.getData().getSerializable(DATA_FIELD_ADDRESS); - if (data instanceof BitmessageAddress) { - bmc.addSubscribtion((BitmessageAddress) data); - } - break; - } - case MSG_ADD_CONTACT: { - Serializable data = msg.getData().getSerializable(DATA_FIELD_ADDRESS); - if (data instanceof BitmessageAddress) { - bmc.addContact((BitmessageAddress) data); - } - break; - } - case MSG_SEND_MESSAGE: { - Serializable identity = msg.getData().getSerializable(DATA_FIELD_IDENTITY); - Serializable address = msg.getData().getSerializable(DATA_FIELD_ADDRESS); - if (identity instanceof BitmessageAddress - && address instanceof BitmessageAddress) { - String subject = msg.getData().getString(DATA_FIELD_SUBJECT); - String message = msg.getData().getString(DATA_FIELD_MESSAGE); - bmc.send((BitmessageAddress) identity, (BitmessageAddress) address, - subject, message); - } else { - Context ctx = service.get(); - Toast.makeText(ctx, "Could not send", Toast.LENGTH_LONG); - } - break; - } - case MSG_SEND_BROADCAST: { - Serializable data = msg.getData().getSerializable(DATA_FIELD_IDENTITY); - if (data instanceof BitmessageAddress) { - String subject = msg.getData().getString(DATA_FIELD_SUBJECT); - String message = msg.getData().getString(DATA_FIELD_MESSAGE); - bmc.broadcast((BitmessageAddress) data, subject, message); - } - break; - } - case MSG_START_NODE: - // TODO: warn user, option to restrict to WiFi - // (I'm not quite sure this can be done here, though) - service.get().startService(new Intent(service.get(), BitmessageService.class)); - running = true; - service.get().startForeground(ONGOING_NOTIFICATION_ID, notification - .getNotification()); - if (!bmc.isRunning()) { - bmc.startup(); - } - notification.show(); - break; - case MSG_STOP_NODE: - if (bmc.isRunning()) { - bmc.shutdown(); - } - running = false; - service.get().stopForeground(false); - service.get().stopSelf(); - break; - default: - super.handleMessage(msg); + public void shutdownNode() { + if (bmc.isRunning()) { + bmc.shutdown(); } + running = false; + stopForeground(false); + stopSelf(); } } } \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 7cb7087..94b5620 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.service; import android.app.Service; diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java index eaffbdf..f986a7f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.service; import android.content.ComponentName; diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 50c95da..5313927 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.service; import android.content.Context; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java index 009b35d..a9f2dd2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/Authenticator.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.synchronization; import android.accounts.AbstractAccountAuthenticator; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java index 8fe717d..1534e33 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/AuthenticatorService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.synchronization; import android.app.Service; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java index f5b3d2d..1ac156e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java @@ -1,9 +1,26 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.synchronization; import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.support.annotation.NonNull; /* * Define an implementation of ContentProvider that stubs out @@ -25,7 +42,7 @@ public class StubProvider extends ContentProvider { * Return no type for MIME type */ @Override - public String getType(Uri uri) { + public String getType(@NonNull Uri uri) { return null; } @@ -35,7 +52,7 @@ public class StubProvider extends ContentProvider { */ @Override public Cursor query( - Uri uri, + @NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, @@ -47,7 +64,7 @@ public class StubProvider extends ContentProvider { * insert() always returns null (no URI) */ @Override - public Uri insert(Uri uri, ContentValues values) { + public Uri insert(@NonNull Uri uri, ContentValues values) { return null; } @@ -55,7 +72,7 @@ public class StubProvider extends ContentProvider { * delete() always returns "no rows affected" (0) */ @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { return 0; } @@ -63,7 +80,7 @@ public class StubProvider extends ContentProvider { * update() always returns "no rows affected" (0) */ public int update( - Uri uri, + @NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 7655044..0177f37 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.synchronization; import android.accounts.Account; diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java index 1f92159..a11b120 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -1,19 +1,31 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.synchronization; import android.app.Service; import android.content.Intent; import android.os.IBinder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Define a Service that returns an IBinder for the * sync adapter class, allowing the sync adapter framework to call * onPerformSync(). */ public class SyncService extends Service { - private static final Logger LOG = LoggerFactory.getLogger(SyncService.class); // Storage for an instance of the sync adapter private static SyncAdapter syncAdapter = null; // Object to use as a thread-safe lock diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Assets.java b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java index e3e2dc6..db9b54c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Assets.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java @@ -28,15 +28,6 @@ import java.util.Scanner; * Helper class to work with Assets. */ public class Assets { - public static String readToString(Context ctx, String name) { - try { - InputStream in = ctx.getAssets().open(name); - return new Scanner(in, "UTF-8").useDelimiter("\\A").next(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public static List<String> readSqlStatements(Context ctx, String name) { try { InputStream in = ctx.getAssets().open(name); diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java index 20fcbee..bd1817d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Constants.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Constants.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.util; import java.util.regex.Pattern; diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index e392e36..1583a1c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -32,7 +32,7 @@ import com.mikepenz.iconics.IconicsDrawable; */ public class Drawables { public static void addIcon(Context ctx, Menu menu, int menuItem, GoogleMaterial.Icon icon) { - menu.findItem(menuItem).setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.primary_text_default_material_dark).actionBar()); + menu.findItem(menuItem).setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()); } public static Bitmap toBitmap(Identicon identicon, int size) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java index a17d0a2..47fef8b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/PRNGFixes.java @@ -1,4 +1,3 @@ -package ch.dissem.apps.abit.util; /* * This software is provided 'as-is', without any express or implied * warranty. In no event will Google be held liable for any damages @@ -9,6 +8,8 @@ package ch.dissem.apps.abit.util; * freely, as long as the origin is not misrepresented. */ +package ch.dissem.apps.abit.util; + import android.os.Build; import android.os.Process; import android.util.Log; @@ -78,7 +79,7 @@ public final class PRNGFixes { // Mix in the device- and invocation-specific seed. Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") .getMethod("RAND_seed", byte[].class) - .invoke(null, generateSeed()); + .invoke(null, (Object) generateSeed()); // Mix output of Linux PRNG into OpenSSL's PRNG int bytesRead = (Integer) Class.forName( @@ -169,6 +170,7 @@ public final class PRNGFixes { * {@link SecureRandomSpi} which passes all requests to the Linux PRNG * ({@code /dev/urandom}). */ + @SuppressWarnings("JavaDoc") public static class LinuxPRNGSecureRandom extends SecureRandomSpi { /* @@ -241,6 +243,7 @@ public final class PRNGFixes { synchronized (sLock) { in = getUrandomInputStream(); } + //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (in) { in.readFully(bytes); } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index df5b1c7..7f67191 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.util; import android.content.Context; diff --git a/app/src/main/res/layout-w720dp/activity_message_list.xml b/app/src/main/res/layout-w720dp/activity_message_list.xml index e955b6b..8ad5fef 100644 --- a/app/src/main/res/layout-w720dp/activity_message_list.xml +++ b/app/src/main/res/layout-w720dp/activity_message_list.xml @@ -1,5 +1,6 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -11,7 +12,8 @@ android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - android:elevation="4dp" /> + android:elevation="4dp" + tools:ignore="UnusedAttribute"/> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" diff --git a/app/src/main/res/layout/activity_message_list.xml b/app/src/main/res/layout/activity_message_list.xml index bb2d440..c30b4cd 100644 --- a/app/src/main/res/layout/activity_message_list.xml +++ b/app/src/main/res/layout/activity_message_list.xml @@ -1,5 +1,6 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -11,7 +12,8 @@ android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - android:elevation="4dp" /> + android:elevation="4dp" + tools:ignore="UnusedAttribute"/> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" diff --git a/app/src/main/res/layout/activity_open_bitmessage_link.xml b/app/src/main/res/layout/activity_open_bitmessage_link.xml index 295dc87..b0aed01 100644 --- a/app/src/main/res/layout/activity_open_bitmessage_link.xml +++ b/app/src/main/res/layout/activity_open_bitmessage_link.xml @@ -9,9 +9,8 @@ android:id="@+id/address" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" - android:text="BM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + tools:text="BM-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" android:textSize="10dp" tools:ignore="SpUsage" /> @@ -19,7 +18,6 @@ android:id="@+id/label_wrapper" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignLeft="@+id/address" android:layout_alignStart="@+id/address" android:layout_below="@+id/address" android:layout_marginTop="16dp"> @@ -37,7 +35,6 @@ android:id="@+id/subscribe" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignLeft="@+id/address" android:layout_alignStart="@+id/address" android:layout_below="@+id/label_wrapper" android:layout_marginBottom="8dp" @@ -50,7 +47,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_below="@+id/subscribe" android:layout_marginBottom="12dp" android:layout_marginTop="12dp" @@ -62,7 +58,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@+id/do_import" - android:layout_toLeftOf="@+id/do_import" android:layout_toStartOf="@+id/do_import" android:text="@string/cancel" /> diff --git a/app/src/main/res/layout/activity_status.xml b/app/src/main/res/layout/activity_status.xml index 2e8d317..6eeca86 100644 --- a/app/src/main/res/layout/activity_status.xml +++ b/app/src/main/res/layout/activity_status.xml @@ -33,7 +33,8 @@ android:title="@string/status" app:title="@string/status" app:layout_scrollFlags="scroll|enterAlways" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + tools:ignore="UnusedAttribute"/> </android.support.design.widget.AppBarLayout> diff --git a/app/src/main/res/layout/contact_row.xml b/app/src/main/res/layout/contact_row.xml index 0f6ad65..c6afc0f 100644 --- a/app/src/main/res/layout/contact_row.xml +++ b/app/src/main/res/layout/contact_row.xml @@ -16,7 +16,7 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -25,41 +25,38 @@ android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:src="@color/colorAccent" - android:layout_margin="16dp"/> + android:layout_margin="16dp" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Name" + tools:text="Name" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_alignTop="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar" android:paddingTop="0dp" android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingBottom="0dp" - android:textStyle="bold" - /> + android:textStyle="bold"/> <TextView android:id="@+id/address" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="BM-2cW0000000000000000000000000000000" + tools:text="BM-2cW0000000000000000000000000000000" android:lines="1" android:ellipsize="marquee" android:textAppearance="?android:attr/textAppearanceSmall" android:paddingLeft="8dp" android:paddingRight="8dp" android:layout_alignBottom="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contact_detail.xml b/app/src/main/res/layout/fragment_contact_detail.xml index f8b4bda..662890d 100644 --- a/app/src/main/res/layout/fragment_contact_detail.xml +++ b/app/src/main/res/layout/fragment_contact_detail.xml @@ -50,10 +50,9 @@ android:lines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - android:text="BM-XyYxXyYxXyYxXyYxXyYx" + tools:text="BM-XyYxXyYxXyYxXyYxXyYx" android:textAppearance="?android:attr/textAppearanceSmall" - android:textStyle="bold" - tools:ignore="HardcodedText"/> + android:textStyle="bold"/> <TextView android:id="@+id/stream_number" @@ -64,9 +63,8 @@ android:lines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - android:text="Stream #" - android:textAppearance="?android:attr/textAppearanceSmall" - tools:ignore="HardcodedText"/> + tools:text="Stream #" + android:textAppearance="?android:attr/textAppearanceSmall"/> <Switch android:id="@+id/active" diff --git a/app/src/main/res/layout/fragment_contact_list.xml b/app/src/main/res/layout/fragment_contact_list.xml index ba540ad..5aa714b 100644 --- a/app/src/main/res/layout/fragment_contact_list.xml +++ b/app/src/main/res/layout/fragment_contact_list.xml @@ -14,7 +14,6 @@ android:scrollbarStyle="outsideOverlay" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentBottom="true"/> @@ -26,6 +25,5 @@ app:elevation="8dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_margin="16dp"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 044dd43..053a8d4 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout @@ -13,15 +14,15 @@ android:id="@+id/subject" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:elegantTextHeight="false" android:enabled="false" android:gravity="center_vertical" android:padding="16dp" - android:text="Subject" - android:textAppearance="?android:attr/textAppearanceLarge" /> + tools:text="Subject" + android:textAppearance="?android:attr/textAppearanceLarge" + tools:ignore="UnusedAttribute"/> <View android:id="@+id/divider" @@ -34,11 +35,11 @@ android:id="@+id/avatar" android:layout_width="40dp" android:layout_height="40dp" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_below="@+id/divider" android:layout_margin="16dp" - android:src="@color/colorAccent" /> + android:src="@color/colorAccent" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/sender" @@ -46,11 +47,10 @@ android:layout_height="20dp" android:layout_alignTop="@+id/avatar" android:layout_toEndOf="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - android:text="Sender" + tools:text="Sender" android:textStyle="bold" /> <TextView @@ -59,17 +59,15 @@ android:layout_height="20dp" android:layout_alignBottom="@+id/avatar" android:layout_toEndOf="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - android:text="Recipient" /> + tools:text="Recipient" /> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_below="@+id/avatar" android:layout_marginLeft="16dp" diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index ba540ad..5aa714b 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -14,7 +14,6 @@ android:scrollbarStyle="outsideOverlay" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentBottom="true"/> @@ -26,6 +25,5 @@ app:elevation="8dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_margin="16dp"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index c3d4f2a..b80d6f1 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -16,6 +16,7 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView @@ -23,21 +24,20 @@ android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:src="@color/colorAccent" - android:layout_margin="16dp"/> + android:layout_margin="16dp" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/sender" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Sender" + tools:text="Sender" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_alignTop="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar" android:layout_marginTop="-5dp" android:paddingTop="0dp" @@ -51,21 +51,20 @@ android:id="@+id/subject" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Subject" + tools:text="Subject" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceSmall" android:paddingLeft="8dp" android:paddingRight="8dp" android:layout_below="@+id/sender" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar"/> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Text" + tools:text="Text" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceSmall" @@ -74,7 +73,6 @@ android:paddingRight="8dp" android:paddingBottom="8dp" android:layout_below="@+id/subject" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/subscription_row.xml b/app/src/main/res/layout/subscription_row.xml index 359edce..70ee7a6 100644 --- a/app/src/main/res/layout/subscription_row.xml +++ b/app/src/main/res/layout/subscription_row.xml @@ -17,6 +17,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -25,21 +26,20 @@ android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:src="@color/colorAccent" - android:layout_margin="16dp"/> + android:layout_margin="16dp" + tools:ignore="ContentDescription"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Name" + tools:text="Name" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_alignTop="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar" android:paddingTop="0dp" android:paddingLeft="8dp" @@ -52,14 +52,13 @@ android:id="@+id/stream_number" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Stream #" + tools:text="Stream #" android:lines="1" android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceSmall" android:paddingLeft="8dp" android:paddingRight="8dp" android:layout_alignBottom="@+id/avatar" - android:layout_toRightOf="@+id/avatar" android:layout_toEndOf="@+id/avatar"/> <com.mikepenz.iconics.view.IconicsImageView @@ -69,8 +68,7 @@ app:iiv_color="@android:color/black" app:iiv_icon="cmd-rss" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_centerVertical="true" - android:layout_marginRight="16dp"/> + android:layout_marginEnd="16dp"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_layout.xml b/app/src/main/res/layout/toolbar_layout.xml index ade8b53..ab60f4a 100644 --- a/app/src/main/res/layout/toolbar_layout.xml +++ b/app/src/main/res/layout/toolbar_layout.xml @@ -1,7 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> @@ -27,7 +28,8 @@ android:elevation="4dp" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:layout_scrollFlags="scroll|enterAlways" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + tools:ignore="UnusedAttribute"/> </android.support.design.widget.AppBarLayout> diff --git a/app/src/main/res/menu/compose.xml b/app/src/main/res/menu/compose.xml index dae8007..922a705 100644 --- a/app/src/main/res/menu/compose.xml +++ b/app/src/main/res/menu/compose.xml @@ -5,5 +5,5 @@ android:id="@+id/send" app:showAsAction="always" android:icon="@drawable/ic_action_send" - android:title="@string/send"/>` + android:title="@string/send"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml deleted file mode 100644 index 9eb7100..0000000 --- a/app/src/main/res/menu/main.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> -</menu> \ No newline at end of file diff --git a/app/src/main/res/values/library_jabit_strings.xml b/app/src/main/res/values/library_jabit_strings.xml index 4bd1769..356e88d 100644 --- a/app/src/main/res/values/library_jabit_strings.xml +++ b/app/src/main/res/values/library_jabit_strings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="define_jabit" translatable="false"></string> + <string name="define_jabit" translatable="false"/> <!-- Author section --> <string name="library_jabit_author" translatable="false">Christian Basler</string> <string name="library_jabit_authorWebsite" translatable="false">dissem.ch</string> From 4a854045e067caeb8e0e09cd08f48aac33d07837 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 21 Jan 2016 20:42:20 +0100 Subject: [PATCH 032/110] Version 1.0-beta bump --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4ec935d..b74b60f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "ch.dissem.apps.abit" minSdkVersion 19 targetSdkVersion 23 - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "1.0-beta" } signingConfigs { release { From 491a8a0ccba6b4b9a229a541966f30d63ceed65a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 22 Jan 2016 09:22:43 +0100 Subject: [PATCH 033/110] Version 1.0-beta v2 fixed sync --- app/build.gradle | 2 +- app/src/main/java/ch/dissem/apps/abit/util/Preferences.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b74b60f..4b3a404 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { applicationId "ch.dissem.apps.abit" minSdkVersion 19 targetSdkVersion 23 - versionCode 2 + versionCode 3 versionName "1.0-beta" } signingConfigs { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index 7f67191..a0cee3f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -42,7 +42,7 @@ public class Preferences { public static boolean useTrustedNode(Context ctx) { String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); - return trustedNode == null || trustedNode.trim().isEmpty(); + return trustedNode != null && !trustedNode.trim().isEmpty(); } /** From adfb3a920a3e09a0cbcd719ac2ae1899b16b9747 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 23 Jan 2016 23:30:51 +0100 Subject: [PATCH 034/110] Fixed problem with proof of work --- app/build.gradle | 11 ++- .../notification/ProofOfWorkNotification.java | 33 ++++--- .../apps/abit/service/ProofOfWorkService.java | 91 +++++++++++-------- .../apps/abit/service/ServicePowEngine.java | 47 +++++----- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 107 insertions(+), 79 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4b3a404..642a542 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { applicationId "ch.dissem.apps.abit" minSdkVersion 19 targetSdkVersion 23 - versionCode 3 + versionCode 5 versionName "1.0-beta" } signingConfigs { @@ -29,16 +29,17 @@ android { } } +ext.jabitVersion = '1.0.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:design:23.1.1' - compile 'ch.dissem.jabit:jabit-core:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-cryptography-spongy:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT' + compile "ch.dissem.jabit:jabit-core:$jabitVersion" + compile "ch.dissem.jabit:jabit-networking:$jabitVersion" + compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion" + compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" compile 'org.slf4j:slf4j-android:1.7.12' diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java index f677130..408bc5d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java @@ -32,23 +32,30 @@ public class ProofOfWorkNotification extends AbstractNotification { public ProofOfWorkNotification(Context ctx) { super(ctx); - NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); - - Intent showMessageIntent = new Intent(ctx, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setUsesChronometer(true) - .setSmallIcon(R.drawable.ic_notification_proof_of_work) - .setContentTitle(ctx.getString(R.string.proof_of_work_title)) - .setContentText(ctx.getString(R.string.proof_of_work_text)) - .setContentIntent(pendingIntent); - - notification = builder.build(); + update(1); } @Override protected int getNotificationId() { return ONGOING_NOTIFICATION_ID; } + + public ProofOfWorkNotification update(int numberOfItems) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); + + Intent showMessageIntent = new Intent(ctx, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setUsesChronometer(true) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_notification_proof_of_work) + .setContentTitle(ctx.getString(R.string.proof_of_work_title)) + .setContentText(ctx.getString(R.string.proof_of_work_text, numberOfItems)) + .setContentIntent(pendingIntent); + + notification = builder.build(); + return this; + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 94b5620..7eee67d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -22,10 +22,8 @@ import android.os.Binder; import android.os.IBinder; import android.support.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.ref.WeakReference; +import java.util.LinkedList; +import java.util.Queue; import ch.dissem.apps.abit.notification.ProofOfWorkNotification; import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; @@ -38,11 +36,12 @@ import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_N * killed by the system before the nonce is found. */ public class ProofOfWorkService extends Service { - public static final Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class); - // Object to use as a thread-safe lock private static final Object lock = new Object(); private static ProofOfWorkEngine engine; + private static boolean calculating; + private static final Queue<PowItem> queue = new LinkedList<>(); + private static ProofOfWorkNotification notification; @Override public void onCreate() { @@ -51,54 +50,74 @@ public class ProofOfWorkService extends Service { engine = new MultiThreadedPOWEngine(); } } + notification = new ProofOfWorkNotification(this); } @Nullable @Override public IBinder onBind(Intent intent) { - return new PowBinder(engine, this); + return new PowBinder(this); } public static class PowBinder extends Binder { - private final ProofOfWorkEngine engine; + private final ProofOfWorkService service; - private PowBinder(ProofOfWorkEngine engine, ProofOfWorkService service) { - this.engine = new EngineWrapper(engine, service); + private PowBinder(ProofOfWorkService service) { + this.service = service; } - public ProofOfWorkEngine getEngine() { - return engine; + public void process(PowItem item) { + synchronized (queue) { + service.startService(new Intent(service, ProofOfWorkService.class)); + service.startForeground(ONGOING_NOTIFICATION_ID, + notification.getNotification()); + if (!calculating) { + calculating = true; + service.calculateNonce(item); + } else { + queue.add(item); + notification.update(queue.size()).show(); + } + } } } - private static class EngineWrapper implements ProofOfWorkEngine { - private final ProofOfWorkNotification notification; - private final ProofOfWorkEngine engine; - private final WeakReference<ProofOfWorkService> serviceRef; - private EngineWrapper(ProofOfWorkEngine engine, ProofOfWorkService service) { - this.engine = engine; - this.serviceRef = new WeakReference<>(service); - this.notification = new ProofOfWorkNotification(service); + static class PowItem { + private final byte[] initialHash; + private final byte[] targetValue; + private final ProofOfWorkEngine.Callback callback; + + PowItem(byte[] initialHash, byte[] targetValue, ProofOfWorkEngine.Callback callback) { + this.initialHash = initialHash; + this.targetValue = targetValue; + this.callback = callback; } + } - @Override - public void calculateNonce(byte[] initialHash, byte[] target, final Callback callback) { - final ProofOfWorkService service = serviceRef.get(); - service.startService(new Intent(service, ProofOfWorkService.class)); - service.startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); - engine.calculateNonce(initialHash, target, new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - try { - callback.onNonceCalculated(initialHash, nonce); - } finally { - service.stopForeground(true); - service.stopSelf(); + private void calculateNonce(final PowItem item) { + engine.calculateNonce(item.initialHash, item.targetValue, new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + try { + item.callback.onNonceCalculated(initialHash, nonce); + } finally { + PowItem item; + synchronized (queue) { + item = queue.poll(); + if (item == null) { + calculating = false; + stopForeground(true); + stopSelf(); + } else { + notification.update(queue.size()).show(); + } + } + if (item != null) { + calculateNonce(item); } } - }); - - } + } + }); } } \ No newline at end of file diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java index f986a7f..7211dcf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -22,9 +22,11 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; -import java.util.concurrent.Semaphore; +import java.util.LinkedList; +import java.util.Queue; import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder; +import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import static android.content.Context.BIND_AUTO_CREATE; @@ -32,12 +34,12 @@ import static android.content.Context.BIND_AUTO_CREATE; /** * Proof of Work engine that uses the Proof of Work service. */ -public class ServicePowEngine implements ProofOfWorkEngine, ProofOfWorkEngine.Callback { - private final Semaphore semaphore = new Semaphore(1, true); +public class ServicePowEngine implements ProofOfWorkEngine { private final Context ctx; - private byte[] initialHash, targetValue; - private Callback callback; + private static final Object lock = new Object(); + private Queue<PowItem> queue = new LinkedList<>(); + private PowBinder service; public ServicePowEngine(Context ctx) { this.ctx = ctx; @@ -46,32 +48,31 @@ public class ServicePowEngine implements ProofOfWorkEngine, ProofOfWorkEngine.Ca private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - ((PowBinder) service).getEngine().calculateNonce(initialHash, targetValue, ServicePowEngine.this); + synchronized (lock) { + ServicePowEngine.this.service = (PowBinder) service; + while (!queue.isEmpty()) { + ServicePowEngine.this.service.process(queue.poll()); + } + } } @Override public void onServiceDisconnected(ComponentName name) { - semaphore.release(); + service = null; } }; @Override public void calculateNonce(byte[] initialHash, byte[] targetValue, Callback callback) { - try { - semaphore.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + PowItem item = new PowItem(initialHash, targetValue, callback); + synchronized (lock) { + if (service != null) { + service.process(item); + } else { + queue.add(item); + ctx.bindService(new Intent(ctx, ProofOfWorkService.class), connection, + BIND_AUTO_CREATE); + } } - this.initialHash = initialHash; - this.targetValue = targetValue; - this.callback = callback; - ctx.bindService(new Intent(ctx, ProofOfWorkService.class), connection, BIND_AUTO_CREATE); } - - @Override - public void onNonceCalculated(byte[] initialHash, byte[] bytes) { - callback.onNonceCalculated(initialHash, bytes); - ctx.unbindService(connection); - } - -} +} \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bc0d062..8a153f1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,7 +42,7 @@ <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> <string name="connection_info_disconnected">Getrennt</string> <string name="connection_info_pending">Verbindung wird aufgebaut…</string> - <string name="proof_of_work_text">Warnung: dies könnte das Gerät erwärmen bis die Batterie leer ist.</string> + <string name="proof_of_work_text">Arbeite am Versenden (%1$d in Warteschlange)</string> <string name="proof_of_work_title">Proof of Work</string> <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a4e21c..82daeff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ <string name="connection_info_disconnected">Disconnected</string> <string name="connection_info_pending">Connecting…</string> <string name="proof_of_work_title">Proof of Work</string> - <string name="proof_of_work_text">Warning: This might heat your device until the battery\'s dead.</string> + <string name="proof_of_work_text">Doing work to send message (%1$d queued)</string> <string name="error_invalid_sync_port">Invalid port in synchronization settings: %s</string> <string name="error_invalid_sync_host">Synchronization failed: Trusted node could not be reached.</string> <string name="compose_body_hint">Write message</string> From 9275f5ca9c100d2643708387be29f122b418f318 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 29 Jan 2016 18:05:43 +0100 Subject: [PATCH 035/110] Address related improvements - QR code is now shown in contact details and 'manage identity' view - Contacts and identities can now be deleted --- app/build.gradle | 4 + app/src/main/AndroidManifest.xml | 2 +- ...tivity.java => AddressDetailActivity.java} | 10 +- .../apps/abit/AddressDetailFragment.java | 259 ++++++++++++++++++ ...Fragment.java => AddressListFragment.java} | 4 +- .../apps/abit/ComposeMessageActivity.java | 1 + .../apps/abit/ComposeMessageFragment.java | 12 +- .../ch/dissem/apps/abit/MainActivity.java | 77 ++++-- .../apps/abit/MessageDetailFragment.java | 11 +- .../dissem/apps/abit/MessageListFragment.java | 2 +- .../apps/abit/SubscriptionDetailFragment.java | 131 --------- .../repository/AndroidAddressRepository.java | 2 +- .../dissem/apps/abit/service/Singleton.java | 6 + ...detail.xml => fragment_address_detail.xml} | 14 + ...act_list.xml => fragment_address_list.xml} | 0 app/src/main/res/menu/address.xml | 33 +++ app/src/main/res/values-de/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 18 files changed, 403 insertions(+), 175 deletions(-) rename app/src/main/java/ch/dissem/apps/abit/{SubscriptionDetailActivity.java => AddressDetailActivity.java} (89%) create mode 100644 app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java rename app/src/main/java/ch/dissem/apps/abit/{SubscriptionListFragment.java => AddressListFragment.java} (96%) delete mode 100644 app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java rename app/src/main/res/layout/{fragment_contact_detail.xml => fragment_address_detail.xml} (86%) rename app/src/main/res/layout/{fragment_contact_list.xml => fragment_address_list.xml} (100%) create mode 100644 app/src/main/res/menu/address.xml diff --git a/app/build.gradle b/app/build.gradle index 642a542..ffa6553 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ dependencies { compile "ch.dissem.jabit:jabit-networking:$jabitVersion" compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion" compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" + compile "ch.dissem.jabit:jabit-wif:$jabitVersion" compile 'org.slf4j:slf4j-android:1.7.12' @@ -51,6 +52,9 @@ dependencies { } compile 'com.mikepenz:iconics:1.6.2@aar' compile 'com.mikepenz:community-material-typeface:1.1.71@aar' + + compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar' + compile 'com.google.zxing:core:3.2.0' } idea.module { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1eb392b..3f83f18 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,7 +38,7 @@ android:value=".MainActivity"/> </activity> <activity - android:name=".SubscriptionDetailActivity" + android:name=".AddressDetailActivity" android:label="@string/title_subscription_detail" android:parentActivityName=".MainActivity" tools:ignore="UnusedAttribute"> diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java similarity index 89% rename from app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java rename to app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java index cc83451..e26749a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java @@ -31,9 +31,9 @@ import android.view.MenuItem; * in a {@link MainActivity}. * <p/> * This activity is mostly just a 'shell' activity containing nothing - * more than a {@link SubscriptionDetailFragment}. + * more than a {@link AddressDetailFragment}. */ -public class SubscriptionDetailActivity extends AppCompatActivity { +public class AddressDetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,9 +59,9 @@ public class SubscriptionDetailActivity extends AppCompatActivity { // Create the detail fragment and add it to the activity // using a fragment transaction. Bundle arguments = new Bundle(); - arguments.putSerializable(SubscriptionDetailFragment.ARG_ITEM, - getIntent().getSerializableExtra(SubscriptionDetailFragment.ARG_ITEM)); - SubscriptionDetailFragment fragment = new SubscriptionDetailFragment(); + arguments.putSerializable(AddressDetailFragment.ARG_ITEM, + getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM)); + AddressDetailFragment fragment = new AddressDetailFragment(); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() .add(R.id.content, fragment) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java new file mode 100644 index 0000000..da08e12 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -0,0 +1,259 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.ShareActionProvider; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Drawables; +import ch.dissem.bitmessage.entity.BitmessageAddress; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; + + +/** + * A fragment representing a single Message detail screen. + * This fragment is either contained in a {@link MainActivity} + * in two-pane mode (on tablets) or a {@link MessageDetailActivity} + * on handsets. + */ +public class AddressDetailFragment extends Fragment { + private static final Logger LOG = LoggerFactory.getLogger(AddressDetailFragment.class); + /** + * The fragment argument representing the item ID that this fragment + * represents. + */ + public static final String ARG_ITEM = "item"; + + private static final int QR_CODE_SIZE = 350; + + private ShareActionProvider shareActionProvider; + /** + * The content this fragment is presenting. + */ + private BitmessageAddress item; + + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public AddressDetailFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments().containsKey(ARG_ITEM)) { + // Load the dummy content specified by the fragment + // arguments. In a real-world scenario, use a Loader + // to load content from a content provider. + item = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM); + } + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.address, menu); + + Drawables.addIcon(getActivity(), menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); + Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); + Drawables.addIcon(getActivity(), menu, R.id.share, GoogleMaterial.Icon.gmd_share); + + shareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider( + menu.findItem(R.id.share)); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + final Activity ctx = getActivity(); + switch (menuItem.getItemId()) { + case R.id.write_message: + Intent intent = new Intent(ctx, ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(ctx)); + intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); + startActivity(intent); + return true; + case R.id.delete: + int warning; + if (item.getPrivateKey() != null) + warning = R.string.delete_identity_warning; + else + warning = R.string.delete_contact_warning; + new AlertDialog.Builder(ctx) + .setMessage(warning) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Singleton.getAddressRepository(ctx).remove(item); + item = null; + ctx.onBackPressed(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + return true; + case R.id.share: + new AlertDialog.Builder(ctx) + .setMessage("I have no fucking clue.") + .show(); + default: + return false; + } + } + + private void setShareIntent(Intent shareIntent) { + if (shareActionProvider != null) { + shareActionProvider.setShareIntent(shareIntent); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_address_detail, container, false); + + // Show the dummy content as text in a TextView. + if (item != null) { + ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); + TextView name = (TextView) rootView.findViewById(R.id.name); + name.setText(item.toString()); + name.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Nothing to do + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Nothing to do + } + + @Override + public void afterTextChanged(Editable s) { + item.setAlias(s.toString()); + } + }); + TextView address = (TextView) rootView.findViewById(R.id.address); + address.setText(item.getAddress()); + address.setSelected(true); + ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity() + .getString(R.string.stream_number, item.getStream())); + if (item.getPrivateKey() == null) { + Switch active = (Switch) rootView.findViewById(R.id.active); + active.setChecked(item.isSubscribed()); + active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + item.setSubscribed(isChecked); + } + }); + + ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id + .pubkey_available); + + if (item.getPubkey() == null) { + pubkeyAvailableImg.setAlpha(0.3f); + TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id + .pubkey_available_desc); + pubkeyAvailableDesc.setText(R.string.pubkey_not_available); + } + } else { + rootView.findViewById(R.id.active).setVisibility(View.GONE); + rootView.findViewById(R.id.pubkey_available).setVisibility(View.GONE); + rootView.findViewById(R.id.pubkey_available_desc).setVisibility(View.GONE); + } + + // QR code + ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); + qrCode.setImageBitmap(encodeAsBitmap(item)); + } + + return rootView; + } + + Bitmap encodeAsBitmap(BitmessageAddress address) { + StringBuilder link = new StringBuilder("bitmessage:"); + link.append(address.getAddress()); + if (address.getAlias() != null) { + link.append("?label=").append(address.getAlias()); + } + BitMatrix result; + try { + result = new MultiFormatWriter().encode(link.toString(), + BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); + } catch (WriterException e) { + LOG.error(e.getMessage(), e); + return null; + } + int w = result.getWidth(); + int h = result.getHeight(); + int[] pixels = new int[w * h]; + for (int y = 0; y < h; y++) { + int offset = y * w; + for (int x = 0; x < w; x++) { + pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; + } + } + Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h); + return bitmap; + } + + @Override + public void onPause() { + if (item != null) { + Singleton.getAddressRepository(getContext()).save(item); + } + super.onPause(); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java similarity index 96% rename from app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java rename to app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index b84aa54..75aa930 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -38,7 +38,7 @@ import java.util.List; /** * Fragment that shows a list of all contacts, the ones we subscribed to first. */ -public class SubscriptionListFragment extends AbstractItemListFragment<BitmessageAddress> { +public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> { @Override public void onResume() { super.onResume(); @@ -113,7 +113,7 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_contact_list, container, false); + return inflater.inflate(R.layout.fragment_address_list, container, false); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index f6a2e38..5fa12a4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -26,6 +26,7 @@ import android.support.v7.widget.Toolbar; public class ComposeMessageActivity extends AppCompatActivity { public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"; public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"; + public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 9184996..b19ef4d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -36,6 +36,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; /** * Compose a new message. @@ -43,6 +44,7 @@ import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; public class ComposeMessageFragment extends Fragment { private BitmessageAddress identity; private BitmessageAddress recipient; + private String subject; private AutoCompleteTextView recipientInput; private EditText subjectInput; private EditText bodyInput; @@ -66,6 +68,9 @@ public class ComposeMessageFragment extends Fragment { if (getArguments().containsKey(EXTRA_RECIPIENT)) { recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT); } + if (getArguments().containsKey(EXTRA_SUBJECT)) { + subject = getArguments().getString(EXTRA_SUBJECT); + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -99,9 +104,9 @@ public class ComposeMessageFragment extends Fragment { recipientInput.setText(recipient.toString()); } subjectInput = (EditText) rootView.findViewById(R.id.subject); + subjectInput.setText(subject); bodyInput = (EditText) rootView.findViewById(R.id.body); -// bodyInput.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); -// bodyInput.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); + return rootView; } @@ -120,7 +125,8 @@ public class ComposeMessageFragment extends Fragment { try { recipient = new BitmessageAddress(inputString); } catch (Exception e) { - List<BitmessageAddress> contacts = Singleton.getAddressRepository(getContext()).getContacts(); + List<BitmessageAddress> contacts = Singleton.getAddressRepository + (getContext()).getContacts(); for (BitmessageAddress contact : contacts) { if (inputString.equalsIgnoreCase(contact.getAlias())) { recipient = contact; diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index f493cb3..c31a0e6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -94,6 +94,7 @@ public class MainActivity extends AppCompatActivity private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final int ADD_IDENTITY = 1; + private static final int MANAGE_IDENTITY = 2; /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet @@ -120,7 +121,6 @@ public class MainActivity extends AppCompatActivity private Label selectedLabel; private BitmessageContext bmc; - private BitmessageAddress selectedIdentity; private AccountHeader accountHeader; @Override @@ -190,6 +190,7 @@ public class MainActivity extends AppCompatActivity profiles.add(new ProfileDrawerItem() .withIcon(new Identicon(identity)) .withName(identity.toString()) + .withNameShown(true) .withEmail(identity.getAddress()) .withTag(identity) ); @@ -216,6 +217,7 @@ public class MainActivity extends AppCompatActivity profiles.add(new ProfileSettingDrawerItem() .withName(getString(R.string.manage_identity)) .withIcon(GoogleMaterial.Icon.gmd_settings) + .withIdentifier(MANAGE_IDENTITY) ); // Create the AccountHeader accountHeader = new AccountHeaderBuilder() @@ -226,25 +228,46 @@ public class MainActivity extends AppCompatActivity @Override public boolean onProfileChanged(View view, IProfile profile, boolean currentProfile) { - if (profile.getIdentifier() == ADD_IDENTITY) { - BitmessageAddress identity = bmc.createIdentity(false); - IProfile newProfile = new ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - // we know that there are 2 setting elements. - // Set the new profile above them ;) - accountHeader.addProfile( - newProfile, accountHeader.getProfiles().size() - 2); - } else { - accountHeader.addProfiles(newProfile); - } - } else if (profile instanceof ProfileDrawerItem) { - Object tag = ((ProfileDrawerItem) profile).getTag(); - if (tag instanceof BitmessageAddress) { - selectedIdentity = (BitmessageAddress) tag; - } + switch (profile.getIdentifier()) { + case ADD_IDENTITY: + new AlertDialog.Builder(MainActivity.this) + .setMessage(R.string.add_identity_warning) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + BitmessageAddress identity = bmc.createIdentity(false); + IProfile newProfile = new ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + // we know that there are 2 setting elements. + // Set the new profile above them ;) + accountHeader.addProfile( + newProfile, accountHeader.getProfiles().size() - 2); + } else { + accountHeader.addProfiles(newProfile); + } + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + break; + case MANAGE_IDENTITY: + Intent show = new Intent(MainActivity.this, + AddressDetailActivity.class); + show.putExtra(AddressDetailFragment.ARG_ITEM, + Singleton.getIdentity(getApplicationContext())); + startActivity(show); + break; + default: + if (profile instanceof ProfileDrawerItem) { + Object tag = ((ProfileDrawerItem) profile).getTag(); + if (tag instanceof BitmessageAddress) { + Singleton.setIdentity((BitmessageAddress) tag); + } + } } // false if it should close the drawer return false; @@ -336,10 +359,10 @@ public class MainActivity extends AppCompatActivity switch (ni.getNameRes()) { case R.string.contacts_and_subscriptions: if (!(getSupportFragmentManager().findFragmentById(R.id - .item_list) instanceof SubscriptionListFragment)) { - changeList(new SubscriptionListFragment()); + .item_list) instanceof AddressListFragment)) { + changeList(new AddressListFragment()); } else { - ((SubscriptionListFragment) getSupportFragmentManager() + ((AddressListFragment) getSupportFragmentManager() .findFragmentById(R.id.item_list)).updateList(); } @@ -415,7 +438,7 @@ public class MainActivity extends AppCompatActivity if (item instanceof Plaintext) fragment = new MessageDetailFragment(); else if (item instanceof BitmessageAddress) - fragment = new SubscriptionDetailFragment(); + fragment = new AddressDetailFragment(); else throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + "was " @@ -431,7 +454,7 @@ public class MainActivity extends AppCompatActivity if (item instanceof Plaintext) detailIntent = new Intent(this, MessageDetailActivity.class); else if (item instanceof BitmessageAddress) - detailIntent = new Intent(this, SubscriptionDetailActivity.class); + detailIntent = new Intent(this, AddressDetailActivity.class); else throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + "was " @@ -468,8 +491,4 @@ public class MainActivity extends AppCompatActivity } super.onStop(); } - - public BitmessageAddress getSelectedIdentity() { - return selectedIdentity; - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 21a9d3f..8296c63 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -43,6 +43,9 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; @@ -152,8 +155,12 @@ public class MessageDetailFragment extends Fragment { case R.id.reply: Intent replyIntent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); - replyIntent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item.getFrom()); - replyIntent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, item.getTo()); + replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); + replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); + replyIntent.putExtra(EXTRA_SUBJECT, + (item.getSubject().substring(0, 3).equalsIgnoreCase("RE:") ? "" : "RE: ") + + item.getSubject() + ); startActivity(replyIntent); return true; case R.id.delete: diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index c4b0d1f..71c8b20 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -132,7 +132,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { @Override public void onClick(View view) { Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, ((MainActivity) getActivity()).getSelectedIdentity()); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity())); startActivity(intent); } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java deleted file mode 100644 index 955531f..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailFragment.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2015 Christian Basler - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ch.dissem.apps.abit; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.Switch; -import android.widget.TextView; - -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.entity.BitmessageAddress; - - -/** - * A fragment representing a single Message detail screen. - * This fragment is either contained in a {@link MainActivity} - * in two-pane mode (on tablets) or a {@link MessageDetailActivity} - * on handsets. - */ -public class SubscriptionDetailFragment extends Fragment { - /** - * The fragment argument representing the item ID that this fragment - * represents. - */ - public static final String ARG_ITEM = "item"; - - /** - * The content this fragment is presenting. - */ - private BitmessageAddress item; - - - /** - * Mandatory empty constructor for the fragment manager to instantiate the - * fragment (e.g. upon screen orientation changes). - */ - public SubscriptionDetailFragment() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getArguments().containsKey(ARG_ITEM)) { - // Load the dummy content specified by the fragment - // arguments. In a real-world scenario, use a Loader - // to load content from a content provider. - item = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM); - } - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_contact_detail, container, false); - - // Show the dummy content as text in a TextView. - if (item != null) { - ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); - TextView name = (TextView) rootView.findViewById(R.id.name); - name.setText(item.toString()); - name.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Nothing to do - } - - @Override - public void afterTextChanged(Editable s) { - item.setAlias(s.toString()); - } - }); - TextView address = (TextView) rootView.findViewById(R.id.address); - address.setText(item.getAddress()); - address.setSelected(true); - ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity() - .getString(R.string.stream_number, item.getStream())); - Switch active = (Switch) rootView.findViewById(R.id.active); - active.setChecked(item.isSubscribed()); - active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - item.setSubscribed(isChecked); - } - }); - - ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id.pubkey_available); - if (item.getPubkey() == null) { - pubkeyAvailableImg.setAlpha(0.3f); - TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id - .pubkey_available_desc); - pubkeyAvailableDesc.setText(R.string.pubkey_not_available); - } - } - - return rootView; - } - - @Override - public void onPause() { - Singleton.getAddressRepository(getContext()).save(item); - super.onPause(); - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 997270b..edbdbe8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -239,7 +239,7 @@ public class AndroidAddressRepository implements AddressRepository { @Override public void remove(BitmessageAddress address) { SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, "address = " + address.getAddress(), null); + db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()}); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 5313927..178c8bb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -124,4 +124,10 @@ public class Singleton { } return identity; } + + public static void setIdentity(BitmessageAddress identity) { + if (identity.getPrivateKey() == null) + throw new IllegalArgumentException("Identity expected, but no private key available"); + Singleton.identity = identity; + } } diff --git a/app/src/main/res/layout/fragment_contact_detail.xml b/app/src/main/res/layout/fragment_address_detail.xml similarity index 86% rename from app/src/main/res/layout/fragment_contact_detail.xml rename to app/src/main/res/layout/fragment_address_detail.xml index 662890d..f80aa59 100644 --- a/app/src/main/res/layout/fragment_contact_detail.xml +++ b/app/src/main/res/layout/fragment_address_detail.xml @@ -100,4 +100,18 @@ android:text="@string/pubkey_available" android:textAppearance="?android:attr/textAppearanceSmall"/> + <ImageView + android:id="@+id/qr_code" + tools:src="@drawable/public_key" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_below="@+id/pubkey_available" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_marginTop="24dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="64dp" + android:contentDescription="@string/alt_qr_code"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contact_list.xml b/app/src/main/res/layout/fragment_address_list.xml similarity index 100% rename from app/src/main/res/layout/fragment_contact_list.xml rename to app/src/main/res/layout/fragment_address_list.xml diff --git a/app/src/main/res/menu/address.xml b/app/src/main/res/menu/address.xml new file mode 100644 index 0000000..a833cb9 --- /dev/null +++ b/app/src/main/res/menu/address.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/write_message" + android:title="@string/write_message" + app:showAsAction="ifRoom"/> + <item + android:id="@+id/delete" + android:title="@string/delete" + app:showAsAction="ifRoom"/> + <item + android:id="@+id/share" + android:title="@string/share" + app:actionProviderClass="android.support.v7.widget.ShareActionProvider" + app:showAsAction="always"/> +</menu> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8a153f1..b7906bb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -58,4 +58,9 @@ <string name="status_summary">Technische Infos</string> <string name="pubkey_available">Öffentlicher Schlüssel verfügbar</string> <string name="pubkey_not_available">Öffentlicher Schlüssel noch nicht verfügbar</string> + <string name="alt_qr_code">QR-Code</string> + <string name="add_identity_warning">Mehrere Identitäten zu haben bedeutet einen höheren Resourcenverbrauch. Sind Sie sicher?</string> + <string name="share">Teilen</string> + <string name="delete_contact_warning">Sind Sie sicher dass dieser Kontakt gelöscht werden soll?</string> + <string name="delete_identity_warning">Sind Sie sicher dass diese Identität gelöscht werden soll? Sie werden keine Nachrichten mehr empfangen können welche an diese Adresse gesendet werden, und es est nicht möglich diese Aktion rückgängig zu machen.</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82daeff..3e37518 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,4 +61,9 @@ <string name="alias_default_identity">Me</string> <string name="pubkey_available">Public key available</string> <string name="pubkey_not_available">Public key not yet available</string> + <string name="alt_qr_code">QR code</string> + <string name="add_identity_warning">Having more identities will reequire more resources. Are you sure you want to add an identity?</string> + <string name="share">Share</string> + <string name="delete_identity_warning">Are you sure you want to delete this identity? You won\'t be able to receive any messages sent to this address and can\'t undo this operation.</string> + <string name="delete_contact_warning">Are you sure you want to delete this contact?</string> </resources> From 6bcb7fc50e953a5432d208c738f7d661dc8fdee9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 31 Jan 2016 18:20:12 +0100 Subject: [PATCH 036/110] Updated Jabit version (there were some bugs) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ffa6553..6928fb4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { } } -ext.jabitVersion = '1.0.0' +ext.jabitVersion = '1.0.1-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' From 3d7c1a504ee390dcd491a404d5ab1df7f875a664 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 31 Jan 2016 23:13:35 +0100 Subject: [PATCH 037/110] Never delete the private key --- .../dissem/apps/abit/repository/AndroidAddressRepository.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index edbdbe8..1e51643 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -196,7 +196,9 @@ public class AndroidAddressRepository implements AddressRepository { } else { values.put(COLUMN_PUBLIC_KEY, (byte[]) null); } - values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); + if (address.getPrivateKey() != null) { + values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); + } values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + From d682df4cdb3f42cebd876b1322e56c393f87bcfd Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 3 Feb 2016 07:42:21 +0100 Subject: [PATCH 038/110] Updated Jabit, which should fix a bug where you can't send messages --- app/build.gradle | 4 +-- .../dissem/apps/abit/AddressListFragment.java | 16 +++++++++++- .../res/drawable/ic_action_add_contact.xml | 25 +++++++++++++++++++ .../main/res/layout/fragment_address_list.xml | 4 +-- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/ic_action_add_contact.xml diff --git a/app/build.gradle b/app/build.gradle index 6928fb4..2bed0e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { applicationId "ch.dissem.apps.abit" minSdkVersion 19 targetSdkVersion 23 - versionCode 5 + versionCode 6 versionName "1.0-beta" } signingConfigs { @@ -29,7 +29,7 @@ android { } } -ext.jabitVersion = '1.0.1-SNAPSHOT' +ext.jabitVersion = '1.0.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 75aa930..89d1489 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -113,7 +114,20 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_address_list, container, false); + View view = inflater.inflate(R.layout.fragment_address_list, container, false); + + FloatingActionButton fab = (FloatingActionButton) view.findViewById(R.id.fab_add_contact); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { +// Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); +// intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity())); +// startActivity(intent); + // TODO + } + }); + + return view; } @Override diff --git a/app/src/main/res/drawable/ic_action_add_contact.xml b/app/src/main/res/drawable/ic_action_add_contact.xml new file mode 100644 index 0000000..e0b6b6e --- /dev/null +++ b/app/src/main/res/drawable/ic_action_add_contact.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zm-9,-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9,4c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> +</vector> diff --git a/app/src/main/res/layout/fragment_address_list.xml b/app/src/main/res/layout/fragment_address_list.xml index 5aa714b..c62c831 100644 --- a/app/src/main/res/layout/fragment_address_list.xml +++ b/app/src/main/res/layout/fragment_address_list.xml @@ -18,10 +18,10 @@ android:layout_alignParentBottom="true"/> <android.support.design.widget.FloatingActionButton - android:id="@+id/fab_compose_message" + android:id="@+id/fab_add_contact" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_action_compose_message" + android:src="@drawable/ic_action_add_contact" app:elevation="8dp" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" From a3c3fc082dd615969b94875e2d2571496f3536a0 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 6 Feb 2016 13:24:25 +0100 Subject: [PATCH 039/110] Fixed error when responding to messages with a very short subject --- .../ch/dissem/apps/abit/MessageDetailFragment.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 8296c63..c86b690 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -157,10 +157,14 @@ public class MessageDetailFragment extends Fragment { ComposeMessageActivity.class); replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); - replyIntent.putExtra(EXTRA_SUBJECT, - (item.getSubject().substring(0, 3).equalsIgnoreCase("RE:") ? "" : "RE: ") - + item.getSubject() - ); + String prefix; + if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) + .equalsIgnoreCase("RE:")) { + prefix = ""; + } else { + prefix = "RE: "; + } + replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); startActivity(replyIntent); return true; case R.id.delete: From a4f6642f6ae29bcf19f14ce2f9a713c9cf615ab7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 6 Feb 2016 13:25:04 +0100 Subject: [PATCH 040/110] Added option to scan QR codes directly from the app --- app/build.gradle | 1 + .../dissem/apps/abit/AddressListFragment.java | 69 ++++++++++++++----- .../res/drawable/ic_action_create_contact.xml | 9 +++ .../main/res/drawable/ic_action_qr_code.xml | 7 ++ .../main/res/layout/fragment_address_list.xml | 39 ++++++----- app/src/main/res/menu/fab_address.xml | 27 ++++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 app/src/main/res/drawable/ic_action_create_contact.xml create mode 100644 app/src/main/res/drawable/ic_action_qr_code.xml create mode 100644 app/src/main/res/menu/fab_address.xml diff --git a/app/build.gradle b/app/build.gradle index 2bed0e5..e1ac379 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,7 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar' compile 'com.google.zxing:core:3.2.0' + compile 'io.github.yavski:fab-speed-dial:1.0.2' } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 89d1489..be0ca68 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -17,25 +17,31 @@ package ch.dissem.apps.abit; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; -import ch.dissem.apps.abit.listener.ActionBarListener; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.valueobject.Label; +import com.google.zxing.integration.android.IntentIntegrator; import java.util.Collections; import java.util.Comparator; import java.util.List; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.valueobject.Label; +import io.github.yavski.fabspeeddial.FabSpeedDial; +import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter; + /** * Fragment that shows a list of all contacts, the ones we subscribed to first. */ @@ -48,7 +54,8 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr } public void updateList() { - List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()).getContacts(); + List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()) + .getContacts(); Collections.sort(addresses, new Comparator<BitmessageAddress>() { /** * Yields the following order: @@ -92,12 +99,15 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr convertView = inflater.inflate(R.layout.subscription_row, null, false); } BitmessageAddress item = getItem(position); - ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); + ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new + Identicon(item)); TextView name = (TextView) convertView.findViewById(R.id.name); name.setText(item.toString()); TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number); - streamNumber.setText(getContext().getString(R.string.stream_number, item.getStream())); - convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? View.VISIBLE : View.INVISIBLE); + streamNumber.setText(getContext().getString(R.string.stream_number, item + .getStream())); + convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? + View.VISIBLE : View.INVISIBLE); return convertView; } }); @@ -106,30 +116,53 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr @Override public void onAttach(Context ctx) { super.onAttach(ctx); - if (ctx instanceof ActionBarListener){ + if (ctx instanceof ActionBarListener) { ((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions)); } } @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { View view = inflater.inflate(R.layout.fragment_address_list, container, false); - FloatingActionButton fab = (FloatingActionButton) view.findViewById(R.id.fab_add_contact); - fab.setOnClickListener(new View.OnClickListener() { + FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact); + fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() { @Override - public void onClick(View view) { -// Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); -// intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity())); -// startActivity(intent); - // TODO + public boolean onMenuItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.action_read_qr_code: + IntentIntegrator.forSupportFragment(AddressListFragment.this) + .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) + .initiateScan(); + return true; + case R.id.action_create_contact: + return true; + default: + return false; + } } }); return view; } + @Override + public void startActivityForResult(Intent intent, int requestCode) { + super.startActivityForResult(intent, requestCode); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data.hasExtra("SCAN_RESULT")) { + Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT")); + Intent intent = new Intent(getActivity(), OpenBitmessageLinkActivity.class); + intent.setData(uri); + startActivity(intent); + } + } + @Override void updateList(Label label) { updateList(); diff --git a/app/src/main/res/drawable/ic_action_create_contact.xml b/app/src/main/res/drawable/ic_action_create_contact.xml new file mode 100644 index 0000000..fc450f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_create_contact.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_qr_code.xml b/app/src/main/res/drawable/ic_action_qr_code.xml new file mode 100644 index 0000000..ee40a97 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_qr_code.xml @@ -0,0 +1,7 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_address_list.xml b/app/src/main/res/layout/fragment_address_list.xml index c62c831..c4a8950 100644 --- a/app/src/main/res/layout/fragment_address_list.xml +++ b/app/src/main/res/layout/fragment_address_list.xml @@ -5,25 +5,28 @@ android:layout_height="match_parent"> <ListView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@id/android:list" + android:id="@id/android:list" + android:layout_width="wrap_content" + android:layout_height="wrap_content" - android:paddingBottom="88dp" - android:clipToPadding="false" - android:scrollbarStyle="outsideOverlay" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:layout_alignParentBottom="true"/> + android:clipToPadding="false" + android:paddingBottom="88dp" + android:scrollbarStyle="outsideOverlay"/> - <android.support.design.widget.FloatingActionButton - android:id="@+id/fab_add_contact" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/ic_action_add_contact" - app:elevation="8dp" - android:layout_alignParentBottom="true" - android:layout_alignParentEnd="true" - android:layout_margin="16dp"/> + <io.github.yavski.fabspeeddial.FabSpeedDial + android:id="@+id/fab_add_contact" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + android:src="@drawable/ic_action_add_contact" + app:elevation="8dp" + app:fabGravity="bottom_end" + app:fabMenu="@menu/fab_address"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/fab_address.xml b/app/src/main/res/menu/fab_address.xml new file mode 100644 index 0000000..d252755 --- /dev/null +++ b/app/src/main/res/menu/fab_address.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/action_read_qr_code" + android:icon="@drawable/ic_action_qr_code" + android:title="@string/scan_qr_code"/> + <item + android:id="@+id/action_create_contact" + android:icon="@drawable/ic_action_create_contact" + android:title="@string/create_contact"/> +</menu> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b7906bb..c5b6f5b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -63,4 +63,6 @@ <string name="share">Teilen</string> <string name="delete_contact_warning">Sind Sie sicher dass dieser Kontakt gelöscht werden soll?</string> <string name="delete_identity_warning">Sind Sie sicher dass diese Identität gelöscht werden soll? Sie werden keine Nachrichten mehr empfangen können welche an diese Adresse gesendet werden, und es est nicht möglich diese Aktion rückgängig zu machen.</string> + <string name="create_contact">Kontakt erfassen</string> + <string name="scan_qr_code">QR-Code scannen</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e37518..6744732 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,4 +66,6 @@ <string name="share">Share</string> <string name="delete_identity_warning">Are you sure you want to delete this identity? You won\'t be able to receive any messages sent to this address and can\'t undo this operation.</string> <string name="delete_contact_warning">Are you sure you want to delete this contact?</string> + <string name="scan_qr_code">Scan QR code</string> + <string name="create_contact">Create contact</string> </resources> From 17a99b656218f0cab948b3b0c7f358343a3880d2 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 12 Feb 2016 00:19:15 +0100 Subject: [PATCH 041/110] Added showcase explaining full node --- app/build.gradle | 2 + .../ch/dissem/apps/abit/MainActivity.java | 98 ++++++++++++++----- .../material_showcase_button_bg.xml | 9 ++ .../drawable/material_showcase_button_bg.xml | 14 +++ app/src/main/res/layout/showcase_button.xml | 14 +++ app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/styles.xml | 12 +++ 8 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/drawable-v21/material_showcase_button_bg.xml create mode 100644 app/src/main/res/drawable/material_showcase_button_bg.xml create mode 100644 app/src/main/res/layout/showcase_button.xml diff --git a/app/build.gradle b/app/build.gradle index e1ac379..eac2b54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,8 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar' compile 'com.google.zxing:core:3.2.0' compile 'io.github.yavski:fab-speed-dial:1.0.2' + + compile 'com.github.amlcurran.showcaseview:library:5.4.0' } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index c31a0e6..6e850a2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -22,15 +22,21 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; +import android.graphics.Point; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.Button; import android.widget.CompoundButton; +import android.widget.RelativeLayout; +import com.github.amlcurran.showcaseview.ShowcaseView; +import com.github.amlcurran.showcaseview.targets.Target; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; @@ -38,6 +44,7 @@ import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; import com.mikepenz.materialdrawer.accountswitcher.AccountHeader; import com.mikepenz.materialdrawer.accountswitcher.AccountHeaderBuilder; +import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem; @@ -123,6 +130,9 @@ public class MainActivity extends AppCompatActivity private BitmessageContext bmc; private AccountHeader accountHeader; + private Drawer drawer; + private ShowcaseView showcaseView; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -167,6 +177,36 @@ public class MainActivity extends AppCompatActivity } else { SyncAdapter.stopSync(this); } + if (drawer.isDrawerOpen()) { + RelativeLayout.LayoutParams lps = new RelativeLayout.LayoutParams(ViewGroup + .LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue(); + lps.setMargins(margin, margin, margin, margin); + + showcaseView = new ShowcaseView.Builder(this) + .withMaterialShowcase() + .setStyle(R.style.CustomShowcaseTheme) + .setContentTitle(R.string.full_node) + .setContentText(R.string.full_node_description) + .setTarget(new Target() { + @Override + public Point getPoint() { + View view = drawer.getStickyFooter(); + int[] location = new int[2]; + view.getLocationInWindow(location); + int x = location[0] + 7 * view.getWidth() / 8; + int y = location[1] + view.getHeight() / 2; + return new Point(x, y); + } + } + ) + .replaceEndButton(R.layout.showcase_button) + .hideOnTouchOutside() + .build(); + showcaseView.setButtonPosition(lps); + } } private void changeList(AbstractItemListFragment<?> listFragment) { @@ -234,23 +274,29 @@ public class MainActivity extends AppCompatActivity .setMessage(R.string.add_identity_warning) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - BitmessageAddress identity = bmc.createIdentity(false); - IProfile newProfile = new ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - // we know that there are 2 setting elements. - // Set the new profile above them ;) - accountHeader.addProfile( - newProfile, accountHeader.getProfiles().size() - 2); - } else { - accountHeader.addProfiles(newProfile); - } - } - }) + @Override + public void onClick(DialogInterface dialog, + int which) { + BitmessageAddress identity = bmc + .createIdentity(false); + IProfile newProfile = new + ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + // we know that there are 2 setting + // elements. + // Set the new profile above them ;) + accountHeader.addProfile( + newProfile, accountHeader + .getProfiles().size() + - 2); + } else { + accountHeader.addProfiles(newProfile); + } + } + }) .setNegativeButton(android.R.string.no, null) .show(); break; @@ -315,20 +361,20 @@ public class MainActivity extends AppCompatActivity .withTag(null) .withIcon(CommunityMaterial.Icon.cmd_archive) ); + drawerItems.add(new DividerDrawerItem()); + drawerItems.add(new PrimaryDrawerItem() + .withName(R.string.contacts_and_subscriptions) + .withIcon(GoogleMaterial.Icon.gmd_contacts)); + drawerItems.add(new PrimaryDrawerItem() + .withName(R.string.settings) + .withIcon(GoogleMaterial.Icon.gmd_settings)); - - new DrawerBuilder() + drawer = new DrawerBuilder() .withActivity(this) .withToolbar(toolbar) .withAccountHeader(accountHeader) .withDrawerItems(drawerItems) .addStickyDrawerItems( - new PrimaryDrawerItem() - .withName(R.string.contacts_and_subscriptions) - .withIcon(GoogleMaterial.Icon.gmd_contacts), - new PrimaryDrawerItem() - .withName(R.string.settings) - .withIcon(GoogleMaterial.Icon.gmd_settings), new SwitchDrawerItem() .withName(R.string.full_node) .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) @@ -382,7 +428,7 @@ public class MainActivity extends AppCompatActivity return false; } }) - .withCloseOnClick(true) + .withShowDrawerOnFirstLaunch(true) .build(); } diff --git a/app/src/main/res/drawable-v21/material_showcase_button_bg.xml b/app/src/main/res/drawable-v21/material_showcase_button_bg.xml new file mode 100644 index 0000000..4df0f4b --- /dev/null +++ b/app/src/main/res/drawable-v21/material_showcase_button_bg.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="#ffffff"> + + <item + android:id="@android:id/mask" + android:drawable="@android:color/white"/> + +</ripple> \ No newline at end of file diff --git a/app/src/main/res/drawable/material_showcase_button_bg.xml b/app/src/main/res/drawable/material_showcase_button_bg.xml new file mode 100644 index 0000000..7171935 --- /dev/null +++ b/app/src/main/res/drawable/material_showcase_button_bg.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"> + <shape android:shape="rectangle"> + <solid android:color="#ccffffff" /> + </shape> + </item> + <item android:state_focused="true"> + <shape android:shape="rectangle"> + <stroke android:color="@android:color/white" /> + </shape> + </item> + <item android:drawable="@android:color/transparent" /> +</selector> \ No newline at end of file diff --git a/app/src/main/res/layout/showcase_button.xml b/app/src/main/res/layout/showcase_button.xml new file mode 100644 index 0000000..f8b0558 --- /dev/null +++ b/app/src/main/res/layout/showcase_button.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<Button xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Got it" + android:textStyle="bold" + android:background="@drawable/material_showcase_button_bg" + android:textSize="13sp" + android:textAllCaps="true" + android:textColor="@android:color/white" + > + +</Button> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5b6f5b..31c31d9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -65,4 +65,8 @@ <string name="delete_identity_warning">Sind Sie sicher dass diese Identität gelöscht werden soll? Sie werden keine Nachrichten mehr empfangen können welche an diese Adresse gesendet werden, und es est nicht möglich diese Aktion rückgängig zu machen.</string> <string name="create_contact">Kontakt erfassen</string> <string name="scan_qr_code">QR-Code scannen</string> + <string name="full_node_description">Solange kein aktiver Knoten gestartet ist, werden keine Meldungen empfangen oder gesendet. Dies braucht jedoch viele Resourcen und Daten. + +Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfiguriert werden, aber im Moment muss dieser selbst bereitgestellt werden.</string> + <string name="got_it">Alles klar</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6744732..7b7aa2f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,4 +68,8 @@ <string name="delete_contact_warning">Are you sure you want to delete this contact?</string> <string name="scan_qr_code">Scan QR code</string> <string name="create_contact">Create contact</string> + <string name="full_node_description">You can\'t receive or send messages unless you start a full node. But be aware that this uses a lot of resources and internet traffic. + +As an alternative you could configure a trusted node in the settings, but as of now you\'ll need to deploy your own.</string> + <string name="got_it">Got it</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 090c4de..58f503e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,4 +23,16 @@ <style name="AppTheme" parent="AppTheme.Base"/> + <style name="CustomShowcaseTheme" parent="ShowcaseView"> + <item name="sv_backgroundColor">#eeffc107</item> + <item name="sv_showcaseColor">#ffc107</item> + <item name="sv_buttonText">Hide</item> + <item name="sv_tintButtonColor">false</item> + <item name="sv_titleTextAppearance">@style/CustomTitle</item> + </style> + + <style name="CustomTitle" parent="TextAppearance.ShowcaseView.Title"> + <item name="android:textColor">@color/colorAccent</item> + </style> + </resources> From df121b25c61441d508cc820fa42800c7f586f675 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 23 Feb 2016 07:05:38 +0100 Subject: [PATCH 042/110] Version 1.0-beta7 bump --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eac2b54..b4bafb0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "ch.dissem.apps.abit" minSdkVersion 19 targetSdkVersion 23 - versionCode 6 - versionName "1.0-beta" + versionCode 7 + versionName "1.0-beta7" } signingConfigs { release { From 563085ed79539fc2ae7dd3c6c3aed0ca1b311671 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 23 Feb 2016 07:06:34 +0100 Subject: [PATCH 043/110] Added badge for unread messages --- .../ch/dissem/apps/abit/MainActivity.java | 37 ++++++++++++++++++- .../apps/abit/MessageDetailFragment.java | 10 +++++ .../apps/abit/listener/ActionBarListener.java | 2 + .../apps/abit/listener/MessageListener.java | 7 ++++ .../repository/AndroidMessageRepository.java | 12 +++--- 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 6e850a2..1baf37e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -31,7 +31,6 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.Button; import android.widget.CompoundButton; import android.widget.RelativeLayout; @@ -58,6 +57,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -103,6 +103,8 @@ public class MainActivity extends AppCompatActivity private static final int ADD_IDENTITY = 1; private static final int MANAGE_IDENTITY = 2; + public static WeakReference<MainActivity> instance; + /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. @@ -432,6 +434,19 @@ public class MainActivity extends AppCompatActivity .build(); } + @Override + protected void onResume() { + instance = new WeakReference<>(this); + updateUnread(); + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + instance = null; + } + private void checkAndStartNode(final CompoundButton buttonView) { if (service == null) return; @@ -456,6 +471,21 @@ public class MainActivity extends AppCompatActivity } } + @Override + public void updateUnread() { + for (IDrawerItem item : drawer.getDrawerItems()) { + if (item.getTag() instanceof Label) { + Label label = (Label) item.getTag(); + int unread = bmc.messages().countUnread(label); + if (unread > 0) { + ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); + } else { + ((PrimaryDrawerItem) item).withBadge(null); + } + } + } + } + private void showSelectedLabel() { if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { @@ -537,4 +567,9 @@ public class MainActivity extends AppCompatActivity } super.onStop(); } + + public static MainActivity getInstance() { + if (instance == null) return null; + return instance.get(); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index c86b690..b67cee5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -35,6 +35,7 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial; import java.util.Iterator; import java.util.regex.Matcher; +import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -129,6 +130,9 @@ public class MessageDetailFragment extends Fragment { } } if (removed) { + if (getActivity() instanceof ActionBarListener) { + ((ActionBarListener) getActivity()).updateUnread(); + } Singleton.getMessageRepository(inflater.getContext()).save(item); } } @@ -180,8 +184,14 @@ public class MessageDetailFragment extends Fragment { case R.id.mark_unread: item.addLabels(messageRepo.getLabels(Label.Type.UNREAD)); messageRepo.save(item); + if (getActivity() instanceof ActionBarListener) { + ((ActionBarListener) getActivity()).updateUnread(); + } return true; case R.id.archive: + if (item.isUnread() && getActivity() instanceof ActionBarListener) { + ((ActionBarListener) getActivity()).updateUnread(); + } item.getLabels().clear(); messageRepo.save(item); return true; diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java index d1e1ea4..39b2f55 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/ActionBarListener.java @@ -21,4 +21,6 @@ package ch.dissem.apps.abit.listener; */ public interface ActionBarListener { void updateTitle(CharSequence title); + + void updateUnread(); } diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index b25fdfc..ddb99f5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -21,6 +21,7 @@ import android.content.Context; import java.util.Deque; import java.util.LinkedList; +import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.notification.NewMessageNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.Plaintext; @@ -58,6 +59,12 @@ public class MessageListener implements BitmessageContext.Listener { notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); } notification.show(); + + // If MainActivity is shown, update the sidebar badges + MainActivity main = MainActivity.getInstance(); + if (main != null) { + main.updateUnread(); + } } public void resetNotification() { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index a186fd3..6b128b4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -19,8 +19,10 @@ package ch.dissem.apps.abit.repository; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; +import android.support.v4.database.DatabaseUtilsCompat; import ch.dissem.apps.abit.R; import ch.dissem.bitmessage.InternalContext; @@ -171,14 +173,10 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont where = ""; } SQLiteDatabase db = sql.getReadableDatabase(); - try (Cursor c = db.query( - TABLE_NAME, new String[]{COLUMN_ID}, + return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME, where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + - "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))", - null, null, null, null - )) { - return c.getColumnCount(); - } + "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))" + ); } @Override From 5db5442064f461f4f4077a8470b50ca599b6156a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 24 Feb 2016 19:50:25 +0100 Subject: [PATCH 044/110] Improvements for Contacts - create contacts by manually entering the address (or pasting it) - share address --- app/src/main/AndroidManifest.xml | 2 +- .../apps/abit/AddressDetailFragment.java | 21 ++---- .../dissem/apps/abit/AddressListFragment.java | 6 +- ...tivity.java => CreateAddressActivity.java} | 61 +++++++++------- .../activity_create_bitmessage_address.xml | 71 +++++++++++++++++++ .../res/layout/fragment_address_detail.xml | 36 +++++----- app/src/main/res/menu/address.xml | 9 ++- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 143 insertions(+), 67 deletions(-) rename app/src/main/java/ch/dissem/apps/abit/{OpenBitmessageLinkActivity.java => CreateAddressActivity.java} (62%) create mode 100644 app/src/main/res/layout/activity_create_bitmessage_address.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3f83f18..f97947c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -89,7 +89,7 @@ </intent-filter> </activity> <activity - android:name=".OpenBitmessageLinkActivity" + android:name=".CreateAddressActivity" android:label="@string/title_activity_open_bitmessage_link" android:theme="@style/Theme.AppCompat.Light.Dialog"> <intent-filter> diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index da08e12..dba8a03 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -23,8 +23,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.view.MenuItemCompat; -import android.support.v7.widget.ShareActionProvider; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -71,7 +69,6 @@ public class AddressDetailFragment extends Fragment { private static final int QR_CODE_SIZE = 350; - private ShareActionProvider shareActionProvider; /** * The content this fragment is presenting. */ @@ -103,11 +100,8 @@ public class AddressDetailFragment extends Fragment { inflater.inflate(R.menu.address, menu); Drawables.addIcon(getActivity(), menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); - Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); Drawables.addIcon(getActivity(), menu, R.id.share, GoogleMaterial.Icon.gmd_share); - - shareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider( - menu.findItem(R.id.share)); + Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); super.onCreateOptionsMenu(menu, inflater); } @@ -143,20 +137,15 @@ public class AddressDetailFragment extends Fragment { .show(); return true; case R.id.share: - new AlertDialog.Builder(ctx) - .setMessage("I have no fucking clue.") - .show(); + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress()); + startActivity(Intent.createChooser(shareIntent, null)); default: return false; } } - private void setShareIntent(Intent shareIntent) { - if (shareActionProvider != null) { - shareActionProvider.setShareIntent(shareIntent); - } - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index be0ca68..9f0dc48 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -138,6 +138,8 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr .initiateScan(); return true; case R.id.action_create_contact: + Intent intent = new Intent(getActivity(), CreateAddressActivity.class); + startActivity(intent); return true; default: return false; @@ -155,9 +157,9 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (data.hasExtra("SCAN_RESULT")) { + if (data != null && data.hasExtra("SCAN_RESULT")) { Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT")); - Intent intent = new Intent(getActivity(), OpenBitmessageLinkActivity.class); + Intent intent = new Intent(getActivity(), CreateAddressActivity.class); intent.setData(uri); startActivity(intent); } diff --git a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java similarity index 62% rename from app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java rename to app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index 9f2fd4c..1f6065a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/OpenBitmessageLinkActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -30,32 +30,36 @@ import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; -public class OpenBitmessageLinkActivity extends AppCompatActivity { +public class CreateAddressActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_open_bitmessage_link); + Uri uri = getIntent().getData(); + if (uri != null) + setContentView(R.layout.activity_open_bitmessage_link); + else + setContentView(R.layout.activity_create_bitmessage_address); - final TextView addressView = (TextView) findViewById(R.id.address); + final TextView address = (TextView) findViewById(R.id.address); final EditText label = (EditText) findViewById(R.id.label); final Switch subscribe = (Switch) findViewById(R.id.subscribe); - Uri uri = getIntent().getData(); - final String address = getAddress(uri); - String[] parameters = getParameters(uri); - for (String parameter : parameters) { - String name = parameter.substring(0, 6).toLowerCase(); - if (name.startsWith("label")) { - label.setText(parameter.substring(parameter.indexOf('=') + 1).trim()); - } else if (name.startsWith("action")) { - parameter = parameter.toLowerCase(); - subscribe.setChecked(parameter.contains("subscribe")); + if (uri != null) { + String addressText = getAddress(uri); + String[] parameters = getParameters(uri); + for (String parameter : parameters) { + String name = parameter.substring(0, 6).toLowerCase(); + if (name.startsWith("label")) { + label.setText(parameter.substring(parameter.indexOf('=') + 1).trim()); + } else if (name.startsWith("action")) { + parameter = parameter.toLowerCase(); + subscribe.setChecked(parameter.contains("subscribe")); + } } + + address.setText(addressText); } - addressView.setText(address); - - final Button cancel = (Button) findViewById(R.id.cancel); cancel.setOnClickListener(new View.OnClickListener() { @Override @@ -68,18 +72,23 @@ public class OpenBitmessageLinkActivity extends AppCompatActivity { ok.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - BitmessageAddress bmAddress = new BitmessageAddress(address); - bmAddress.setAlias(label.getText().toString()); + String addressText = String.valueOf(address.getText()).trim(); + try { + BitmessageAddress bmAddress = new BitmessageAddress(addressText); + bmAddress.setAlias(label.getText().toString()); - BitmessageContext bmc = Singleton.getBitmessageContext(OpenBitmessageLinkActivity - .this); - bmc.addContact(bmAddress); - if (subscribe.isChecked()) { - bmc.addSubscribtion(bmAddress); + BitmessageContext bmc = Singleton.getBitmessageContext + (CreateAddressActivity.this); + bmc.addContact(bmAddress); + if (subscribe.isChecked()) { + bmc.addSubscribtion(bmAddress); + } + + setResult(Activity.RESULT_OK); + finish(); + } catch (RuntimeException e) { + address.setError(getString(R.string.error_illegal_address)); } - - setResult(Activity.RESULT_OK); - finish(); } }); } diff --git a/app/src/main/res/layout/activity_create_bitmessage_address.xml b/app/src/main/res/layout/activity_create_bitmessage_address.xml new file mode 100644 index 0000000..4d1e14b --- /dev/null +++ b/app/src/main/res/layout/activity_create_bitmessage_address.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="24dp"> + + <android.support.design.widget.TextInputLayout + android:id="@+id/address_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_marginTop="16dp"> + + <EditText + android:id="@+id/address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/address" + android:inputType="textNoSuggestions" /> + + </android.support.design.widget.TextInputLayout> + + <android.support.design.widget.TextInputLayout + android:id="@+id/label_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignStart="@+id/address_wrapper" + android:layout_below="@+id/address_wrapper" + android:layout_marginTop="16dp"> + + <EditText + android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label" + android:inputType="textPersonName" /> + + </android.support.design.widget.TextInputLayout> + + <Switch + android:id="@+id/subscribe" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignStart="@+id/address_wrapper" + android:layout_below="@+id/label_wrapper" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:text="@string/subscribe" /> + + <Button + android:id="@+id/do_import" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@+id/subscribe" + android:layout_marginBottom="12dp" + android:layout_marginTop="12dp" + android:text="@string/do_import" /> + + <Button + android:id="@+id/cancel" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/do_import" + android:layout_toStartOf="@+id/do_import" + android:text="@string/cancel" /> + +</RelativeLayout> diff --git a/app/src/main/res/layout/fragment_address_detail.xml b/app/src/main/res/layout/fragment_address_detail.xml index f80aa59..679aa5c 100644 --- a/app/src/main/res/layout/fragment_address_detail.xml +++ b/app/src/main/res/layout/fragment_address_detail.xml @@ -38,8 +38,8 @@ android:layout_alignTop="@+id/avatar" android:layout_marginEnd="16dp" android:layout_toEndOf="@+id/avatar" - android:text="" android:inputType="textPersonName" + android:text="" tools:ignore="LabelFor"/> <TextView @@ -50,9 +50,9 @@ android:lines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - tools:text="BM-XyYxXyYxXyYxXyYxXyYx" android:textAppearance="?android:attr/textAppearanceSmall" - android:textStyle="bold"/> + android:textStyle="bold" + tools:text="BM-XyYxXyYxXyYxXyYxXyYx"/> <TextView android:id="@+id/stream_number" @@ -63,8 +63,8 @@ android:lines="1" android:paddingLeft="16dp" android:paddingRight="16dp" - tools:text="Stream #" - android:textAppearance="?android:attr/textAppearanceSmall"/> + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Stream #"/> <Switch android:id="@+id/active" @@ -82,8 +82,8 @@ android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_below="@+id/active" - android:paddingStart="16dp" android:paddingEnd="4dp" + android:paddingStart="16dp" android:paddingTop="16dp" android:src="@drawable/public_key" tools:ignore="ContentDescription"/> @@ -92,26 +92,28 @@ android:id="@+id/pubkey_available_desc" android:layout_width="0dp" android:layout_height="wrap_content" - android:paddingStart="0dp" - android:paddingEnd="16dp" android:layout_alignBottom="@id/pubkey_available" - android:layout_toEndOf="@id/pubkey_available" android:layout_alignParentEnd="true" + android:layout_toEndOf="@id/pubkey_available" + android:paddingEnd="16dp" + android:paddingStart="0dp" android:text="@string/pubkey_available" android:textAppearance="?android:attr/textAppearanceSmall"/> <ImageView android:id="@+id/qr_code" - tools:src="@drawable/public_key" android:layout_width="0dp" android:layout_height="0dp" - android:layout_below="@+id/pubkey_available" - android:layout_alignParentStart="true" - android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" - android:layout_marginTop="24dp" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" + android:layout_alignParentEnd="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/pubkey_available" android:layout_marginBottom="64dp" - android:contentDescription="@string/alt_qr_code"/> + android:layout_marginEnd="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="24dp" + android:contentDescription="@string/alt_qr_code" + android:elevation="2dp" + tools:ignore="UnusedAttribute" + tools:src="@drawable/public_key"/> </RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/address.xml b/app/src/main/res/menu/address.xml index a833cb9..ec22ee9 100644 --- a/app/src/main/res/menu/address.xml +++ b/app/src/main/res/menu/address.xml @@ -21,13 +21,12 @@ android:id="@+id/write_message" android:title="@string/write_message" app:showAsAction="ifRoom"/> - <item - android:id="@+id/delete" - android:title="@string/delete" - app:showAsAction="ifRoom"/> <item android:id="@+id/share" android:title="@string/share" - app:actionProviderClass="android.support.v7.widget.ShareActionProvider" app:showAsAction="always"/> + <item + android:id="@+id/delete" + android:title="@string/delete" + app:showAsAction="never"/> </menu> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 31c31d9..2bfee26 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -69,4 +69,6 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfiguriert werden, aber im Moment muss dieser selbst bereitgestellt werden.</string> <string name="got_it">Alles klar</string> + <string name="address">Bitmessage-Adresse</string> + <string name="error_illegal_address">Vielleicht hat es einen Tippfehler</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b7aa2f..81350c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,4 +72,6 @@ As an alternative you could configure a trusted node in the settings, but as of now you\'ll need to deploy your own.</string> <string name="got_it">Got it</string> + <string name="address">Bitmessage Address</string> + <string name="error_illegal_address">There might be a typo</string> </resources> From 59f0bc7b74e10acc0c208c15397b3925201002f1 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 3 Mar 2016 16:43:05 +0100 Subject: [PATCH 045/110] Added export option for identities (import will follow) --- .../apps/abit/AddressDetailFragment.java | 42 ++++++++++++++++--- .../ch/dissem/apps/abit/util/Drawables.java | 13 +++--- app/src/main/res/menu/address.xml | 6 ++- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index dba8a03..5c06208 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -40,6 +40,7 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.MultiFormatWriter; import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; +import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import org.slf4j.Logger; @@ -48,6 +49,7 @@ import org.slf4j.LoggerFactory; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.wif.WifExporter; import static android.graphics.Color.BLACK; import static android.graphics.Color.WHITE; @@ -66,6 +68,7 @@ public class AddressDetailFragment extends Fragment { * represents. */ public static final String ARG_ITEM = "item"; + public static final String EXPORT_POSTFIX = ".keys.dat"; private static final int QR_CODE_SIZE = 350; @@ -102,6 +105,9 @@ public class AddressDetailFragment extends Fragment { Drawables.addIcon(getActivity(), menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); Drawables.addIcon(getActivity(), menu, R.id.share, GoogleMaterial.Icon.gmd_share); Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); + Drawables.addIcon(getActivity(), menu, R.id.export, + CommunityMaterial.Icon.cmd_export) + .setVisible(item != null && item.getPrivateKey() != null); super.onCreateOptionsMenu(menu, inflater); } @@ -110,13 +116,14 @@ public class AddressDetailFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem menuItem) { final Activity ctx = getActivity(); switch (menuItem.getItemId()) { - case R.id.write_message: + case R.id.write_message: { Intent intent = new Intent(ctx, ComposeMessageActivity.class); intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(ctx)); intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); startActivity(intent); return true; - case R.id.delete: + } + case R.id.delete: { int warning; if (item.getPrivateKey() != null) warning = R.string.delete_identity_warning; @@ -136,11 +143,36 @@ public class AddressDetailFragment extends Fragment { .setNegativeButton(android.R.string.no, null) .show(); return true; - case R.id.share: + } + case R.id.export: { + new AlertDialog.Builder(ctx) + .setMessage(R.string.confirm_export) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TITLE, item + + EXPORT_POSTFIX); + WifExporter exporter = new WifExporter(Singleton + .getBitmessageContext(ctx)); + exporter.addIdentity(item); + shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString + ()); + startActivity(Intent.createChooser(shareIntent, null)); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + return true; + } + case R.id.share: { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress()); startActivity(Intent.createChooser(shareIntent, null)); + } default: return false; } @@ -204,13 +236,13 @@ public class AddressDetailFragment extends Fragment { // QR code ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); - qrCode.setImageBitmap(encodeAsBitmap(item)); + qrCode.setImageBitmap(qrCode(item)); } return rootView; } - Bitmap encodeAsBitmap(BitmessageAddress address) { + Bitmap qrCode(BitmessageAddress address) { StringBuilder link = new StringBuilder("bitmessage:"); link.append(address.getAddress()); if (address.getAlias() != null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index 1583a1c..a35281a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -20,19 +20,22 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.view.Menu; +import android.view.MenuItem; + +import com.mikepenz.iconics.IconicsDrawable; +import com.mikepenz.iconics.typeface.IIcon; import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.R; -import com.mikepenz.google_material_typeface_library.GoogleMaterial; -import com.mikepenz.iconics.IconicsDrawable; - /** * Some helper methods to work with drawables. */ public class Drawables { - public static void addIcon(Context ctx, Menu menu, int menuItem, GoogleMaterial.Icon icon) { - menu.findItem(menuItem).setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()); + public static MenuItem addIcon(Context ctx, Menu menu, int menuItem, IIcon icon) { + MenuItem item = menu.findItem(menuItem); + item.setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()); + return item; } public static Bitmap toBitmap(Identicon identicon, int size) { diff --git a/app/src/main/res/menu/address.xml b/app/src/main/res/menu/address.xml index ec22ee9..653d411 100644 --- a/app/src/main/res/menu/address.xml +++ b/app/src/main/res/menu/address.xml @@ -24,7 +24,11 @@ <item android:id="@+id/share" android:title="@string/share" - app:showAsAction="always"/> + app:showAsAction="ifRoom"/> + <item + android:id="@+id/export" + android:title="@string/export" + app:showAsAction="ifRoom"/> <item android:id="@+id/delete" android:title="@string/delete" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2bfee26..76473aa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -71,4 +71,6 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="got_it">Alles klar</string> <string name="address">Bitmessage-Adresse</string> <string name="error_illegal_address">Vielleicht hat es einen Tippfehler</string> + <string name="export">Exportieren</string> + <string name="confirm_export">Identität wirklich exportieren? Der Export wird die privaten Schlüssel unverschlüsselt enthalten.</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81350c7..9bd6afc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,4 +74,6 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="got_it">Got it</string> <string name="address">Bitmessage Address</string> <string name="error_illegal_address">There might be a typo</string> + <string name="export">Export</string> + <string name="confirm_export">Do you really want to export your identity? The export will contain the unencrypted private keys.</string> </resources> From 0ecfbd3fb8277fe5cd91d9504a3f0685349e209b Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 25 Apr 2016 09:27:36 +0200 Subject: [PATCH 046/110] Implemented methods needed for chan support (no GUI yet, though) --- app/build.gradle | 13 ++++++++----- .../db/migration/V3.0__Update_table_address.sql | 1 + .../abit/repository/AndroidAddressRepository.java | 12 +++++++++++- .../ch/dissem/apps/abit/repository/SqlHelper.java | 4 +++- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 6 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 app/src/main/assets/db/migration/V3.0__Update_table_address.sql diff --git a/app/build.gradle b/app/build.gradle index b4bafb0..c2f57a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion "23.0.2" + buildToolsVersion "23.0.3" defaultConfig { applicationId "ch.dissem.apps.abit" @@ -29,12 +29,12 @@ android { } } -ext.jabitVersion = '1.0.1' +ext.jabitVersion = '1.1.0-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:support-v4:23.1.1' - compile 'com.android.support:design:23.1.1' + compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:support-v4:23.3.0' + compile 'com.android.support:design:23.3.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -58,6 +58,9 @@ dependencies { compile 'io.github.yavski:fab-speed-dial:1.0.2' compile 'com.github.amlcurran.showcaseview:library:5.4.0' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' } idea.module { diff --git a/app/src/main/assets/db/migration/V3.0__Update_table_address.sql b/app/src/main/assets/db/migration/V3.0__Update_table_address.sql new file mode 100644 index 0000000..01e036b --- /dev/null +++ b/app/src/main/assets/db/migration/V3.0__Update_table_address.sql @@ -0,0 +1 @@ +ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0'; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 1e51643..25c9b2a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -52,6 +52,7 @@ public class AndroidAddressRepository implements AddressRepository { private static final String COLUMN_PUBLIC_KEY = "public_key"; private static final String COLUMN_PRIVATE_KEY = "private_key"; private static final String COLUMN_SUBSCRIBED = "subscribed"; + private static final String COLUMN_CHAN = "chan"; private final SqlHelper sql; @@ -88,6 +89,11 @@ public class AndroidAddressRepository implements AddressRepository { return find("private_key IS NOT NULL"); } + @Override + public List<BitmessageAddress> getChans() { + return find("chan = '1'"); + } + @Override public List<BitmessageAddress> getSubscriptions() { return find("subscribed = '1'"); @@ -117,7 +123,8 @@ public class AndroidAddressRepository implements AddressRepository { COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, - COLUMN_SUBSCRIBED + COLUMN_SUBSCRIBED, + COLUMN_CHAN }; SQLiteDatabase db = sql.getReadableDatabase(); @@ -150,6 +157,7 @@ public class AndroidAddressRepository implements AddressRepository { } } address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS))); + address.setChan(c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1); address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1); result.add(address); @@ -199,6 +207,7 @@ public class AndroidAddressRepository implements AddressRepository { if (address.getPrivateKey() != null) { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); } + values.put(COLUMN_CHAN, address.isChan()); values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + @@ -227,6 +236,7 @@ public class AndroidAddressRepository implements AddressRepository { values.put(COLUMN_PUBLIC_KEY, (byte[]) null); } values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); + values.put(COLUMN_CHAN, address.isChan()); values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); long insert = db.insert(TABLE_NAME, null, values); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 9b5083a..3041020 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -26,7 +26,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 2; + public static final int DATABASE_VERSION = 3; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -51,6 +51,8 @@ public class SqlHelper extends SQLiteOpenHelper { case 1: // executeMigration(db, "V2.0__Update_table_message"); executeMigration(db, "V2.1__Create_table_POW"); + case 2: + executeMigration(db, "V3.0__Update_table_address"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION. } diff --git a/build.gradle b/build.gradle index f0f1e2b..8484636 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.0' + classpath 'com.android.tools.build:gradle:2.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 07fc193..551b2fa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Mon Apr 11 14:09:13 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip From f705e13d0ba379e1105efefbdb220730190fa569 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 27 Apr 2016 17:30:05 +0200 Subject: [PATCH 047/110] Fixed notification and made code slightly simpler --- app/src/main/AndroidManifest.xml | 2 +- .../apps/abit/notification/ProofOfWorkNotification.java | 6 ++++-- .../ch/dissem/apps/abit/service/ProofOfWorkService.java | 8 +------- app/src/main/res/values-de/strings.xml | 4 +++- app/src/main/res/values/strings.xml | 4 +++- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f97947c..a868652 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,7 +48,7 @@ </activity> <activity android:name=".ComposeMessageActivity" - android:label="Compose" + android:label="@string/compose_message" android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java index 408bc5d..73657e2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ProofOfWorkNotification.java @@ -32,7 +32,7 @@ public class ProofOfWorkNotification extends AbstractNotification { public ProofOfWorkNotification(Context ctx) { super(ctx); - update(1); + update(0); } @Override @@ -52,7 +52,9 @@ public class ProofOfWorkNotification extends AbstractNotification { .setOngoing(true) .setSmallIcon(R.drawable.ic_notification_proof_of_work) .setContentTitle(ctx.getString(R.string.proof_of_work_title)) - .setContentText(ctx.getString(R.string.proof_of_work_text, numberOfItems)) + .setContentText(numberOfItems == 0 + ? ctx.getString(R.string.proof_of_work_text_0) + : ctx.getString(R.string.proof_of_work_text_n, numberOfItems)) .setContentIntent(pendingIntent); notification = builder.build(); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 7eee67d..cb61cf3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -37,19 +37,13 @@ import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_N */ public class ProofOfWorkService extends Service { // Object to use as a thread-safe lock - private static final Object lock = new Object(); - private static ProofOfWorkEngine engine; + private static ProofOfWorkEngine engine = new MultiThreadedPOWEngine(); private static boolean calculating; private static final Queue<PowItem> queue = new LinkedList<>(); private static ProofOfWorkNotification notification; @Override public void onCreate() { - synchronized (lock) { - if (engine == null) { - engine = new MultiThreadedPOWEngine(); - } - } notification = new ProofOfWorkNotification(this); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 76473aa..95d73b8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,7 +42,8 @@ <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> <string name="connection_info_disconnected">Getrennt</string> <string name="connection_info_pending">Verbindung wird aufgebaut…</string> - <string name="proof_of_work_text">Arbeite am Versenden (%1$d in Warteschlange)</string> + <string name="proof_of_work_text_0">Arbeite am Versenden</string> + <string name="proof_of_work_text_n">Arbeite am Versenden (%1$d in Warteschlange)</string> <string name="proof_of_work_title">Proof of Work</string> <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> @@ -73,4 +74,5 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="error_illegal_address">Vielleicht hat es einen Tippfehler</string> <string name="export">Exportieren</string> <string name="confirm_export">Identität wirklich exportieren? Der Export wird die privaten Schlüssel unverschlüsselt enthalten.</string> + <string name="compose_message">Schreiben</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd6afc..49e4b52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,8 @@ <string name="connection_info_disconnected">Disconnected</string> <string name="connection_info_pending">Connecting…</string> <string name="proof_of_work_title">Proof of Work</string> - <string name="proof_of_work_text">Doing work to send message (%1$d queued)</string> + <string name="proof_of_work_text_0">Doing work to send message</string> + <string name="proof_of_work_text_n">Doing work to send message (%1$d queued)</string> <string name="error_invalid_sync_port">Invalid port in synchronization settings: %s</string> <string name="error_invalid_sync_host">Synchronization failed: Trusted node could not be reached.</string> <string name="compose_body_hint">Write message</string> @@ -76,4 +77,5 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="error_illegal_address">There might be a typo</string> <string name="export">Export</string> <string name="confirm_export">Do you really want to export your identity? The export will contain the unencrypted private keys.</string> + <string name="compose_message">Compose</string> </resources> From 709e333e784401b72f857e36ba1c9de3df2c029a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 12 Aug 2016 23:54:01 +0200 Subject: [PATCH 048/110] Updated libraries, most notably Jabit to develop-SNAPSHOT Also, finally added proper icon Known issue: the client seems to sever all connections after some time, I'll need to look into this. This might happen when 8 connections are reached for the first time. --- app/build.gradle | 32 +- .../db/migration/V3.1__Update_table_POW.sql | 2 + .../migration/V3.2__Update_table_message.sql | 4 + app/src/main/ic_launcher-web.png | Bin 0 -> 25944 bytes .../dissem/apps/abit/pow/ServerPowEngine.java | 4 +- .../repository/AndroidMessageRepository.java | 121 ++---- .../AndroidProofOfWorkRepository.java | 25 +- .../apps/abit/repository/SqlHelper.java | 5 +- .../abit/synchronization/SyncAdapter.java | 6 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 2236 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 1390 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 3097 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 5175 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7508 bytes build.gradle | 2 +- gradle.properties | 20 +- gradle/wrapper/gradle-wrapper.jar | Bin 49896 -> 53324 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 52 +-- gradlew.bat | 8 +- store/icon.svg | 406 ++++++++++++++++-- 21 files changed, 510 insertions(+), 181 deletions(-) create mode 100644 app/src/main/assets/db/migration/V3.1__Update_table_POW.sql create mode 100644 app/src/main/assets/db/migration/V3.2__Update_table_message.sql create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/app/build.gradle b/app/build.gradle index c2f57a2..e600ee0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,25 +1,25 @@ apply plugin: 'idea' apply plugin: 'com.android.application' +ext { + appName = "Abit" +} +if (project.hasProperty("project.configs") + && new File(project.property("project.configs") + appName + ".gradle").exists()) { + apply from: project.property("project.configs") + appName + ".gradle"; +} + android { - compileSdkVersion 23 - buildToolsVersion "23.0.3" + compileSdkVersion 24 + buildToolsVersion "24.0.1" defaultConfig { - applicationId "ch.dissem.apps.abit" + applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 - targetSdkVersion 23 + targetSdkVersion 24 versionCode 7 versionName "1.0-beta7" } - signingConfigs { - release { - storeFile file(keyStoreFile) - storePassword keyStorePassword - keyAlias signingKeyAlias - keyPassword signingKeyPassword - } - } buildTypes { release { minifyEnabled false @@ -29,12 +29,12 @@ android { } } -ext.jabitVersion = '1.1.0-SNAPSHOT' +ext.jabitVersion = 'develop-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.3.0' - compile 'com.android.support:support-v4:23.3.0' - compile 'com.android.support:design:23.3.0' + compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:support-v4:24.1.1' + compile 'com.android.support:design:24.1.1' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" diff --git a/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql b/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql new file mode 100644 index 0000000..d67a1b5 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql @@ -0,0 +1,2 @@ +ALTER TABLE POW ADD COLUMN expiration_time BIGINT; +ALTER TABLE POW ADD COLUMN message_id BIGINT; diff --git a/app/src/main/assets/db/migration/V3.2__Update_table_message.sql b/app/src/main/assets/db/migration/V3.2__Update_table_message.sql new file mode 100644 index 0000000..1eba39f --- /dev/null +++ b/app/src/main/assets/db/migration/V3.2__Update_table_message.sql @@ -0,0 +1,4 @@ +ALTER TABLE Message ADD COLUMN ack_data BINARY(32); +ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN next_try BIGINT; diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..ef455eeca98c6dba0571ee83ad9f560f23ea6f9a GIT binary patch literal 25944 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java index 1b94aa5..1dd81bb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java @@ -36,7 +36,7 @@ import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler @@ -78,7 +78,7 @@ public class ServerPowEngine implements ProofOfWorkEngine, InternalContext (request); cryptoMsg.signAndEncrypt( identity, - security().createPublicKey(identity.getPublicDecryptionKey()) + cryptography().createPublicKey(identity.getPublicDecryptionKey()) ); context.getNetworkHandler().send( Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 6b128b4..5a1765e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -22,17 +22,6 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; -import android.support.v4.database.DatabaseUtilsCompat; - -import ch.dissem.apps.abit.R; -import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,12 +32,19 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; -import static ch.dissem.apps.abit.repository.SqlHelper.join; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.ports.AbstractMessageRepository; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.Encode; /** * {@link MessageRepository} implementation using the Android SQL API. */ -public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder { +public class AndroidMessageRepository extends AbstractMessageRepository { private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class); private static final String TABLE_NAME = "Message"; @@ -58,9 +54,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont private static final String COLUMN_SENDER = "sender"; private static final String COLUMN_RECIPIENT = "recipient"; private static final String COLUMN_DATA = "data"; + private static final String COLUMN_ACK_DATA = "ack_data"; private static final String COLUMN_SENT = "sent"; private static final String COLUMN_RECEIVED = "received"; private static final String COLUMN_STATUS = "status"; + private static final String COLUMN_TTL = "ttl"; + private static final String COLUMN_RETRIES = "retries"; + private static final String COLUMN_NEXT_TRY = "next_try"; private static final String COLUMN_INITIAL_HASH = "initial_hash"; private static final String JOIN_TABLE_NAME = "Message_Label"; @@ -74,27 +74,11 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont private static final String LBL_COLUMN_COLOR = "color"; private static final String LBL_COLUMN_ORDER = "ord"; private final SqlHelper sql; - private final Context ctx; - private InternalContext bmc; + private final Context context; public AndroidMessageRepository(SqlHelper sql, Context ctx) { this.sql = sql; - this.ctx = ctx; - } - - @Override - public void setContext(InternalContext context) { - bmc = context; - } - - @Override - public List<Label> getLabels() { - return findLabels(null); - } - - @Override - public List<Label> getLabels(Label.Type... types) { - return findLabels("type IN (" + join(types) + ")"); + this.context = ctx; } public List<Label> findLabels(String where) { @@ -134,22 +118,22 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont } else { switch (type) { case INBOX: - text = ctx.getString(R.string.inbox); + text = context.getString(R.string.inbox); break; case DRAFT: - text = ctx.getString(R.string.draft); + text = context.getString(R.string.draft); break; case SENT: - text = ctx.getString(R.string.sent); + text = context.getString(R.string.sent); break; case UNREAD: - text = ctx.getString(R.string.unread); + text = context.getString(R.string.unread); break; case TRASH: - text = ctx.getString(R.string.trash); + text = context.getString(R.string.trash); break; case BROADCAST: - text = ctx.getString(R.string.broadcasts); + text = context.getString(R.string.broadcasts); break; default: text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); @@ -179,47 +163,7 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont ); } - @Override - public Plaintext getMessage(byte[] initialHash) { - List<Plaintext> results = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); - switch (results.size()) { - case 0: - return null; - case 1: - return results.get(0); - default: - throw new RuntimeException("This shouldn't happen, found " + results.size() + - " messages, one or none was expected"); - } - } - - @Override - public List<Plaintext> findMessages(Label label) { - if (label != null) { - return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label - .getId() + ")"); - } else { - return find("id NOT IN (SELECT message_id FROM Message_Label)"); - } - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { - return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + - "'"); - } - - @Override - public List<Plaintext> findMessages(BitmessageAddress sender) { - return find("sender=" + sender.getAddress()); - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status) { - return find("status='" + status.name() + "'"); - } - - private List<Plaintext> find(String where) { + protected List<Plaintext> find(String where) { List<Plaintext> result = new LinkedList<>(); // Define a projection that specifies which columns from the database @@ -231,9 +175,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, + COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, - COLUMN_STATUS + COLUMN_STATUS, + COLUMN_TTL, + COLUMN_RETRIES, + COLUMN_NEXT_TRY }; SQLiteDatabase db = sql.getReadableDatabase(); @@ -254,14 +202,21 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); - builder.from(bmc.getAddressRepository().getAddress(c.getString(c.getColumnIndex + builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex (COLUMN_SENDER)))); - builder.to(bmc.getAddressRepository().getAddress(c.getString(c.getColumnIndex + builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex (COLUMN_RECIPIENT)))); + builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))); builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex (COLUMN_STATUS)))); + builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL))); + builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES))); + int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY); + if (!c.isNull(nextTryColumn)) { + builder.nextTry(c.getLong(nextTryColumn)); + } builder.labels(findLabels(id)); result.add(builder.build()); c.moveToNext(); @@ -284,13 +239,13 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont // save from address if necessary if (message.getId() == null) { - BitmessageAddress savedAddress = bmc.getAddressRepository().getAddress(message + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message .getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); } - bmc.getAddressRepository().save(message.getFrom()); + ctx.getAddressRepository().save(message.getFrom()); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index 6ef5cd0..e352156 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -35,7 +35,7 @@ import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Strings; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler @@ -49,6 +49,8 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { private static final String COLUMN_VERSION = "version"; private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"; private static final String COLUMN_EXTRA_BYTES = "extra_bytes"; + private static final String COLUMN_EXPIRATION_TIME = "expiration_time"; + private static final String COLUMN_MESSAGE_ID = "message_id"; private final SqlHelper sql; @@ -114,16 +116,20 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { } @Override - public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + public void putObject(Item item) { try { SQLiteDatabase db = sql.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); - values.put(COLUMN_INITIAL_HASH, security().getInitialHash(object)); - values.put(COLUMN_DATA, Encode.bytes(object)); - values.put(COLUMN_VERSION, object.getVersion()); - values.put(COLUMN_NONCE_TRIALS_PER_BYTE, nonceTrialsPerByte); - values.put(COLUMN_EXTRA_BYTES, extraBytes); + values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.object)); + values.put(COLUMN_DATA, Encode.bytes(item.object)); + values.put(COLUMN_VERSION, item.object.getVersion()); + values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte); + values.put(COLUMN_EXTRA_BYTES, item.extraBytes); + if (item.message != null) { + values.put(COLUMN_EXPIRATION_TIME, item.expirationTime); + values.put(COLUMN_MESSAGE_ID, (Long) item.message.getId()); + } db.insertOrThrow(TABLE_NAME, null, values); } catch (SQLiteConstraintException e) { @@ -133,6 +139,11 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { } } + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + putObject(new Item(object, nonceTrialsPerByte, extraBytes)); + } + @Override public void removeObject(byte[] initialHash) { SQLiteDatabase db = sql.getWritableDatabase(); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 3041020..9a0f7ec 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -26,7 +26,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 3; + public static final int DATABASE_VERSION = 4; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -53,6 +53,9 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V2.1__Create_table_POW"); case 2: executeMigration(db, "V3.0__Update_table_address"); + case 3: + executeMigration(db, "V3.1__Update_table_POW"); + executeMigration(db, "V3.2__Update_table_message"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION. } diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index 0177f37..a1982c8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -44,7 +44,7 @@ import static ch.dissem.apps.abit.synchronization.Authenticator.ACCOUNT_SYNC; import static ch.dissem.apps.abit.synchronization.StubProvider.AUTHORITY; import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Sync Adapter to synchronize with the Bitmessage network - fetches @@ -107,13 +107,13 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { try { BitmessageAddress identity = Singleton.getIdentity(getContext()); byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); - byte[] signingKey = security().createPublicKey(identity.getPublicDecryptionKey()); + byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); List<byte[]> items = powRepo.getItems(); for (byte[] initialHash : items) { ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); - byte[] target = security().getProofOfWorkTarget(item.object, item + byte[] target = cryptography().getProofOfWorkTarget(item.object, item .nonceTrialsPerByte, item.extraBytes); CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index cde69bcccec65160d92116f20ffce4fce0b5245c..9c16971ceb1215ab77930c6689bd7b26ae212957 100644 GIT binary patch delta 2225 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 literal 3418 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 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index c133a0cbd379f5af6dbf1a899a0293ca5eccfad0..27a6ee47b3236deee63e53c8de6da6b2206624eb 100644 GIT binary patch delta 1372 zcmV-i1*7_&5$+0*BYy=oNkl<Zc-rlmTTGK@7{_%po6Ie7W~U3cgiW)UDA@(djKsLj z0%{PU3bY(r3Z(}QJ<;<S5NJUWiqHZg0xH=eZg(2vHkT!f7t6BjW>=17ce_|jwi}nZ z|K8`N5D_RX?N@ivC;6w%*L>~o_x5?;@BP|Mn=+NDOl2xl0)N!z6AHt-QcHVdm9^9I zEJF;%4b9oa6fgXiN|VnaclSPG!T%(WDoj2bv8_Aur&K0MtT$8p{Fogp#&@&1xIJ|k zo`8-W52i(=L}jd4@0-YynA^Wu>kahncKm+pO+5a3E9O6c1ApClg+Ct+orc^cyo9vP ze~%br3GAr0c7Oc3+Q~9!0QYBe@F#=TnWZSzT2QWS!Tp)tcy#wAD2z>l3lJMI#uC`B z@dW1U99=6<wsqp~FSp@lI1f@w2V|}QY8>5YwJU*pFT!s*h_WWDVDQ95Y{VE#;AKbX z@&og^2vqJK$ele)Z%||LaM0-?5&t$8jt8N%S@`jwGk+IH#MKa|T7oeU3o#KJF~$^d z4^Kbvjb4ZA;uYw-2BC1AMetk$Zj64!L4W*oH|8!CU~Zz2Jr?4N@x%Q1w-aw;pgj*o zl~oWo+mZ)obE=+=Ix!FnF%er_0{*dCP`r_A=n47pFPo?Y8Bb>Va`17v9O4#x67A9r zt*;lh3xDIBVUZ_c0kV#awZ#ojxi#`07dR@Z$LT~RSZgX>-7p1*;U1ahpTEemVS(^y z;sT#k)IlU|KtYufr&}Fyzz12YcA=%?JoP#I+-u9gQ!ens^jlEM4kF}we|3RM1&1%F zZbV_V3TIlKF-E-`Do+5G^P}*DudWK7asjj97=KR5)ewoL`0eKQWv|^1?_%wvSb{IA zQKP6<gEKnk%1P?%f}uOaCh63Qb`ezIv%&o+SL>lON!hg_YlVP__5YLb$7EU@mp7qY z?^;^ZoBVyS4_sO{Nz*1k`Qz5B_~)yv700;%>;I?WkJmNf1beK~+0oWJ0?)`yT%(@$ zNq>ANvI~W*g^Fs`%ixRU8dRtaP*{90^$sTlo|Hf+Y)edFJO>pTBT5+XaVBv>ra@VK zGinVEDD53kv1L-@S-7$aE<myMM{sxY03O_U4Zl$4p3dt$^DIAI-wAzN4eHuFkel3) zG@Bu9vG7rEbe>&;*Lnl&LKWmIkKh8{$bVHnS`w`Z#WhNttZhVzOvP8B(mD-F6<U-j znjmJQOcPsXbfVtsLxZgy4Q#}fj?Pu#X%aWLpM$l3Ojv=%R-)BH#XzcBt*CCbp+;}# zZ6PzVmBr-Zqpq-c8N8pvQ@b=WcokbJ)oZ}hg{!r*A6B+y5LDnv5e?fJ*6JO6q<<IS z-B6glyiGO&uRD8zZ`f>XNflgRQ9|ntt_@EulOVjICj?_)2=>7V;RO~Y2qq@HJ27}; z;38jMX)h$W01uNOm{h{I^@aI@;T)PwgFsY*fRcg_E{CV3l~yLYi4k5P8eP{wO5y27 zhW1H}B|QQZ+Eu0!-Z6NY&4)?8XMakI0EK!vmGI7?D{u`@!5h6#h)aN-@bg9mD0DlR zN_e`p^n@37uEe&Uz*bY=@N6;y6f=WMB|L3sNPyUgF_ysdM@rA+`@&ZrCM!X3sf1_G z9}*ieuJ;4)wt^B#G5dh;8OY;gBnU8R_{g=##3CxIJV|VE{WlQ(0rj1o*;UyG_I;Rt zbZ_1v(MGf9gWPEHM0;|VXYZ;xv=DagzWw<}iGf(6Oq&z=ljFJQ@9{4@<7|ntWGYjc e%2cLOU;hD546W}r#*u*l0000<MNUMnLSTZ?eU!8S delta 2195 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 VFwL6zQVak9002ovPDHLkV1kwc3L5|b diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index bfa42f0e7b91d006d22352c9ff2f134e504e3c1d..84b83a5d8e6ab38b53807070b686e5ae2459a573 100644 GIT binary patch delta 3093 zcmZuyX*3iJ7oIUpc4aI>F(O+c)XaoTlyxl0NVXVDD*GNX#x7e!_7Gzq3>vZ<qbykp z*>`WKkHiQyW&6DEch2|gy+7_b_nzlI_ug~QxzD;}HZ)lR&#|PhrD+y8^F7bi4f9o~ zmt!_8IY53g08-B-VvQtLKxQ8a@hjdG<DN{eg!DeoyLN9j4kArmU7tIz(G(M<##~-O z#^`FV^%#*=AJ4?z9hGuVZFrnl8t@5KH5*H8xc>C0W=P5SefR6-u3i<wuHfnC%PYU| zL%u;neSMAuJc$@1>{9-3PAPcM9v25Eya`LESzR+o7gNfTR?3FB)9ozlJO)Om;Yk@x z55M}im+kWed;Zd`vl`U+y3Vi+@V~Wc@NM$)siH?vCBf%gmF1(~+ST)+!aq|hbi3V9 z#x>=m`VV{ONKwauG2{68F3%qtEb2s2Ckq=}#hmynz<jau<XYt$yo@&Ga@wd-y1ecB zntY)IPSs`64*jJ^;Hbj`RVuw;$hhe!Gm3_bi$kcnwSn90d96D&f8ovp7PZ~gC3p9c z9r`?*@?tH>8wEp~mx_6+t4oCQZz+q18V)zB9=}Mg4P=4pIN&uLd~ChFr|9!}yUU~2 zFyIZ-<JLoF5!G^tb9XA6wXcyj7OKB#*U;E@WO~h$n{n?C{GH~am0m|nnildD)u;o9 zW5Id<A}ZxC>K9}LMC#q$b^uI;+c&WYm3^$UL~OO|e+`Lw<VoU{)d3$Y{2otpN1UF7 z%-B<sdrUN)@vex&-ya?_^&TvfwbLvg3&JHtIRR>loJcPFCX00I)k7?WB)vGTi5%+S z1?=Zk4vi{cBunm%VFLUmR;O}gDl5Y8J&W`)Fxss-o50kIP_sSL+Z_qSo{Hfe&_4x^ z|C;FkVJkZqrR;@fBQLL&+UUw1?(|RqV=w=>_1CxHWoxKOewzv@-<oSvGYFuKPS9CU z@51$Lt+N3SFfrX^8^%F<xX@HMu}is%Zx^1hH9TMO9Hrbk+h?+fj$v!zr+uj0_WCP* zCse#7bNofaN5L_lpUkSob2HAARzDyp^ohU9p_i_dyWx=ZEA=!MQBtZ<IDb~S2s9=q zf?kMUeZGKcRPX(W`5qox@km8ODiqXShnxFcSxMpm9PZ9bce{M?K1>OPHH$us?taXD z?zk73@#&9<kei06d%uiHA{&+ofn_$wf&;M7IN-oKPk!^b-#R-yw@vFK^@`cNe;WF% zDyg<QnzN`rNl#w2)iDd0jUXsGR|VVqXW}`LVnzrJ-JB~NT7Tj#8H0dJG-fv%YZZ-i zqgX*E1eANAZw0QovqQUkRD5eiJv4lsn>+c<_tX5%O^C7mE|T=^R@INV7MFcR5T(j^ z+q=DN`ONB0#p~vHQ19_tl4Negr^m|+HZ%1@o-~5;-s^`EdPr2~|GVG^Rno!lr8JTb zGy8D6=f-z)3w}r)-8^SBHtks4ao<ihc>mOsSRPwO*1>j)@HfVghxBmG<(dm*Z^B~K zrCWnp1y7zNzou%3Y+(TcSn-{dAfskrr_Qr_zt4qU)Yk+0zdWU%t9_5DB!sp^H8{CU z1JvF76r3Q}uQaW<9%@hEmoHQHVsCSRv1d3@>tN=@CFIX@>bE`b&k6D~DE$K%E#U}Y zpNh^MOlima25;ACbUL|xB{|;B@p!m=scZ-k>YE$7Wn-LB0Sp+Bni&)s5s1mpb>)4D zfG}w{BqVGo`LOU$Z!g`>0ai;qT<eiEGUF<5aN`4CFaWzn*K6ZP;F=dH-OGH_aaYyB z^!O|RP}AvC?_y1V#1}b4EJ*TCe>pU(!*7VIC2%hBb-}S0LWm(dW7%PO7RE@)InjIm zqV{1j6k^F)Z76+BEzYmu0YBW@t~h@eWstvUdD@3d<BTZR5klV*;ps$gelOsi@}}VZ z&sV(K4J5qGB|2W|F%7?9RBYp}+@8^rV>|y+#}z4sdGmX9n6uV{*Oz8NZ~TUieyiO1 zJ%4>bxV3KqXERpcld6ZjCmsRw1-C8ii_Bz{6o~{$p+|kxnvt=#MiaxanVB(ev_N{J zV47(KHIY)$mO67yE}$`me5g1M6J8O2s<hz;*#YO$@#LcWEYAHh4JI+u=%?%hGLjPc z9(y+gUjVa!J&_&l1%}Y%ip%-K%<Yx;`KH+wC~x*}$J}?~e?Z&+O4WQS6m0$&{vdN& zwysNO4Gh8g;6|cq$gkbGp^9e?%x*O|%^)h+rmkzH>;;(mf2AkYk(gnr;H5t_jbA$b zE93be@KTQq2G#`2CZo(iQ9zaG^Y144{zO-L{HU(%Tw50ZCXuiuHcotROewq92Be^< z)I?NUL;MkLr!UzK1X7GL0;8(c%Y@|99{B%GaDxa=zaq&~>YTY*p#^emBB-OWY;CgU zQ`z{9xP&_fNN;xy_f^-l+!7uU6f81=;FpQ_^nEcha=~hFjS2Py9O#14OLZ;yu5t;* z(GxXvyg-yI^>b8NH6MFH?U6ITQ9Y6osMe)W>Y@R4`MJSIc5NX=1gzx9z-#of6gg34 zIrD#b`!ABj@27J-x!^_ElBg@YMeFr&wsaJ{-F)cn3=n)n)O0uFs-4=A9kROoiOmW> z)gHZ?)&t6m!6fO}^_TLEQVY?a%qLQa4d-jub>GH%@pD*nc{_$RITjYh9);qx&6XBX zJB3+7>=o)}7PB&+(A05Rt7$*S=xcI5{ofCcJ(TUoTSGp_^V0}myYgn;`oef5z-1|e zyV2eZ4*yiL02?M_^!=VL1)1~C3*n6-hc6Oai`I5IZZ?9=F?Qd;GY>BjuiH+Cc~*D| zkh(1<wZI6bVU7*A#G*S@!ESO<OfZO|2dq=H<1?3rbu0%zbZx|SYel?C9wYq|*I<DD zpnzSIh3noHRw%yN9GwP!oRn*v=s?hu>39}E3Gfw^_{1JhnjfI&SjaQg<7M@j+A57` zms3oQW$1FmiG}i)it;BP30vIzid~h<Pb}<|{g!my)J3behIL7l!MlYf;?Yh9v%D%M zK}jYaLFBMk4@nYvAG+n*HLC)JsHe+>0D^M1wtM!p*e<7Yl9195VX=)>Hf*_P0A*w^ zFMB`MtRMk}LYd+w-DjQbc<)A2lU0G=B5%>U&wE`{eAq>d-kFD9UAon@@=Vic1f0m% zgX7cIfN?b~C;%Jx!v}_yuCi^rrtFk~sBaEy>m_ljp`53scI^ZJ>43$F5#75YM+3<x z2XkWr=rDBz+A#9Yzsu|-u)rq6yASg)PB#sqOGtY?zl-QFp6OQn;jiPaBw!)N0iu|9 z5r{%;cJ2K|t=s-zJv*#1Lia4nqorG2bIK*|EXWCh4%$0`#TXd=-jw+IxfgJ}gwT4x zd54XzWZ|&$5w{z102RAEG}U6l6k^za8X>ye1DIw8nEf3MI~hI;I-vlZpwADo{zHgg zf$f&NsTZ&^nlf3hFl`nloCPsSF@pJlFtV(w#6h6x98@d^@C+SBAU|Q9rJpQ@x87%u zyk5o1ka_>gQLyWy=Ly2j8lWdh)79+ZvYXn9|9eh=DI0wobM>>-;u$H!Bp@#GMn2m~ z+DovGcS(Xv^HC)Ttk|l@iKj*CWlo&C=Ivg}$(l9ne@U6KU&ekE&9=nD)w(RaK{Ig> z$^a(B=b<JIg--@aMu%NqJmC0SzFh&Ye1CQ|PK5k=IwyLB6fi7bQ>lhB{t@EME6ZQ> zhO}+_aKA^g5n@=TcZvQChE*r$#)TOB8)|07#Gtsi%PPV9_(cLgq$@A*3Ref-je%ux zPASe`s>7nbW!ESK1?Q2;Ti-1diP0T*07=@65N9fvIaca*0%N<|w<PusK5vgArR%wx zv6-2>yg5|Q1`y_;|K9r5sZ|7@qUkxnS9$2h$-y(@Rc{D4_Qt+!dCt7-IPXpf>Pc(q zZS(MrFCN3kqD4tpiF*{&o(VAV{=0P5mj_!sU@TCIH|GIwrEDb3?d|n7($at2w{2D~ zOmNiM=<_C&@a6=Kgu!eeU(l)Z2bM>(S0eBUJt1*@bB~S%lCTnAn1z&c_y=!t!N*K) f{hLd=vwR#MqOd|q;r_A#I5B-~W3B3&*oglEPleO; literal 4842 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 324e72cdd7480cb983fa1bcc7ce686e51ef87fe7..393ce2cd8959481f12bc5c165c14ba0ab7c6dcb1 100644 GIT binary patch literal 5175 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 aT$3_KzFuoUN>V`(prx*dZcu#?_5T3F0CqP3 literal 7718 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9590643353a1013585fc206ee28c1e3f71135c GIT binary patch literal 7508 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 literal 0 HcmV?d00001 diff --git a/build.gradle b/build.gradle index 8484636..e8ad5dc 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0' + classpath 'com.android.tools.build:gradle:2.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index ab12df9..d68d912 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -20,10 +20,18 @@ -# Signing and encryption settings # Don't change those parameters - override those properties in the # gradle.properties file in your home directory instead -keyStoreFile= -keyStorePassword= -signingKeyAlias= -signingKeyPassword= \ No newline at end of file +# Create a directory containing the signing configs and keystores +project.configs= +# The signing config should be called 'Abit.gradle' and look like this: +# android { +# signingConfigs { +# release { +# storeFile file("C:/Path/To/Keystores/abit.jks") +# storePassword "****************" +# keyAlias "Abit" +# keyPassword "****************" +# } +# } +# } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a8698b08ecc4158d828ca593c4928e9dd..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 46699 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 Q0Mb`5u^>&^@z>)22a<glo&W#< delta 43491 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 551b2fa..e50d996 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Apr 11 14:09:13 CEST 2016 +#Fri Aug 12 22:10:25 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew index 91a7e26..27309d9 100755 --- a/gradlew +++ b/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/gradlew.bat b/gradlew.bat index aec9973..f6d5974 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/store/icon.svg b/store/icon.svg index ef7a4a6..695b713 100644 --- a/store/icon.svg +++ b/store/icon.svg @@ -7,20 +7,356 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="512" - height="512" - viewBox="0 0 512.00001 512.00001" + width="256" + height="256" + viewBox="0 0 256 256" id="svg2" version="1.1" inkscape:version="0.91 r13725" - sodipodi:docname="icon.svg" - inkscape:export-filename="C:\Users\chrig\Documents\Projekte\Abit\store\icon.png" - inkscape:export-xdpi="90" - inkscape:export-ydpi="90"> + sodipodi:docname="icon.svg"> <defs - id="defs4" /> + id="defs4"> + <linearGradient + inkscape:collect="always" + id="linearGradient4475"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4477" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4479" /> + </linearGradient> + <filter + id="filter4284" + inkscape:label="z-depth1" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4286" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.12" /> + <feComposite + id="feComposite4288" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4290" + result="blur" + stdDeviation="1.5" /> + <feOffset + id="feOffset4292" + result="offset" + dy="1" + dx="0" /> + <feComposite + id="feComposite4294" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4332" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.24" + id="feFlood4334" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4336" /> + <feGaussianBlur + result="blur" + stdDeviation="1" + id="feGaussianBlur4338" /> + <feOffset + result="offset" + dy="1" + dx="0" + id="feOffset4340" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4342" /> + </filter> + <filter + id="filter4346" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4348" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4350" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4352" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4354" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4356" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4358" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4360" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4362" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4364" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4366" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4368" /> + </filter> + <filter + id="filter4375" + inkscape:label="z-depth3" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4377" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.19" /> + <feComposite + id="feComposite4379" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4381" + result="blur" + stdDeviation="10" /> + <feOffset + id="feOffset4383" + result="offset" + dy="10" + dx="0" /> + <feComposite + id="feComposite4385" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4387" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4389" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4391" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4393" /> + <feOffset + result="offset" + dy="6" + dx="0" + id="feOffset4395" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4397" /> + </filter> + <filter + id="filter4419" + inkscape:label="z-depth4" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4421" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.25" /> + <feComposite + id="feComposite4423" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4425" + result="blur" + stdDeviation="14" /> + <feOffset + id="feOffset4427" + result="offset" + dy="14" + dx="0" /> + <feComposite + id="feComposite4429" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4431" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.22" + id="feFlood4433" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4435" /> + <feGaussianBlur + result="blur" + stdDeviation="5" + id="feGaussianBlur4437" /> + <feOffset + result="offset" + dy="10" + dx="0" + id="feOffset4439" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4441" /> + </filter> + <filter + id="filter4449" + inkscape:label="z-depth5" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4451" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.3" /> + <feComposite + id="feComposite4453" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4455" + result="blur" + stdDeviation="19" /> + <feOffset + id="feOffset4457" + result="offset" + dy="19" + dx="0" /> + <feComposite + id="feComposite4459" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4461" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.22" + id="feFlood4463" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4465" /> + <feGaussianBlur + result="blur" + stdDeviation="6" + id="feGaussianBlur4467" /> + <feOffset + result="offset" + dy="15" + dx="0" + id="feOffset4469" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4471" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4475" + id="linearGradient4481" + x1="-96.75" + y1="-92.613281" + x2="224" + y2="228.13672" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-7,822.68216)" /> + </defs> <sodipodi:namedview id="base" pagecolor="#ffffff" @@ -28,9 +364,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.35" - inkscape:cx="-602.14286" - inkscape:cy="520" + inkscape:zoom="1" + inkscape:cx="228.5498" + inkscape:cy="137.07133" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -48,7 +384,7 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title /> + <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> @@ -56,23 +392,33 @@ inkscape:label="Ebene 1" inkscape:groupmode="layer" id="layer1" - transform="translate(0,-540.36216)"> - <path - sodipodi:type="star" - style="opacity:1;fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path3343" - sodipodi:sides="7" - sodipodi:cx="254.65251" - sodipodi:cy="795.56248" - sodipodi:r1="231.12856" - sodipodi:r2="76.300064" - sodipodi:arg1="-1.4219063" - sodipodi:arg2="-0.97310738" - inkscape:flatsided="false" - inkscape:rounded="1.39" - inkscape:randomized="0" - d="m 288.93824,566.99107 c 227.8089,34.17135 -181.77145,35.86897 8.65088,165.49887 190.42233,129.6299 41.82424,-252.04738 157.14453,-52.63373 C 570.05394,879.26986 313.35752,560.1055 330.73516,789.8066 348.1128,1019.5077 553.87073,665.35713 469.86388,879.85055 385.85704,1094.344 475.34218,694.65489 306.58941,851.45756 137.83665,1008.2602 543.01068,948.319 322.93556,1016.3743 102.86045,1084.4295 471.14302,905.18976 243.33412,871.01841 15.525224,836.84705 315.01104,1116.2521 124.58871,986.62222 -65.833623,856.99233 303.92209,1033.1731 188.60181,833.75941 73.281523,634.34576 41.560203,1042.6994 24.182553,812.99831 6.804904,583.29722 99.600168,982.23077 183.60702,767.73737 267.61388,553.24396 -71.427781,783.04756 97.324978,626.24488 266.07773,469.4422 12.035822,790.72346 232.11094,722.66821 452.18606,654.61296 61.129346,532.81973 288.93824,566.99107 Z" - inkscape:transform-center-x="-12.36068" - inkscape:transform-center-y="-11.435089" /> + transform="translate(0,-796.36216)"> + <g + id="g4508" + inkscape:export-filename="C:\Users\chrig\Abit2.png" + inkscape:export-xdpi="602.35297" + inkscape:export-ydpi="602.35297"> + <path + sodipodi:nodetypes="sssssssss" + style="fill:#819ba8;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 230.00001,827.68211 -204.000009,0 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,152.99999 c -5.523e-5,14.0833 11.4167,25.5001 25.5,25.5001 l 204.000009,0 c 14.0833,0 25.50005,-11.4168 25.49999,-25.5001 l 0,-152.99999 c 0,-14.1525 -11.47499,-25.5 -25.49999,-25.5 z" + id="path4226" /> + <path + id="path4435" + d="m 26,827.68216 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,1.21289 127.5,79.6875 127.5,-79.6875 0,-1.21289 c 0,-14.1525 -11.475,-25.5 -25.5,-25.5 l -204,0 z" + style="fill:#607d8b;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" /> + <path + id="path4456" + d="M 84.9375,845.9556 C 99.46494,905.00376 49.357218,869.16386 30,929.84622 c 0,2.61651 1.051586,4.98591 2.755859,6.71094 0.04073,0.0412 52.392395,52.39156 95.126951,95.12504 l 102.11719,0 c 10.99514,0 20.36483,-6.9593 23.94531,-16.7129 C 197.53224,958.55338 85.073657,846.08936 84.9375,845.9556 Z" + style="opacity:0.70099996;fill:url(#linearGradient4481);fill-opacity:1" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffc107;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 68.204082,915.51889 a 9.5510204,9.5510204 0 0 0 9.55102,-9.55102 c 0,-5.30081 -4.297959,-9.55102 -9.55102,-9.55102 a 9.5510204,9.5510204 0 0 0 -9.551021,9.55102 9.5510204,9.5510204 0 0 0 9.551021,9.55102 M 96.85714,872.5393 a 9.5510204,9.5510204 0 0 1 9.55102,9.55102 l 0,47.7551 a 9.5510204,9.5510204 0 0 1 -9.55102,9.55102 l -57.30612,0 A 9.5510204,9.5510204 0 0 1 30,929.84542 l 0,-47.7551 c 0,-5.30081 4.297959,-9.55102 9.55102,-9.55102 l 4.775511,0 0,-9.55102 a 23.877551,23.877551 0 0 1 23.877551,-23.87755 23.877551,23.877551 0 0 1 23.877551,23.87755 l 0,9.55102 4.775507,0 M 68.204082,848.66175 a 14.326531,14.326531 0 0 0 -14.326531,14.32653 l 0,9.55102 28.653061,0 0,-9.55102 a 14.326531,14.326531 0 0 0 -14.32653,-14.32653 z" + id="path4160" /> + </g> </g> </svg> From f0e03f15a359c85e305d33f98cc122be09361f0e Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 13 Aug 2016 12:06:41 +0200 Subject: [PATCH 049/110] Fixed layout for composing messages --- .../apps/abit/AddressDetailActivity.java | 2 +- .../apps/abit/MessageDetailActivity.java | 2 +- .../ch/dissem/apps/abit/SettingsActivity.java | 2 +- .../res/layout/fragment_compose_message.xml | 15 ++++---- .../res/layout/scrolling_toolbar_layout.xml | 37 +++++++++++++++++++ app/src/main/res/layout/toolbar_layout.xml | 24 ++++++------ 6 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 app/src/main/res/layout/scrolling_toolbar_layout.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java index e26749a..077a5d4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java @@ -38,7 +38,7 @@ public class AddressDetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.toolbar_layout); + setContentView(R.layout.scrolling_toolbar_layout); final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java index 77be840..7957185 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java @@ -22,7 +22,7 @@ public class MessageDetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.toolbar_layout); + setContentView(R.layout.scrolling_toolbar_layout); final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index 9199c42..6a86ef2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -11,7 +11,7 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.toolbar_layout); + setContentView(R.layout.scrolling_toolbar_layout); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/app/src/main/res/layout/fragment_compose_message.xml b/app/src/main/res/layout/fragment_compose_message.xml index 72c23c7..35153ca 100644 --- a/app/src/main/res/layout/fragment_compose_message.xml +++ b/app/src/main/res/layout/fragment_compose_message.xml @@ -1,9 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true" - android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" @@ -16,7 +15,7 @@ android:layout_height="wrap_content" android:hint="@string/to" android:inputType="textNoSuggestions" - android:singleLine="true" /> + android:singleLine="true"/> </android.support.design.widget.TextInputLayout> @@ -30,7 +29,7 @@ android:layout_height="wrap_content" android:hint="@string/subject" android:inputType="textEmailSubject" - android:textAppearance="?android:attr/textAppearanceLarge" /> + android:textAppearance="?android:attr/textAppearanceLarge"/> </android.support.design.widget.TextInputLayout> @@ -39,9 +38,9 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" + android:gravity="start|top" android:hint="@string/compose_body_hint" android:inputType="textMultiLine|textCapSentences" - android:gravity="top" - android:isScrollContainer="true" /> + android:scrollbars="vertical"/> </LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/scrolling_toolbar_layout.xml b/app/src/main/res/layout/scrolling_toolbar_layout.xml new file mode 100644 index 0000000..9abd179 --- /dev/null +++ b/app/src/main/res/layout/scrolling_toolbar_layout.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + tools:context=".ComposeMessageActivity"> + + <FrameLayout android:id="@+id/content" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:context=".ComposeMessageActivity" + tools:layout="@layout/fragment_compose_message"/> + + <android.support.design.widget.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + android:elevation="4dp" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + tools:ignore="UnusedAttribute"/> + + </android.support.design.widget.AppBarLayout> + +</android.support.design.widget.CoordinatorLayout> + diff --git a/app/src/main/res/layout/toolbar_layout.xml b/app/src/main/res/layout/toolbar_layout.xml index ab60f4a..ce3946d 100644 --- a/app/src/main/res/layout/toolbar_layout.xml +++ b/app/src/main/res/layout/toolbar_layout.xml @@ -1,20 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true" + tools:context=".ComposeMessageActivity"> - <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/content" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_below="@id/toolbar" - app:layout_behavior="@string/appbar_scrolling_view_behavior" - tools:context=".MessageListActivity" - tools:layout="@android:layout/list_content" /> + <FrameLayout android:id="@+id/content" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="@dimen/action_bar_offset" + tools:context=".ComposeMessageActivity" + tools:layout="@layout/fragment_compose_message"/> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" From e102908acf05ebd9a28268e64a5e929886e090be Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 13 Aug 2016 23:24:42 +0200 Subject: [PATCH 050/110] Fixed sending messages --- .../AndroidProofOfWorkRepository.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index e352156..5fccc5d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; +import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; @@ -40,7 +41,8 @@ import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler */ -public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { +public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext + .ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); private static final String TABLE_NAME = "POW"; @@ -53,11 +55,17 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { private static final String COLUMN_MESSAGE_ID = "message_id"; private final SqlHelper sql; + private InternalContext bmc; public AndroidProofOfWorkRepository(SqlHelper sql) { this.sql = sql; } + @Override + public void setContext(InternalContext internalContext) { + this.bmc = internalContext; + } + @Override public Item getItem(byte[] initialHash) { // Define a projection that specifies which columns from the database @@ -66,7 +74,9 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, - COLUMN_EXTRA_BYTES + COLUMN_EXTRA_BYTES, + COLUMN_EXPIRATION_TIME, + COLUMN_MESSAGE_ID }; SQLiteDatabase db = sql.getReadableDatabase(); @@ -79,12 +89,24 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository { if (!c.isAfterLast()) { int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); - return new Item( - Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob - .length), - c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), - c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) - ); + if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) { + return new Item( + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) + ); + } else { + return new Item( + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), + c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), + bmc.getMessageRepository().getMessage( + c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) + ); + } } } throw new RuntimeException("Object requested that we don't have. Initial hash: " + From 044776acbb3a5613acc2249920cb1e2456d0f8ca Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 2 Sep 2016 17:16:34 +0200 Subject: [PATCH 051/110] Added store graphics --- store/function_graphic.png | Bin 3297 -> 89368 bytes store/function_graphic.svg | 310 +++++++++++++++++++++++++++- store/promotional_graphic.png | Bin 0 -> 9847 bytes store/promotional_graphic.svg | 368 ++++++++++++++++++++++++++++++++++ store/tv_banner.png | Bin 0 -> 117322 bytes store/tv_banner.svg | 367 +++++++++++++++++++++++++++++++++ 6 files changed, 1038 insertions(+), 7 deletions(-) create mode 100644 store/promotional_graphic.png create mode 100644 store/promotional_graphic.svg create mode 100644 store/tv_banner.png create mode 100644 store/tv_banner.svg diff --git a/store/function_graphic.png b/store/function_graphic.png index 8c281772568a119e18c6346d854f12610df21f16..d2d4c00b544a46aeff7df00878db9e0334449fc1 100644 GIT binary patch literal 89368 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 literal 3297 zcmeAS@N?(olHy`uVBq!ia0y~yU;#22zi_Ytsl{sX+CYk>*vT`50|;t3QaXTq&H|6f zVxao#Ak65bF}ngNC|TkfQ4*Y=R#Ki=l*&+EUaps!mtCBkSdglhUz9%kosAR&15cc% zi(^Q|oVPa&c^eo64jf?D|1tegpEwh@7zgLx;0rrX+pMeIX8BwKXx@YJYwMYTbi;Q^ zAju$*03sb47(o;x$Z-rTAZ4Qxqd_v77)G<fXvr{ICXUtyqZQ<6(_pkwG}=xakuBr{ Yyn*fPn~rgIB!RMkr>mdKI;Vst0Qj)mv;Y7A diff --git a/store/function_graphic.svg b/store/function_graphic.svg index 7b4db20..e02e318 100644 --- a/store/function_graphic.svg +++ b/store/function_graphic.svg @@ -7,6 +7,7 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="1024" @@ -16,11 +17,244 @@ version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="function_graphic.svg" - inkscape:export-filename="C:\Users\chrig\Documents\Projekte\Abit\store\function_graphic.png" + inkscape:export-filename="C:\Users\chrig\Projects\Abit\store\function_graphic.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> <defs - id="defs4" /> + id="defs4"> + <linearGradient + inkscape:collect="always" + id="linearGradient4296"> + <stop + style="stop-color:#ffecb3;stop-opacity:1;" + offset="0" + id="stop4298" /> + <stop + style="stop-color:#ffc107;stop-opacity:1" + offset="1" + id="stop4300" /> + </linearGradient> + <filter + id="filter4346" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4348" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4350" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4352" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4354" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4356" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4358" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4360" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4362" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4364" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4366" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4368" /> + </filter> + <filter + id="filter4222" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4224" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4226" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4228" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4230" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4232" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4234" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4236" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4238" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4240" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4242" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4244" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4475" + id="linearGradient4481" + x1="-96.75" + y1="-92.613281" + x2="224" + y2="228.13672" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-7,822.68216)" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4475"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4477" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4479" /> + </linearGradient> + <filter + id="filter4250" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4252" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4254" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4256" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4258" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4260" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4262" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4264" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4266" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4268" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4270" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4272" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4296" + id="linearGradient4302" + x1="477.62885" + y1="551.84436" + x2="477.62885" + y2="1052.88" + gradientUnits="userSpaceOnUse" /> + </defs> <sodipodi:namedview id="base" pagecolor="#ffffff" @@ -28,9 +262,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.35" - inkscape:cx="-249.28571" - inkscape:cy="520" + inkscape:zoom="0.49497475" + inkscape:cx="504.44126" + inkscape:cy="273.50135" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -48,7 +282,7 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> + <dc:title /> </cc:Work> </rdf:RDF> </metadata> @@ -58,11 +292,73 @@ id="layer1" transform="translate(0,-552.36216)"> <rect - style="opacity:1;fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + style="opacity:1;fill:url(#linearGradient4302);fill-opacity:1.0;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect3336" width="1024" height="500" x="2.8571429" y="552.36218" /> + <path + style="fill:none;fill-rule:evenodd;stroke:#607d8b;stroke-width:3.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" + d="m 237.14286,970.07645 c 21.22559,1.59192 45.12779,12.2133 65.65697,30.54595 56.85434,50.7713 163.8859,-36.32334 317.58615,16.9642 50.86665,17.6354 101.47749,18.6011 139.26053,12.619 122.36869,-19.3742 50.78221,-129.85677 -49.14945,-119.58354 -180.48828,18.55465 -81.95538,130.45724 145.95862,63.71699 171.37962,-50.18523 -9.54808,-235.20057 49.25861,-189.97689" + id="path4294" + inkscape:connector-curvature="0" + sodipodi:nodetypes="csssssc" /> + <g + transform="matrix(1.1248833,0,0,1.1248833,668.4078,-306.70747)" + id="g4508" + inkscape:export-filename="C:\Users\chrig\Abit2.png" + inkscape:export-xdpi="602.35297" + inkscape:export-ydpi="602.35297"> + <path + sodipodi:nodetypes="sssssssss" + style="fill:#819ba8;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 230.00001,827.68211 -204.000009,0 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,152.99999 c -5.523e-5,14.0833 11.4167,25.5001 25.5,25.5001 l 204.000009,0 c 14.0833,0 25.50005,-11.4168 25.49999,-25.5001 l 0,-152.99999 c 0,-14.1525 -11.47499,-25.5 -25.49999,-25.5 z" + id="path4226" /> + <path + id="path4435" + d="m 26,827.68216 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,1.21289 127.5,79.6875 127.5,-79.6875 0,-1.21289 c 0,-14.1525 -11.475,-25.5 -25.5,-25.5 l -204,0 z" + style="fill:#607d8b;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" /> + <path + id="path4456" + d="M 84.9375,845.9556 C 99.46494,905.00376 49.357218,869.16386 30,929.84622 c 0,2.61651 1.051586,4.98591 2.755859,6.71094 0.04073,0.0412 52.392395,52.39156 95.126951,95.12504 l 102.11719,0 c 10.99514,0 20.36483,-6.9593 23.94531,-16.7129 C 197.53224,958.55338 85.073657,846.08936 84.9375,845.9556 Z" + style="opacity:0.70099996;fill:url(#linearGradient4481);fill-opacity:1" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffc107;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 68.204082,915.51889 a 9.5510204,9.5510204 0 0 0 9.55102,-9.55102 c 0,-5.30081 -4.297959,-9.55102 -9.55102,-9.55102 a 9.5510204,9.5510204 0 0 0 -9.551021,9.55102 9.5510204,9.5510204 0 0 0 9.551021,9.55102 M 96.85714,872.5393 a 9.5510204,9.5510204 0 0 1 9.55102,9.55102 l 0,47.7551 a 9.5510204,9.5510204 0 0 1 -9.55102,9.55102 l -57.30612,0 A 9.5510204,9.5510204 0 0 1 30,929.84542 l 0,-47.7551 c 0,-5.30081 4.297959,-9.55102 9.55102,-9.55102 l 4.775511,0 0,-9.55102 a 23.877551,23.877551 0 0 1 23.877551,-23.87755 23.877551,23.877551 0 0 1 23.877551,23.87755 l 0,9.55102 4.775507,0 M 68.204082,848.66175 a 14.326531,14.326531 0 0 0 -14.326531,14.32653 l 0,9.55102 28.653061,0 0,-9.55102 a 14.326531,14.326531 0 0 0 -14.32653,-14.32653 z" + id="path4160" /> + </g> + <text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:298.45654297px;line-height:125%;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter4346)" + x="64.155144" + y="851.16418" + id="text4230" + sodipodi:linespacing="125%"><tspan + sodipodi:role="line" + id="tspan4232" + x="64.155144" + y="851.16418">Abit</tspan></text> + <g + id="g4254" + transform="matrix(0.98088654,0.19458057,-0.19458057,0.98088654,-982.26237,72.359809)"> + <ellipse + ry="50" + rx="20" + cy="639.6261" + cx="1231.6552" + id="path4246" + style="opacity:1;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" /> + <path + id="rect4244" + transform="translate(0,552.36216)" + d="m 1232.3867,37.263672 0,0.0918 a 20,50 0 0 1 19.2676,49.908203 20,50 0 0 1 -19.2676,49.935545 l 0,0.0644 139.2676,0 0.7324,-0.0312 c 10.7487,-1.01692 19.2609,-23.07902 19.2676,-49.968745 -0.012,-26.879125 -8.5232,-48.924883 -19.2676,-49.955078 l -0.7324,-0.04492 -139.2676,0 z" + style="opacity:1;fill:#819ba8;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4222)" + inkscape:connector-curvature="0" /> + </g> </g> </svg> diff --git a/store/promotional_graphic.png b/store/promotional_graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..2218c7994fc66d90dd115645bcae26aef19f1e3b GIT binary patch literal 9847 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 QlGr<0Nd<{=ke=WF05C~HaR2}S literal 0 HcmV?d00001 diff --git a/store/promotional_graphic.svg b/store/promotional_graphic.svg new file mode 100644 index 0000000..2132360 --- /dev/null +++ b/store/promotional_graphic.svg @@ -0,0 +1,368 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="180" + height="120" + viewBox="0 0 180 120" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="promotional_graphic.svg" + inkscape:export-filename="C:\Users\chrig\Projects\Abit\store\promotional_graphic.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <defs + id="defs4"> + <linearGradient + inkscape:collect="always" + id="linearGradient4296"> + <stop + style="stop-color:#ffecb3;stop-opacity:1;" + offset="0" + id="stop4298" /> + <stop + style="stop-color:#ffc107;stop-opacity:1" + offset="1" + id="stop4300" /> + </linearGradient> + <filter + id="filter4346" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4348" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4350" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4352" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4354" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4356" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4358" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4360" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4362" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4364" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4366" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4368" /> + </filter> + <filter + id="filter4222" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4224" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4226" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4228" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4230" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4232" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4234" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4236" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4238" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4240" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4242" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4244" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4475" + id="linearGradient4481" + x1="-96.75" + y1="-92.613281" + x2="224" + y2="228.13672" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-7,822.68216)" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4475"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4477" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4479" /> + </linearGradient> + <filter + id="filter4250" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4252" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4254" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4256" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4258" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4260" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4262" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4264" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4266" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4268" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4270" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4272" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4296" + id="linearGradient4302" + x1="477.62885" + y1="551.84436" + x2="477.62885" + y2="1052.88" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.17578125,0,0,0.24,-0.50223215,799.79526)" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="71.383061" + inkscape:cy="65.949753" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1280" + inkscape:window-height="657" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + showguides="false" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Ebene 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-932.36216)"> + <rect + style="opacity:1;fill:url(#linearGradient4302);fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect3336" + width="180" + height="120" + x="4.83129e-009" + y="932.36218" /> + <path + style="fill:none;fill-rule:evenodd;stroke:#607d8b;stroke-width:3.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" + d="m 237.14286,970.07645 c 21.22559,1.59192 45.12779,12.2133 65.65697,30.54595 56.85434,50.7713 163.8859,-36.32334 317.58615,16.9642 50.86665,17.6354 101.47749,18.6011 139.26053,12.619 122.36869,-19.3742 50.78221,-129.85677 -49.14945,-119.58354 -180.48828,18.55465 -81.95538,130.45724 145.95862,63.71699 171.37962,-50.18523 -9.54808,-235.20057 49.25861,-189.97689" + id="path4294" + inkscape:connector-curvature="0" + sodipodi:nodetypes="csssssc" + transform="matrix(0.17578125,0,0,0.17578125,-0.50223215,854.72639)" /> + <g + transform="matrix(0.19773339,0,0,0.19773339,116.99133,788.81297)" + id="g4508" + inkscape:export-filename="C:\Users\chrig\Abit2.png" + inkscape:export-xdpi="602.35297" + inkscape:export-ydpi="602.35297"> + <path + sodipodi:nodetypes="sssssssss" + style="fill:#819ba8;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 230.00001,827.68211 -204.000009,0 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,152.99999 c -5.523e-5,14.0833 11.4167,25.5001 25.5,25.5001 l 204.000009,0 c 14.0833,0 25.50005,-11.4168 25.49999,-25.5001 l 0,-152.99999 c 0,-14.1525 -11.47499,-25.5 -25.49999,-25.5 z" + id="path4226" /> + <path + id="path4435" + d="m 26,827.68216 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,1.21289 127.5,79.6875 127.5,-79.6875 0,-1.21289 c 0,-14.1525 -11.475,-25.5 -25.5,-25.5 l -204,0 z" + style="fill:#607d8b;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" /> + <path + id="path4456" + d="M 84.9375,845.9556 C 99.46494,905.00376 49.357218,869.16386 30,929.84622 c 0,2.61651 1.051586,4.98591 2.755859,6.71094 0.04073,0.0412 52.392395,52.39156 95.126951,95.12504 l 102.11719,0 c 10.99514,0 20.36483,-6.9593 23.94531,-16.7129 C 197.53224,958.55338 85.073657,846.08936 84.9375,845.9556 Z" + style="opacity:0.70099996;fill:url(#linearGradient4481);fill-opacity:1" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffc107;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 68.204082,915.51889 a 9.5510204,9.5510204 0 0 0 9.55102,-9.55102 c 0,-5.30081 -4.297959,-9.55102 -9.55102,-9.55102 a 9.5510204,9.5510204 0 0 0 -9.551021,9.55102 9.5510204,9.5510204 0 0 0 9.551021,9.55102 M 96.85714,872.5393 a 9.5510204,9.5510204 0 0 1 9.55102,9.55102 l 0,47.7551 a 9.5510204,9.5510204 0 0 1 -9.55102,9.55102 l -57.30612,0 A 9.5510204,9.5510204 0 0 1 30,929.84542 l 0,-47.7551 c 0,-5.30081 4.297959,-9.55102 9.55102,-9.55102 l 4.775511,0 0,-9.55102 a 23.877551,23.877551 0 0 1 23.877551,-23.87755 23.877551,23.877551 0 0 1 23.877551,23.87755 l 0,9.55102 4.775507,0 M 68.204082,848.66175 a 14.326531,14.326531 0 0 0 -14.326531,14.32653 l 0,9.55102 28.653061,0 0,-9.55102 a 14.326531,14.326531 0 0 0 -14.32653,-14.32653 z" + id="path4160" /> + </g> + <text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:298.45654297px;line-height:125%;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter4346)" + x="64.155144" + y="851.16418" + id="text4230" + sodipodi:linespacing="125%" + transform="matrix(0.17578125,0,0,0.17578125,-0.50223215,842.72639)"><tspan + sodipodi:role="line" + id="tspan4232" + x="64.155144" + y="851.16418">Abit</tspan></text> + <g + id="g4254" + transform="matrix(0.17242146,0.03420362,-0.03420362,0.17242146,-173.16554,867.44589)"> + <ellipse + ry="50" + rx="20" + cy="639.6261" + cx="1231.6552" + id="path4246" + style="opacity:1;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" /> + <path + id="rect4244" + transform="translate(0,552.36216)" + d="m 1232.3867,37.263672 0,0.0918 a 20,50 0 0 1 19.2676,49.908203 20,50 0 0 1 -19.2676,49.935545 l 0,0.0644 139.2676,0 0.7324,-0.0312 c 10.7487,-1.01692 19.2609,-23.07902 19.2676,-49.968745 -0.012,-26.879125 -8.5232,-48.924883 -19.2676,-49.955078 l -0.7324,-0.04492 -139.2676,0 z" + style="opacity:1;fill:#819ba8;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4222)" + inkscape:connector-curvature="0" /> + </g> + </g> +</svg> diff --git a/store/tv_banner.png b/store/tv_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..da0703bd653f7f3721a524d03dc245eab234b699 GIT binary patch literal 117322 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 literal 0 HcmV?d00001 diff --git a/store/tv_banner.svg b/store/tv_banner.svg new file mode 100644 index 0000000..cf9e656 --- /dev/null +++ b/store/tv_banner.svg @@ -0,0 +1,367 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1280" + height="720" + viewBox="0 0 1280 720.00001" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="tv_banner.svg" + inkscape:export-filename="C:\Users\chrig\Projects\Abit\store\tv_banner.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <defs + id="defs4"> + <linearGradient + inkscape:collect="always" + id="linearGradient4296"> + <stop + style="stop-color:#ffecb3;stop-opacity:1;" + offset="0" + id="stop4298" /> + <stop + style="stop-color:#ffc107;stop-opacity:1" + offset="1" + id="stop4300" /> + </linearGradient> + <filter + id="filter4346" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4348" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4350" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4352" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4354" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4356" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4358" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4360" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4362" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4364" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4366" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4368" /> + </filter> + <filter + id="filter4222" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4224" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4226" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4228" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4230" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4232" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4234" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4236" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4238" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4240" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4242" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4244" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4475" + id="linearGradient4481" + x1="-96.75" + y1="-92.613281" + x2="224" + y2="228.13672" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-7,822.68216)" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4475"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4477" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4479" /> + </linearGradient> + <filter + id="filter4250" + inkscape:label="z-depth2" + style="color-interpolation-filters:sRGB"> + <feFlood + id="feFlood4252" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.16" /> + <feComposite + id="feComposite4254" + result="composite1" + operator="in" + in2="SourceGraphic" + in="flood" /> + <feGaussianBlur + id="feGaussianBlur4256" + result="blur" + stdDeviation="3" /> + <feOffset + id="feOffset4258" + result="offset" + dy="3" + dx="0" /> + <feComposite + id="feComposite4260" + result="fbSourceGraphic" + operator="over" + in2="offset" + in="SourceGraphic" /> + <feColorMatrix + id="feColorMatrix4262" + values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0" + in="fbSourceGraphic" + result="fbSourceGraphicAlpha" /> + <feFlood + in="fbSourceGraphic" + result="flood" + flood-color="rgb(0,0,0)" + flood-opacity="0.23" + id="feFlood4264" /> + <feComposite + result="composite1" + operator="in" + in="flood" + in2="fbSourceGraphic" + id="feComposite4266" /> + <feGaussianBlur + result="blur" + stdDeviation="3" + id="feGaussianBlur4268" /> + <feOffset + result="offset" + dy="3" + dx="0" + id="feOffset4270" /> + <feComposite + result="composite2" + operator="over" + in="fbSourceGraphic" + in2="offset" + id="feComposite4272" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4296" + id="linearGradient4302" + x1="477.62885" + y1="551.84436" + x2="477.62885" + y2="1052.88" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.25,0,0,1.44,-3.5714286,-463.03937)" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.24748738" + inkscape:cx="285.45006" + inkscape:cy="354.31355" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1280" + inkscape:window-height="657" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Ebene 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-332.36216)"> + <rect + style="opacity:1;fill:url(#linearGradient4302);fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect3336" + width="1280" + height="720" + x="3.1578065e-008" + y="332.36215" /> + <path + style="fill:none;fill-rule:evenodd;stroke:#607d8b;stroke-width:3.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" + d="m 234.28572,856.68845 c 21.22559,1.59192 45.12779,12.2133 65.65697,30.54595 56.85434,50.7713 163.8859,-36.32334 317.58615,16.9642 50.86665,17.6354 101.47749,18.6011 139.26053,12.619 122.36869,-19.3742 50.78221,-129.85677 -49.14945,-119.58354 -180.48828,18.55465 -81.95538,130.45724 145.95862,63.71699 171.37966,-50.18523 -9.54808,-235.20057 49.25861,-189.97689" + id="path4294" + inkscape:connector-curvature="0" + sodipodi:nodetypes="csssssc" + transform="matrix(1.25,0,0,1.25,0,-198.43754)" /> + <g + transform="matrix(1.4061041,0,0,1.4061041,831.93832,-725.55688)" + id="g4508" + inkscape:export-filename="C:\Users\chrig\Abit2.png" + inkscape:export-xdpi="602.35297" + inkscape:export-ydpi="602.35297"> + <path + sodipodi:nodetypes="sssssssss" + style="fill:#819ba8;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 230.00001,827.68211 -204.000009,0 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,152.99999 c -5.523e-5,14.0833 11.4167,25.5001 25.5,25.5001 l 204.000009,0 c 14.0833,0 25.50005,-11.4168 25.49999,-25.5001 l 0,-152.99999 c 0,-14.1525 -11.47499,-25.5 -25.49999,-25.5 z" + id="path4226" /> + <path + id="path4435" + d="m 26,827.68216 c -14.1525,0 -25.5,11.3475 -25.5,25.5 l 0,1.21289 127.5,79.6875 127.5,-79.6875 0,-1.21289 c 0,-14.1525 -11.475,-25.5 -25.5,-25.5 l -204,0 z" + style="fill:#607d8b;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" /> + <path + id="path4456" + d="M 84.9375,845.9556 C 99.46494,905.00376 49.357218,869.16386 30,929.84622 c 0,2.61651 1.051586,4.98591 2.755859,6.71094 0.04073,0.0412 52.392395,52.39156 95.126951,95.12504 l 102.11719,0 c 10.99514,0 20.36483,-6.9593 23.94531,-16.7129 C 197.53224,958.55338 85.073657,846.08936 84.9375,845.9556 Z" + style="opacity:0.70099996;fill:url(#linearGradient4481);fill-opacity:1" + inkscape:connector-curvature="0" /> + <path + style="fill:#ffc107;fill-opacity:1;filter:url(#filter4346)" + inkscape:connector-curvature="0" + d="m 68.204082,915.51889 a 9.5510204,9.5510204 0 0 0 9.55102,-9.55102 c 0,-5.30081 -4.297959,-9.55102 -9.55102,-9.55102 a 9.5510204,9.5510204 0 0 0 -9.551021,9.55102 9.5510204,9.5510204 0 0 0 9.551021,9.55102 M 96.85714,872.5393 a 9.5510204,9.5510204 0 0 1 9.55102,9.55102 l 0,47.7551 a 9.5510204,9.5510204 0 0 1 -9.55102,9.55102 l -57.30612,0 A 9.5510204,9.5510204 0 0 1 30,929.84542 l 0,-47.7551 c 0,-5.30081 4.297959,-9.55102 9.55102,-9.55102 l 4.775511,0 0,-9.55102 a 23.877551,23.877551 0 0 1 23.877551,-23.87755 23.877551,23.877551 0 0 1 23.877551,23.87755 l 0,9.55102 4.775507,0 M 68.204082,848.66175 a 14.326531,14.326531 0 0 0 -14.326531,14.32653 l 0,9.55102 28.653061,0 0,-9.55102 a 14.326531,14.326531 0 0 0 -14.32653,-14.32653 z" + id="path4160" /> + </g> + <text + xml:space="preserve" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:298.45654297px;line-height:125%;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter4346)" + x="61.298" + y="737.77618" + id="text4230" + sodipodi:linespacing="125%" + transform="matrix(1.25,0,0,1.25,0,-200.43754)"><tspan + sodipodi:role="line" + id="tspan4232" + x="61.298" + y="737.77618">Abit</tspan></text> + <g + id="g4254" + transform="matrix(1.2261082,0.24322571,-0.24322571,1.2261082,-1231.3994,-249.72278)"> + <ellipse + ry="50" + rx="20" + cy="639.6261" + cx="1231.6552" + id="path4246" + style="opacity:1;fill:#607d8b;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4346)" /> + <path + id="rect4244" + transform="translate(0,552.36216)" + d="m 1232.3867,37.263672 0,0.0918 a 20,50 0 0 1 19.2676,49.908203 20,50 0 0 1 -19.2676,49.935545 l 0,0.0644 139.2676,0 0.7324,-0.0312 c 10.7487,-1.01692 19.2609,-23.07902 19.2676,-49.968745 -0.012,-26.879125 -8.5232,-48.924883 -19.2676,-49.955078 l -0.7324,-0.04492 -139.2676,0 z" + style="opacity:1;fill:#819ba8;fill-opacity:1;stroke:none;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4222)" + inkscape:connector-curvature="0" /> + </g> + </g> +</svg> From c3eef5d711e6b9c28feea8e674a1f28382ac9f3b Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 2 Sep 2016 17:18:04 +0200 Subject: [PATCH 052/110] Fixed icon color for pre-lollipop phones --- app/src/main/res/drawable/ic_notification_error.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/drawable/ic_notification_error.xml b/app/src/main/res/drawable/ic_notification_error.xml index bc132bb..c4ac5ff 100644 --- a/app/src/main/res/drawable/ic_notification_error.xml +++ b/app/src/main/res/drawable/ic_notification_error.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-2h2v2zm0,-4h-2V7h2v6z"/> </vector> From da73388d8cc1678fc97b9f4ab4e09fc2dc2b5aaf Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 2 Sep 2016 17:29:53 +0200 Subject: [PATCH 053/110] Added .editorconfig, mainly to enforce Unix LF line breaks --- .editorconfig | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..656384c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 From dd9539aa3fc6727943f2e490c49f8b37d1c0bd67 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 12 Sep 2016 09:55:48 +0200 Subject: [PATCH 054/110] Added UI for chans --- .../ch/dissem/apps/abit/MainActivity.java | 178 +++++++++++++----- .../res/layout/dialog_input_passphrase.xml | 14 ++ app/src/main/res/values-de/strings.xml | 12 +- app/src/main/res/values/strings.xml | 6 + 4 files changed, 156 insertions(+), 54 deletions(-) create mode 100644 app/src/main/res/layout/dialog_input_passphrase.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 1baf37e..b1e9ded 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; @@ -23,6 +24,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Point; +import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.Fragment; @@ -33,6 +35,8 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.CompoundButton; import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.targets.Target; @@ -72,6 +76,7 @@ import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.Label; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -102,6 +107,7 @@ public class MainActivity extends AppCompatActivity private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final int ADD_IDENTITY = 1; private static final int MANAGE_IDENTITY = 2; + private static final int ADD_CHAN = 3; public static WeakReference<MainActivity> instance; @@ -230,36 +236,45 @@ public class MainActivity extends AppCompatActivity for (BitmessageAddress identity : bmc.addresses().getIdentities()) { LOG.info("Adding identity " + identity.getAddress()); profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withNameShown(true) - .withEmail(identity.getAddress()) - .withTag(identity) + .withIcon(new Identicon(identity)) + .withName(identity.toString()) + .withNameShown(true) + .withEmail(identity.getAddress()) + .withTag(identity) ); } if (profiles.isEmpty()) { // Create an initial identity BitmessageAddress identity = Singleton.getIdentity(this); profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity) + .withIcon(new Identicon(identity)) + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity) ); } profiles.add(new ProfileSettingDrawerItem() - .withName("Add Identity") - .withDescription("Create new identity") - .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) - .actionBar() - .paddingDp(5) - .colorRes(R.color.icons)) - .withIdentifier(ADD_IDENTITY) + .withName(getString(R.string.add_identity)) + .withDescription(getString(R.string.add_identity_summary)) + .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) + .actionBar() + .paddingDp(5) + .colorRes(R.color.icons)) + .withIdentifier(ADD_IDENTITY) ); profiles.add(new ProfileSettingDrawerItem() - .withName(getString(R.string.manage_identity)) - .withIcon(GoogleMaterial.Icon.gmd_settings) - .withIdentifier(MANAGE_IDENTITY) + .withName(getString(R.string.add_chan)) + .withDescription(getString(R.string.add_chan_summary)) + .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) + .actionBar() + .paddingDp(5) + .colorRes(R.color.icons)) + .withIdentifier(ADD_CHAN) + ); + profiles.add(new ProfileSettingDrawerItem() + .withName(getString(R.string.manage_identity)) + .withIcon(GoogleMaterial.Icon.gmd_settings) + .withIdentifier(MANAGE_IDENTITY) ); // Create the AccountHeader accountHeader = new AccountHeaderBuilder() @@ -272,35 +287,10 @@ public class MainActivity extends AppCompatActivity currentProfile) { switch (profile.getIdentifier()) { case ADD_IDENTITY: - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.add_identity_warning) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - BitmessageAddress identity = bmc - .createIdentity(false); - IProfile newProfile = new - ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); - if (accountHeader.getProfiles() != null) { - // we know that there are 2 setting - // elements. - // Set the new profile above them ;) - accountHeader.addProfile( - newProfile, accountHeader - .getProfiles().size() - - 2); - } else { - accountHeader.addProfiles(newProfile); - } - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + addIdentityDialog(); + break; + case ADD_CHAN: + addChanDialog(); break; case MANAGE_IDENTITY: Intent show = new Intent(MainActivity.this, @@ -359,9 +349,9 @@ public class MainActivity extends AppCompatActivity drawerItems.add(item); } drawerItems.add(new PrimaryDrawerItem() - .withName(R.string.archive) - .withTag(null) - .withIcon(CommunityMaterial.Icon.cmd_archive) + .withName(R.string.archive) + .withTag(null) + .withIcon(CommunityMaterial.Icon.cmd_archive) ); drawerItems.add(new DividerDrawerItem()); drawerItems.add(new PrimaryDrawerItem() @@ -434,6 +424,73 @@ public class MainActivity extends AppCompatActivity .build(); } + private void addIdentityDialog() { + new AlertDialog.Builder(MainActivity.this) + .setMessage(R.string.add_identity_warning) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + Toast.makeText(MainActivity.this, + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); + } + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(MainActivity.this, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + addIdentityEntry(chan); + } + }.execute(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + private void addChanDialog() { + @SuppressLint("InflateParams") + final View dialogView = getLayoutInflater().inflate(R.layout.dialog_input_passphrase, null); + new AlertDialog.Builder(MainActivity.this) + .setMessage(R.string.add_chan) + .setView(dialogView) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + Toast.makeText(MainActivity.this, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<String, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(String... args) { + String pass = args[0]; + BitmessageAddress chan = bmc.createChan(pass); + chan.setAlias(pass); + bmc.addresses().save(chan); + return chan; + } + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(MainActivity.this, + R.string.toast_chan_created, + Toast.LENGTH_SHORT).show(); + addIdentityEntry(chan); + } + }.execute(passphrase.getText().toString()); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + @Override protected void onResume() { instance = new WeakReference<>(this); @@ -441,6 +498,25 @@ public class MainActivity extends AppCompatActivity super.onResume(); } + private void addIdentityEntry(BitmessageAddress identity) { + IProfile newProfile = new + ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); + if (accountHeader.getProfiles() != null) { + // we know that there are 3 setting + // elements. + // Set the new profile above them ;) + accountHeader.addProfile( + newProfile, accountHeader + .getProfiles().size() + - 3); + } else { + accountHeader.addProfiles(newProfile); + } + } + @Override protected void onPause() { super.onPause(); diff --git a/app/src/main/res/layout/dialog_input_passphrase.xml b/app/src/main/res/layout/dialog_input_passphrase.xml new file mode 100644 index 0000000..db9c2ae --- /dev/null +++ b/app/src/main/res/layout/dialog_input_passphrase.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <EditText + android:id="@+id/passphrase" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:hint="@string/passphrase" + android:inputType="textMultiLine"/> +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 95d73b8..9068551 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -13,6 +13,8 @@ <string name="action_settings">Einstellungen</string> <string name="add_identity">Identität hinzufügen</string> <string name="add_identity_summary">Eine neue Identität erstellen</string> + <string name="add_chan">Chan hinzufügen</string> + <string name="add_chan_summary">Einen Chan hinzufügen oder erstellen</string> <string name="broadcast">Broadcast</string> <string name="cancel">Abbrechen</string> <string name="do_import">Importieren</string> @@ -60,10 +62,10 @@ <string name="pubkey_available">Öffentlicher Schlüssel verfügbar</string> <string name="pubkey_not_available">Öffentlicher Schlüssel noch nicht verfügbar</string> <string name="alt_qr_code">QR-Code</string> - <string name="add_identity_warning">Mehrere Identitäten zu haben bedeutet einen höheren Resourcenverbrauch. Sind Sie sicher?</string> + <string name="add_identity_warning">Mehrere Identitäten zu haben bedeutet einen höheren Resourcenverbrauch. Bist Du sicher?</string> <string name="share">Teilen</string> - <string name="delete_contact_warning">Sind Sie sicher dass dieser Kontakt gelöscht werden soll?</string> - <string name="delete_identity_warning">Sind Sie sicher dass diese Identität gelöscht werden soll? Sie werden keine Nachrichten mehr empfangen können welche an diese Adresse gesendet werden, und es est nicht möglich diese Aktion rückgängig zu machen.</string> + <string name="delete_contact_warning">Bist Du sicher dass dieser Kontakt gelöscht werden soll?</string> + <string name="delete_identity_warning">Bist Du sicher dass diese Identität gelöscht werden soll? Nachrichten, welche an diese Adresse gesendet werden, können nicht mehr empfangen werden und es est nicht möglich diese Aktion rückgängig zu machen.</string> <string name="create_contact">Kontakt erfassen</string> <string name="scan_qr_code">QR-Code scannen</string> <string name="full_node_description">Solange kein aktiver Knoten gestartet ist, werden keine Meldungen empfangen oder gesendet. Dies braucht jedoch viele Resourcen und Daten. @@ -75,4 +77,8 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="export">Exportieren</string> <string name="confirm_export">Identität wirklich exportieren? Der Export wird die privaten Schlüssel unverschlüsselt enthalten.</string> <string name="compose_message">Schreiben</string> + <string name="passphrase">Passphrase</string> + <string name="toast_chan_created">Chan erstellt</string> + <string name="toast_long_running_operation">Dies kann einige Minuten dauern</string> + <string name="toast_identity_created">Identität erstellt</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49e4b52..6dc6dca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,8 @@ <string name="manage_identity">Manage Identity</string> <string name="add_identity">Add Identity</string> <string name="add_identity_summary">Create new identity</string> + <string name="add_chan">Add Chan</string> + <string name="add_chan_summary">Add or create a chan</string> <string name="title_activity_open_bitmessage_link">Import Contact</string> <string name="action_settings">Settings</string> @@ -78,4 +80,8 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="export">Export</string> <string name="confirm_export">Do you really want to export your identity? The export will contain the unencrypted private keys.</string> <string name="compose_message">Compose</string> + <string name="passphrase">passphrase</string> + <string name="toast_long_running_operation">This may take a few minutes</string> + <string name="toast_identity_created">Identity created</string> + <string name="toast_chan_created">Chan created</string> </resources> From af2bfc796b76ab594532a386c77f6397f52899a2 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 12 Sep 2016 11:00:00 +0200 Subject: [PATCH 055/110] Use the nio network listener. --- app/build.gradle | 10 +- .../db/migration/V3.3__Create_table_node.sql | 9 + .../apps/abit/ComposeMessageActivity.java | 1 + .../apps/abit/ComposeMessageFragment.java | 22 ++- .../apps/abit/MessageDetailFragment.java | 22 ++- .../repository/AndroidAddressRepository.java | 40 ++-- .../abit/repository/AndroidInventory.java | 47 ++--- .../repository/AndroidMessageRepository.java | 108 +++++----- .../abit/repository/AndroidNodeRegistry.java | 187 ++++++++++++++++++ .../AndroidProofOfWorkRepository.java | 69 ++++--- .../apps/abit/repository/SqlHelper.java | 10 +- .../dissem/apps/abit/service/Singleton.java | 11 +- build.gradle | 2 +- 13 files changed, 382 insertions(+), 156 deletions(-) create mode 100644 app/src/main/assets/db/migration/V3.3__Create_table_node.sql create mode 100644 app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java diff --git a/app/build.gradle b/app/build.gradle index e600ee0..3ba3317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ if (project.hasProperty("project.configs") android { compileSdkVersion 24 - buildToolsVersion "24.0.1" + buildToolsVersion "24.0.2" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() @@ -29,12 +29,12 @@ android { } } -ext.jabitVersion = 'develop-SNAPSHOT' +ext.jabitVersion = 'feature-nio-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:24.1.1' - compile 'com.android.support:support-v4:24.1.1' - compile 'com.android.support:design:24.1.1' + compile 'com.android.support:appcompat-v7:24.2.0' + compile 'com.android.support:support-v4:24.2.0' + compile 'com.android.support:design:24.2.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" diff --git a/app/src/main/assets/db/migration/V3.3__Create_table_node.sql b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql new file mode 100644 index 0000000..5d03bb5 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql @@ -0,0 +1,9 @@ +CREATE TABLE Node ( + stream BIGINT NOT NULL, + address BINARY(32) NOT NULL, + port INT NOT NULL, + services BIGINT NOT NULL, + time BIGINT NOT NULL, + PRIMARY KEY (stream, address, port) +); +CREATE INDEX idx_time on Node(time); diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 5fa12a4..c5f2032 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -27,6 +27,7 @@ public class ComposeMessageActivity extends AppCompatActivity { public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"; public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"; public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; + public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index b19ef4d..5d80e31 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.text.Selection; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -34,6 +35,7 @@ import ch.dissem.apps.abit.adapter.ContactAdapter; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; @@ -45,6 +47,7 @@ public class ComposeMessageFragment extends Fragment { private BitmessageAddress identity; private BitmessageAddress recipient; private String subject; + private String content; private AutoCompleteTextView recipientInput; private EditText subjectInput; private EditText bodyInput; @@ -71,6 +74,9 @@ public class ComposeMessageFragment extends Fragment { if (getArguments().containsKey(EXTRA_SUBJECT)) { subject = getArguments().getString(EXTRA_SUBJECT); } + if (getArguments().containsKey(EXTRA_CONTENT)) { + content = getArguments().getString(EXTRA_CONTENT); + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -106,6 +112,16 @@ public class ComposeMessageFragment extends Fragment { subjectInput = (EditText) rootView.findViewById(R.id.subject); subjectInput.setText(subject); bodyInput = (EditText) rootView.findViewById(R.id.body); + bodyInput.setText(content); + + if (recipient == null) { + recipientInput.requestFocus(); + } else if (subject == null || subject.isEmpty()) { + subjectInput.requestFocus(); + } else { + bodyInput.requestFocus(); + bodyInput.setSelection(0); + } return rootView; } @@ -126,7 +142,7 @@ public class ComposeMessageFragment extends Fragment { recipient = new BitmessageAddress(inputString); } catch (Exception e) { List<BitmessageAddress> contacts = Singleton.getAddressRepository - (getContext()).getContacts(); + (getContext()).getContacts(); for (BitmessageAddress contact : contacts) { if (inputString.equalsIgnoreCase(contact.getAlias())) { recipient = contact; @@ -137,8 +153,8 @@ public class ComposeMessageFragment extends Fragment { } } Singleton.getBitmessageContext(getContext()).send(identity, recipient, - subjectInput.getText().toString(), - bodyInput.getText().toString()); + subjectInput.getText().toString(), + bodyInput.getText().toString()); getActivity().finish(); return true; default: diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index b67cee5..6afb2d0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -44,6 +44,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; @@ -100,7 +101,7 @@ public class MessageDetailFragment extends Fragment { ((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); BitmessageAddress sender = item.getFrom(); ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon - (sender)); + (sender)); ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString()); if (item.getTo() != null) { ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); @@ -112,11 +113,11 @@ public class MessageDetailFragment extends Fragment { Linkify.addLinks(messageBody, WEB_URLS); Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, - new TransformFilter() { - public final String transformUrl(final Matcher match, String url) { - return match.group(); - } - }); + new TransformFilter() { + public final String transformUrl(final Matcher match, String url) { + return match.group(); + } + }); messageBody.setLinksClickable(true); messageBody.setTextIsSelectable(true); @@ -146,7 +147,7 @@ public class MessageDetailFragment extends Fragment { Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply); Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon - .gmd_markunread); + .gmd_markunread); Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive); super.onCreateOptionsMenu(menu, inflater); @@ -158,17 +159,20 @@ public class MessageDetailFragment extends Fragment { switch (menuItem.getItemId()) { case R.id.reply: Intent replyIntent = new Intent(getActivity().getApplicationContext(), - ComposeMessageActivity.class); + ComposeMessageActivity.class); replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) - .equalsIgnoreCase("RE:")) { + .equalsIgnoreCase("RE:")) { prefix = ""; } else { prefix = "RE: "; } replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); + replyIntent.putExtra(EXTRA_CONTENT, + "\n\n------------------------------------------------------\n" + + item.getText()); startActivity(replyIntent); return true; case R.id.delete: diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 25c9b2a..4eb3374 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -119,37 +119,36 @@ public class AndroidAddressRepository implements AddressRepository { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_ADDRESS, - COLUMN_ALIAS, - COLUMN_PUBLIC_KEY, - COLUMN_PRIVATE_KEY, - COLUMN_SUBSCRIBED, - COLUMN_CHAN + COLUMN_ADDRESS, + COLUMN_ALIAS, + COLUMN_PUBLIC_KEY, + COLUMN_PRIVATE_KEY, + COLUMN_SUBSCRIBED, + COLUMN_CHAN }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, null + TABLE_NAME, projection, + where, + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { BitmessageAddress address; byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)); if (privateKeyBytes != null) { PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream - (privateKeyBytes)); + (privateKeyBytes)); address = new BitmessageAddress(privateKey); } else { address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))); byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY)); if (publicKeyBytes != null) { Pubkey pubkey = Factory.readPubkey(address.getVersion(), address - .getStream(), - new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, - false); + .getStream(), + new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, + false); if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { pubkey = new V4Pubkey((V3Pubkey) pubkey); } @@ -161,7 +160,6 @@ public class AndroidAddressRepository implements AddressRepository { address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1); result.add(address); - c.moveToNext(); } } catch (IOException e) { LOG.error(e.getMessage(), e); @@ -184,8 +182,10 @@ public class AndroidAddressRepository implements AddressRepository { private boolean exists(BitmessageAddress address) { SQLiteDatabase db = sql.getReadableDatabase(); - try (Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address - .getAddress() + "'", null)) { + try (Cursor cursor = db.rawQuery( + "SELECT COUNT(*) FROM Address WHERE address=?", + new String[]{address.getAddress()} + )) { cursor.moveToFirst(); return cursor.getInt(0) > 0; } @@ -210,8 +210,8 @@ public class AndroidAddressRepository implements AddressRepository { values.put(COLUMN_CHAN, address.isChan()); values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); - int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + - "'", null); + int update = db.update(TABLE_NAME, values, "address=?", + new String[]{address.getAddress()}); if (update < 0) { LOG.error("Could not update address " + address); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java index c88ca2a..8632679 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidInventory.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -42,6 +41,7 @@ import ch.dissem.bitmessage.utils.Encode; import static ch.dissem.apps.abit.repository.SqlHelper.join; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; +import static java.lang.String.valueOf; /** * {@link Inventory} implementation using the Android SQL API. @@ -88,21 +88,19 @@ public class AndroidInventory implements Inventory { cache.put(stream, result); String[] projection = { - COLUMN_HASH, COLUMN_EXPIRES + COLUMN_HASH, COLUMN_EXPIRES }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "stream = " + stream, - null, null, null, null + TABLE_NAME, projection, + "stream = " + stream, + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES)); result.put(new InventoryVector(blob), expires); - c.moveToNext(); } } LOG.info("Stream #" + stream + " inventory size: " + result.size()); @@ -126,18 +124,17 @@ public class AndroidInventory implements Inventory { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_VERSION, - COLUMN_DATA + COLUMN_VERSION, + COLUMN_DATA }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "hash = X'" + vector + "'", - null, null, null, null + TABLE_NAME, projection, + "hash = X'" + vector + "'", + null, null, null, null )) { - c.moveToFirst(); - if (c.isAfterLast()) { + if (!c.moveToFirst()) { LOG.info("Object requested that we don't have. IV: " + vector); return null; } @@ -153,8 +150,8 @@ public class AndroidInventory implements Inventory { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_VERSION, - COLUMN_DATA + COLUMN_VERSION, + COLUMN_DATA }; StringBuilder where = new StringBuilder("1=1"); if (stream > 0) { @@ -170,17 +167,15 @@ public class AndroidInventory implements Inventory { SQLiteDatabase db = sql.getReadableDatabase(); List<ObjectMessage> result = new LinkedList<>(); try (Cursor c = db.query( - TABLE_NAME, projection, - where.toString(), - null, null, null, null + TABLE_NAME, projection, + where.toString(), + null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), - blob.length)); - c.moveToNext(); + blob.length)); } } return result; @@ -211,8 +206,6 @@ public class AndroidInventory implements Inventory { getCache(object.getStream()).put(iv, object.getExpiresTime()); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); - } catch (IOException e) { - LOG.error(e.getMessage(), e); } } @@ -225,7 +218,7 @@ public class AndroidInventory implements Inventory { public void cleanup() { long fiveMinutesAgo = now() - 5 * MINUTE; SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, "expires < " + fiveMinutesAgo, null); + db.delete(TABLE_NAME, "expires < ?", new String[]{valueOf(fiveMinutesAgo)}); for (Map<InventoryVector, Long> c : cache.values()) { Iterator<Map.Entry<InventoryVector, Long>> iterator = c.entrySet().iterator(); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 5a1765e..f430f66 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -41,6 +41,8 @@ import ch.dissem.bitmessage.ports.AbstractMessageRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; +import static java.lang.String.valueOf; + /** * {@link MessageRepository} implementation using the Android SQL API. */ @@ -87,23 +89,21 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - LBL_COLUMN_ID, - LBL_COLUMN_LABEL, - LBL_COLUMN_TYPE, - LBL_COLUMN_COLOR + LBL_COLUMN_ID, + LBL_COLUMN_LABEL, + LBL_COLUMN_TYPE, + LBL_COLUMN_COLOR }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - LBL_TABLE_NAME, projection, - where, - null, null, null, - LBL_COLUMN_ORDER + LBL_TABLE_NAME, projection, + where, + null, null, null, + LBL_COLUMN_ORDER )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { result.add(getLabel(c)); - c.moveToNext(); } } return result; @@ -140,26 +140,34 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } } Label label = new Label( - text, - type, - c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR))); + text, + type, + c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR))); label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID))); return label; } @Override public int countUnread(Label label) { + String[] args; String where; if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() - + ") AND "; + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND "; + args = new String[]{ + label.getId().toString(), + Label.Type.UNREAD.name() + }; } else { where = ""; + args = new String[]{ + Label.Type.UNREAD.name() + }; } SQLiteDatabase db = sql.getReadableDatabase(); return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME, - where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + - "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))" + where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + + "SELECT id FROM Label WHERE type=?))", + args ); } @@ -169,48 +177,47 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_ID, - COLUMN_IV, - COLUMN_TYPE, - COLUMN_SENDER, - COLUMN_RECIPIENT, - COLUMN_DATA, - COLUMN_ACK_DATA, - COLUMN_SENT, - COLUMN_RECEIVED, - COLUMN_STATUS, - COLUMN_TTL, - COLUMN_RETRIES, - COLUMN_NEXT_TRY + COLUMN_ID, + COLUMN_IV, + COLUMN_TYPE, + COLUMN_SENDER, + COLUMN_RECIPIENT, + COLUMN_DATA, + COLUMN_ACK_DATA, + COLUMN_SENT, + COLUMN_RECEIVED, + COLUMN_STATUS, + COLUMN_TTL, + COLUMN_RETRIES, + COLUMN_NEXT_TRY }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - where, - null, null, null, - COLUMN_RECEIVED + " DESC" + TABLE_NAME, projection, + where, + null, null, null, + COLUMN_RECEIVED + " DESC" )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV)); byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA)); Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex - (COLUMN_TYPE))); + (COLUMN_TYPE))); Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new - ByteArrayInputStream(data)); + ByteArrayInputStream(data)); long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex - (COLUMN_SENDER)))); + (COLUMN_SENDER)))); builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex - (COLUMN_RECIPIENT)))); + (COLUMN_RECIPIENT)))); builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))); builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex - (COLUMN_STATUS)))); + (COLUMN_STATUS)))); builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL))); builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES))); int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY); @@ -219,7 +226,6 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } builder.labels(findLabels(id)); result.add(builder.build()); - c.moveToNext(); } } catch (IOException e) { LOG.error(e.getMessage(), e); @@ -240,7 +246,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // save from address if necessary if (message.getId() == null) { BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message - .getFrom().getAddress()); + .getFrom().getAddress()); if (savedAddress == null || savedAddress.getPrivateKey() == null) { if (savedAddress != null && savedAddress.getAlias() != null) { message.getFrom().setAlias(savedAddress.getAlias()); @@ -257,7 +263,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { } // remove existing labels - db.delete(JOIN_TABLE_NAME, "message_id=" + message.getId(), null); + db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())}); // save labels ContentValues values = new ContentValues(); @@ -279,15 +285,19 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private void insert(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message - .getInventoryVector().getHash()); + .getInventoryVector().getHash()); values.put(COLUMN_TYPE, message.getType().name()); values.put(COLUMN_SENDER, message.getFrom().getAddress()); values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); values.put(COLUMN_DATA, Encode.bytes(message)); + values.put(COLUMN_ACK_DATA, message.getAckData()); values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); + values.put(COLUMN_TTL, message.getTTL()); + values.put(COLUMN_RETRIES, message.getRetries()); + values.put(COLUMN_NEXT_TRY, message.getNextTry()); long id = db.insertOrThrow(TABLE_NAME, null, values); message.setId(id); } @@ -295,15 +305,19 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private void update(SQLiteDatabase db, Plaintext message) throws IOException { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message - .getInventoryVector().getHash()); + .getInventoryVector().getHash()); values.put(COLUMN_TYPE, message.getType().name()); values.put(COLUMN_SENDER, message.getFrom().getAddress()); values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); values.put(COLUMN_DATA, Encode.bytes(message)); + values.put(COLUMN_ACK_DATA, message.getAckData()); values.put(COLUMN_SENT, message.getSent()); values.put(COLUMN_RECEIVED, message.getReceived()); values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); + values.put(COLUMN_TTL, message.getTTL()); + values.put(COLUMN_RETRIES, message.getRetries()); + values.put(COLUMN_NEXT_TRY, message.getNextTry()); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java new file mode 100644 index 0000000..b20c173 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java @@ -0,0 +1,187 @@ +package ch.dissem.apps.abit.repository; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteStatement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.ports.NodeRegistry; +import ch.dissem.bitmessage.utils.Collections; +import ch.dissem.bitmessage.utils.SqlStrings; + +import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes; +import static ch.dissem.bitmessage.utils.Strings.hex; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static ch.dissem.bitmessage.utils.UnixTime.now; +import static java.lang.String.valueOf; + +/** + * @author Christian Basler + */ +public class AndroidNodeRegistry implements NodeRegistry { + private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class); + + private static final String TABLE_NAME = "Node"; + private static final String COLUMN_STREAM = "stream"; + private static final String COLUMN_ADDRESS = "address"; + private static final String COLUMN_PORT = "port"; + private static final String COLUMN_SERVICES = "services"; + private static final String COLUMN_TIME = "time"; + + private final ThreadLocal<SQLiteStatement> loadExistingStatement = new ThreadLocal<>(); + + private final SqlHelper sql; + private Map<Long, Set<NetworkAddress>> stableNodes; + + public AndroidNodeRegistry(SqlHelper sql) { + this.sql = sql; + cleanUp(); + } + + private void cleanUp() { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))}); + } + + private Long loadExistingTime(NetworkAddress node) { + SQLiteStatement statement = loadExistingStatement.get(); + if (statement == null) { + statement = sql.getWritableDatabase().compileStatement( + "SELECT " + COLUMN_TIME + + " FROM " + TABLE_NAME + + " WHERE stream=? AND address=? AND port=?" + ); + loadExistingStatement.set(statement); + } + statement.bindLong(1, node.getStream()); + statement.bindBlob(2, node.getIPv6()); + statement.bindLong(3, node.getPort()); + try { + return statement.simpleQueryForLong(); + } catch (SQLiteDoneException e) { + return null; + } + } + + @Override + public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { + String[] projection = { + COLUMN_STREAM, + COLUMN_ADDRESS, + COLUMN_PORT, + COLUMN_SERVICES, + COLUMN_TIME + }; + + List<NetworkAddress> result = new LinkedList<>(); + SQLiteDatabase db = sql.getReadableDatabase(); + try (Cursor c = db.query( + TABLE_NAME, projection, + "stream IN (?)", + new String[]{SqlStrings.join(streams).toString()}, + null, null, + "time DESC", + valueOf(limit) + )) { + while (c.moveToNext()) { + result.add( + new NetworkAddress.Builder() + .stream(c.getLong(c.getColumnIndex(COLUMN_STREAM))) + .ipv6(c.getBlob(c.getColumnIndex(COLUMN_ADDRESS))) + .port(c.getInt(c.getColumnIndex(COLUMN_PORT))) + .services(c.getLong(c.getColumnIndex(COLUMN_SERVICES))) + .time(c.getLong(c.getColumnIndex(COLUMN_TIME))) + .build() + ); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + if (result.isEmpty()) { + synchronized (this) { + if (stableNodes == null) { + stableNodes = loadStableNodes(); + } + } + for (long stream : streams) { + Set<NetworkAddress> nodes = stableNodes.get(stream); + if (nodes != null && !nodes.isEmpty()) { + result.add(Collections.selectRandom(nodes)); + } + } + } + return result; + } + + @Override + public void offerAddresses(List<NetworkAddress> nodes) { + SQLiteDatabase db = sql.getWritableDatabase(); + db.beginTransaction(); + try { + cleanUp(); + for (NetworkAddress node : nodes) { + if (node.getTime() < now(+5 * MINUTE) && node.getTime() > now(-28 * DAY)) { + synchronized (this) { + Long existing = loadExistingTime(node); + if (existing == null) { + insert(node); + } else if (node.getTime() > existing) { + update(node); + } + } + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void insert(NetworkAddress node) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_STREAM, node.getStream()); + values.put(COLUMN_ADDRESS, node.getIPv6()); + values.put(COLUMN_PORT, node.getPort()); + values.put(COLUMN_SERVICES, node.getServices()); + values.put(COLUMN_TIME, node.getTime()); + + db.insertOrThrow(TABLE_NAME, null, values); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } + } + + private void update(NetworkAddress node) { + try { + SQLiteDatabase db = sql.getWritableDatabase(); + // Create a new map of values, where column names are the keys + ContentValues values = new ContentValues(); + values.put(COLUMN_SERVICES, node.getServices()); + values.put(COLUMN_TIME, node.getTime()); + + db.update(TABLE_NAME, values, + "stream=" + node.getStream() + " AND address=X'" + hex(node.getIPv6()) + "' AND " + + "port=" + node.getPort(), + null); + } catch (SQLiteConstraintException e) { + LOG.trace(e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index 5fccc5d..d27a821 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -37,12 +36,13 @@ import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Strings; import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static ch.dissem.bitmessage.utils.Strings.hex; /** * @author Christian Basler */ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext - .ContextHolder { + .ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); private static final String TABLE_NAME = "POW"; @@ -71,46 +71,45 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_DATA, - COLUMN_VERSION, - COLUMN_NONCE_TRIALS_PER_BYTE, - COLUMN_EXTRA_BYTES, - COLUMN_EXPIRATION_TIME, - COLUMN_MESSAGE_ID + COLUMN_DATA, + COLUMN_VERSION, + COLUMN_NONCE_TRIALS_PER_BYTE, + COLUMN_EXTRA_BYTES, + COLUMN_EXPIRATION_TIME, + COLUMN_MESSAGE_ID }; SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query( - TABLE_NAME, projection, - "initial_hash = X'" + Strings.hex(initialHash) + "'", - null, null, null, null + TABLE_NAME, projection, + "initial_hash=X'" + hex(initialHash) + "'", + null, null, null, null )) { - c.moveToFirst(); - if (!c.isAfterLast()) { + if (c.moveToFirst()) { int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) { return new Item( - Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob - .length), - c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), - c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) ); } else { return new Item( - Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob - .length), - c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), - c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), - c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), - bmc.getMessageRepository().getMessage( - c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) + Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob + .length), + c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), + c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), + c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), + bmc.getMessageRepository().getMessage( + c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) ); } } } throw new RuntimeException("Object requested that we don't have. Initial hash: " + - Strings.hex(initialHash)); + hex(initialHash)); } @Override @@ -118,20 +117,18 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { - COLUMN_INITIAL_HASH + COLUMN_INITIAL_HASH }; SQLiteDatabase db = sql.getReadableDatabase(); List<byte[]> result = new LinkedList<>(); try (Cursor c = db.query( - TABLE_NAME, projection, - null, null, null, null, null + TABLE_NAME, projection, + null, null, null, null, null )) { - c.moveToFirst(); - while (!c.isAfterLast()) { + while (c.moveToNext()) { byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); result.add(initialHash); - c.moveToNext(); } } return result; @@ -156,8 +153,6 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte db.insertOrThrow(TABLE_NAME, null, values); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); - } catch (IOException e) { - LOG.error(e.getMessage(), e); } } @@ -169,8 +164,10 @@ public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, Inte @Override public void removeObject(byte[] initialHash) { SQLiteDatabase db = sql.getWritableDatabase(); - db.delete(TABLE_NAME, - "initial_hash = X'" + Strings.hex(initialHash) + "'", - null); + db.delete( + TABLE_NAME, + "initial_hash=X'" + hex(initialHash) + "'", + null + ); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 9a0f7ec..87a2bc2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.repository; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; + import ch.dissem.apps.abit.util.Assets; /** @@ -26,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 4; + public static final int DATABASE_VERSION = 5; public static final String DATABASE_NAME = "jabit.db"; protected final Context ctx; @@ -38,7 +39,7 @@ public class SqlHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { - onUpgrade(db, 0, 2); + onUpgrade(db, 0, DATABASE_VERSION); } @Override @@ -56,8 +57,11 @@ public class SqlHelper extends SQLiteOpenHelper { case 3: executeMigration(db, "V3.1__Update_table_POW"); executeMigration(db, "V3.2__Update_table_message"); + case 4: + executeMigration(db, "V3.3__Create_table_node"); default: - // Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION. + // Nothing to do. Let's assume we won't upgrade from a version that's newer than + // DATABASE_VERSION. } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 178c8bb..1bc0f77 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -28,16 +28,17 @@ import ch.dissem.apps.abit.pow.ServerPowEngine; import ch.dissem.apps.abit.repository.AndroidAddressRepository; import ch.dissem.apps.abit.repository.AndroidInventory; import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.repository.AndroidNodeRegistry; import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository; import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.apps.abit.util.Constants; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.TTL; import static ch.dissem.bitmessage.utils.UnixTime.DAY; @@ -58,6 +59,7 @@ public class Singleton { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); powRepo = new AndroidProofOfWorkRepository(sqlHelper); + TTL.pubkey(2 * DAY); bitmessageContext = new BitmessageContext.Builder() .proofOfWorkEngine(new SwitchingProofOfWorkEngine( ctx, Constants.PREFERENCE_SERVER_POW, @@ -65,15 +67,14 @@ public class Singleton { new ServicePowEngine(ctx) )) .cryptography(new AndroidCryptography()) - .nodeRegistry(new MemoryNodeRegistry()) + .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) .inventory(new AndroidInventory(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) .powRepo(powRepo) - .networkHandler(new DefaultNetworkHandler()) + .networkHandler(new NioNetworkHandler()) .listener(getMessageListener(ctx)) .doNotSendPubkeyOnIdentityCreation() - .pubkeyTTL(2 * DAY) .build(); } } diff --git a/build.gradle b/build.gradle index e8ad5dc..de60887 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From c8a0301402abafcc0a4556d066be1b7254c15a02 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 16 Sep 2016 17:35:24 +0200 Subject: [PATCH 056/110] GUI, layout and style improvements, updated dependencies --- app/build.gradle | 15 +- .../apps/abit/AddressDetailActivity.java | 31 +- .../ch/dissem/apps/abit/DetailActivity.java | 52 ++ .../ch/dissem/apps/abit/MainActivity.java | 474 +++++++++--------- .../apps/abit/MessageDetailActivity.java | 31 +- .../ch/dissem/apps/abit/SettingsActivity.java | 12 +- .../ch/dissem/apps/abit/StatusActivity.java | 9 + .../apps/abit/listener/WifiReceiver.java | 19 +- .../ch/dissem/apps/abit/util/Preferences.java | 2 +- app/src/main/res/values-v21/styles.xml | 24 - app/src/main/res/values/colors.xml | 53 +- app/src/main/res/values/styles.xml | 18 +- 12 files changed, 370 insertions(+), 370 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/DetailActivity.java delete mode 100644 app/src/main/res/values-v21/styles.xml diff --git a/app/build.gradle b/app/build.gradle index 3ba3317..f2ecf7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,20 +44,23 @@ dependencies { compile 'org.slf4j:slf4j-android:1.7.12' - compile('com.mikepenz:materialdrawer:3.1.0@aar') { + compile 'com.mikepenz:materialize:1.0.0@aar' + compile('com.mikepenz:materialdrawer:5.6.0@aar') { transitive = true } - compile('com.mikepenz:aboutlibraries:5.3.4@aar') { + compile('com.mikepenz:aboutlibraries:5.8.1@aar') { transitive = true } compile 'com.mikepenz:iconics:1.6.2@aar' - compile 'com.mikepenz:community-material-typeface:1.1.71@aar' + compile 'com.mikepenz:community-material-typeface:1.5.54.2@aar' compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar' compile 'com.google.zxing:core:3.2.0' compile 'io.github.yavski:fab-speed-dial:1.0.2' - - compile 'com.github.amlcurran.showcaseview:library:5.4.0' + compile 'com.github.amlcurran.showcaseview:library:5.4.3' + compile ('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar'){ + transitive=true + } testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' @@ -72,4 +75,4 @@ android { lintOptions { abortOnError false } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java index 077a5d4..a00e613 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java @@ -16,12 +16,7 @@ package ch.dissem.apps.abit; -import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.NavUtils; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; /** @@ -33,18 +28,11 @@ import android.view.MenuItem; * This activity is mostly just a 'shell' activity containing nothing * more than a {@link AddressDetailFragment}. */ -public class AddressDetailActivity extends AppCompatActivity { +public class AddressDetailActivity extends DetailActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.scrolling_toolbar_layout); - - final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - // Show the Up button in the action bar. - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(true); // savedInstanceState is non-null when there is fragment state // saved from previous configurations of this activity @@ -68,21 +56,4 @@ public class AddressDetailActivity extends AppCompatActivity { .commit(); } } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - // This ID represents the Home or Up button. In the case of this - // activity, the Up button is shown. Use NavUtils to allow users - // to navigate up one level in the application structure. For - // more details, see the Navigation pattern on Android Design: - // - // http://developer.android.com/design/patterns/navigation.html#up-vs-back - // - NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); - return true; - } - return super.onOptionsItemSelected(item); - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java new file mode 100644 index 0000000..fb94ef2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java @@ -0,0 +1,52 @@ +package ch.dissem.apps.abit; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + +import com.mikepenz.materialize.MaterializeBuilder; + +/** + * @author Christian Basler + */ +public class DetailActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.scrolling_toolbar_layout); + + final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + // Show the Up button in the action bar. + //noinspection ConstantConditions + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + new MaterializeBuilder() + .withActivity(this) + .withStatusBarColorRes(R.color.colorPrimaryDark) + .withTranslucentStatusBarProgrammatically(true) + .withStatusBarPadding(true) + .build(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + // This ID represents the Home or Up button. In the case of this + // activity, the Up button is shown. Use NavUtils to allow users + // to navigate up one level in the application structure. For + // more details, see the Navigation pattern on Android Design: + // + // http://developer.android.com/design/patterns/navigation.html#up-vs-back + // + NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index b1e9ded..b8825c8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -32,7 +32,6 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.CompoundButton; import android.widget.RelativeLayout; import android.widget.TextView; @@ -43,10 +42,11 @@ import com.github.amlcurran.showcaseview.targets.Target; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; +import com.mikepenz.materialdrawer.AccountHeader; +import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; -import com.mikepenz.materialdrawer.accountswitcher.AccountHeader; -import com.mikepenz.materialdrawer.accountswitcher.AccountHeaderBuilder; +import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener; import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; @@ -55,7 +55,6 @@ import com.mikepenz.materialdrawer.model.SwitchDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IProfile; import com.mikepenz.materialdrawer.model.interfaces.Nameable; -import com.mikepenz.materialdrawer.model.interfaces.OnCheckedChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +99,7 @@ import static ch.dissem.apps.abit.service.BitmessageService.isRunning; * </p> */ public class MainActivity extends AppCompatActivity - implements ListSelectionListener<Serializable>, ActionBarListener { + implements ListSelectionListener<Serializable>, ActionBarListener { public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; @@ -109,7 +108,7 @@ public class MainActivity extends AppCompatActivity private static final int MANAGE_IDENTITY = 2; private static final int ADD_CHAN = 3; - public static WeakReference<MainActivity> instance; + private static WeakReference<MainActivity> instance; /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet @@ -157,7 +156,7 @@ public class MainActivity extends AppCompatActivity MessageListFragment listFragment = new MessageListFragment(); getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment) - .commit(); + .commit(); if (findViewById(R.id.message_detail_container) != null) { // The detail container view will be present only in the @@ -187,42 +186,42 @@ public class MainActivity extends AppCompatActivity } if (drawer.isDrawerOpen()) { RelativeLayout.LayoutParams lps = new RelativeLayout.LayoutParams(ViewGroup - .LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + .LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT); int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue(); lps.setMargins(margin, margin, margin, margin); showcaseView = new ShowcaseView.Builder(this) - .withMaterialShowcase() - .setStyle(R.style.CustomShowcaseTheme) - .setContentTitle(R.string.full_node) - .setContentText(R.string.full_node_description) - .setTarget(new Target() { - @Override - public Point getPoint() { - View view = drawer.getStickyFooter(); - int[] location = new int[2]; - view.getLocationInWindow(location); - int x = location[0] + 7 * view.getWidth() / 8; - int y = location[1] + view.getHeight() / 2; - return new Point(x, y); - } + .withMaterialShowcase() + .setStyle(R.style.CustomShowcaseTheme) + .setContentTitle(R.string.full_node) + .setContentText(R.string.full_node_description) + .setTarget(new Target() { + @Override + public Point getPoint() { + View view = drawer.getStickyFooter(); + int[] location = new int[2]; + view.getLocationInWindow(location); + int x = location[0] + 7 * view.getWidth() / 8; + int y = location[1] + view.getHeight() / 2; + return new Point(x, y); } - ) - .replaceEndButton(R.layout.showcase_button) - .hideOnTouchOutside() - .build(); + } + ) + .replaceEndButton(R.layout.showcase_button) + .hideOnTouchOutside() + .build(); showcaseView.setButtonPosition(lps); } } private void changeList(AbstractItemListFragment<?> listFragment) { getSupportFragmentManager() - .beginTransaction() - .replace(R.id.item_list, listFragment) - .addToBackStack(null) - .commit(); + .beginTransaction() + .replace(R.id.item_list, listFragment) + .addToBackStack(null) + .commit(); if (twoPane) { // In two-pane mode, list items should be given the @@ -236,90 +235,91 @@ public class MainActivity extends AppCompatActivity for (BitmessageAddress identity : bmc.addresses().getIdentities()) { LOG.info("Adding identity " + identity.getAddress()); profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withNameShown(true) - .withEmail(identity.getAddress()) - .withTag(identity) + .withIcon(new Identicon(identity)) + .withName(identity.toString()) + .withNameShown(true) + .withEmail(identity.getAddress()) + .withTag(identity) ); } if (profiles.isEmpty()) { // Create an initial identity BitmessageAddress identity = Singleton.getIdentity(this); profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity) + .withIcon(new Identicon(identity)) + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity) ); } profiles.add(new ProfileSettingDrawerItem() - .withName(getString(R.string.add_identity)) - .withDescription(getString(R.string.add_identity_summary)) - .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) - .actionBar() - .paddingDp(5) - .colorRes(R.color.icons)) - .withIdentifier(ADD_IDENTITY) + .withName(getString(R.string.add_identity)) + .withDescription(getString(R.string.add_identity_summary)) + .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) + .actionBar() + .paddingDp(5) + .colorRes(R.color.icons)) + .withIdentifier(ADD_IDENTITY) ); profiles.add(new ProfileSettingDrawerItem() - .withName(getString(R.string.add_chan)) - .withDescription(getString(R.string.add_chan_summary)) - .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) - .actionBar() - .paddingDp(5) - .colorRes(R.color.icons)) - .withIdentifier(ADD_CHAN) + .withName(getString(R.string.add_chan)) + .withDescription(getString(R.string.add_chan_summary)) + .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) + .actionBar() + .paddingDp(5) + .colorRes(R.color.icons)) + .withIdentifier(ADD_CHAN) ); profiles.add(new ProfileSettingDrawerItem() - .withName(getString(R.string.manage_identity)) - .withIcon(GoogleMaterial.Icon.gmd_settings) - .withIdentifier(MANAGE_IDENTITY) + .withName(getString(R.string.manage_identity)) + .withIcon(GoogleMaterial.Icon.gmd_settings) + .withIdentifier(MANAGE_IDENTITY) ); // Create the AccountHeader accountHeader = new AccountHeaderBuilder() - .withActivity(this) - .withHeaderBackground(R.drawable.header) - .withProfiles(profiles) - .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { - @Override - public boolean onProfileChanged(View view, IProfile profile, boolean - currentProfile) { - switch (profile.getIdentifier()) { - case ADD_IDENTITY: - addIdentityDialog(); - break; - case ADD_CHAN: - addChanDialog(); - break; - case MANAGE_IDENTITY: - Intent show = new Intent(MainActivity.this, - AddressDetailActivity.class); - show.putExtra(AddressDetailFragment.ARG_ITEM, - Singleton.getIdentity(getApplicationContext())); - startActivity(show); - break; - default: - if (profile instanceof ProfileDrawerItem) { - Object tag = ((ProfileDrawerItem) profile).getTag(); - if (tag instanceof BitmessageAddress) { - Singleton.setIdentity((BitmessageAddress) tag); - } + .withActivity(this) + .withHeaderBackground(R.drawable.header) + .withProfiles(profiles) + .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { + @Override + public boolean onProfileChanged(View view, IProfile profile, boolean + currentProfile) { + switch ((int) profile.getIdentifier()) { + case ADD_IDENTITY: + addIdentityDialog(); + break; + case ADD_CHAN: + addChanDialog(); + break; + case MANAGE_IDENTITY: + Intent show = new Intent(MainActivity.this, + AddressDetailActivity.class); + show.putExtra(AddressDetailFragment.ARG_ITEM, + Singleton.getIdentity(getApplicationContext())); + startActivity(show); + break; + default: + if (profile instanceof ProfileDrawerItem) { + Object tag = ((ProfileDrawerItem) profile).getTag(); + if (tag instanceof BitmessageAddress) { + Singleton.setIdentity((BitmessageAddress) tag); } - } - // false if it should close the drawer - return false; + } } - }) - .build(); + // false if it should close the drawer + return false; + } + }) + .build(); if (profiles.size() > 2) { // There's always the add and manage identity items accountHeader.setActiveProfile(profiles.get(0), true); } ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); for (Label label : labels) { - PrimaryDrawerItem item = new PrimaryDrawerItem().withName(label.toString()).withTag - (label); + PrimaryDrawerItem item = new PrimaryDrawerItem() + .withName(label.toString()) + .withTag(label); if (label.getType() == null) { item.withIcon(CommunityMaterial.Icon.cmd_label); } else { @@ -349,146 +349,144 @@ public class MainActivity extends AppCompatActivity drawerItems.add(item); } drawerItems.add(new PrimaryDrawerItem() - .withName(R.string.archive) - .withTag(null) - .withIcon(CommunityMaterial.Icon.cmd_archive) + .withName(R.string.archive) + .withTag(null) + .withIcon(CommunityMaterial.Icon.cmd_archive) ); drawerItems.add(new DividerDrawerItem()); drawerItems.add(new PrimaryDrawerItem() - .withName(R.string.contacts_and_subscriptions) - .withIcon(GoogleMaterial.Icon.gmd_contacts)); + .withName(R.string.contacts_and_subscriptions) + .withIcon(GoogleMaterial.Icon.gmd_contacts)); drawerItems.add(new PrimaryDrawerItem() - .withName(R.string.settings) - .withIcon(GoogleMaterial.Icon.gmd_settings)); + .withName(R.string.settings) + .withIcon(GoogleMaterial.Icon.gmd_settings)); drawer = new DrawerBuilder() - .withActivity(this) - .withToolbar(toolbar) - .withAccountHeader(accountHeader) - .withDrawerItems(drawerItems) - .addStickyDrawerItems( - new SwitchDrawerItem() - .withName(R.string.full_node) - .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) - .withChecked(isRunning()) - .withOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(IDrawerItem drawerItem, - CompoundButton buttonView, - boolean isChecked) { - if (isChecked) { - checkAndStartNode(buttonView); - } else { - service.shutdownNode(); - } - } - }) - ) - .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { - @Override - public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long - l, IDrawerItem item) { - if (item.getTag() instanceof Label) { - selectedLabel = (Label) item.getTag(); - showSelectedLabel(); - return false; - } else if (item instanceof Nameable<?>) { - Nameable<?> ni = (Nameable<?>) item; - switch (ni.getNameRes()) { - case R.string.contacts_and_subscriptions: - if (!(getSupportFragmentManager().findFragmentById(R.id - .item_list) instanceof AddressListFragment)) { - changeList(new AddressListFragment()); - } else { - ((AddressListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(); - } - - break; - case R.string.settings: - startActivity(new Intent(MainActivity.this, SettingsActivity - .class)); - break; - case R.string.archive: - selectedLabel = null; - showSelectedLabel(); - break; - case R.string.full_node: - return true; + .withActivity(this) + .withToolbar(toolbar) + .withAccountHeader(accountHeader) + .withDrawerItems(drawerItems) + .addStickyDrawerItems( + new SwitchDrawerItem() + .withName(R.string.full_node) + .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) + .withChecked(isRunning()) + .withOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(IDrawerItem drawerItem, + CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + checkAndStartNode(buttonView); + } else { + service.shutdownNode(); } } + }) + ) + .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { + @Override + public boolean onItemClick(View view, int position, IDrawerItem item) { + if (item.getTag() instanceof Label) { + selectedLabel = (Label) item.getTag(); + showSelectedLabel(); return false; + } else if (item instanceof Nameable<?>) { + Nameable<?> ni = (Nameable<?>) item; + switch (ni.getName().getTextRes()) { + case R.string.contacts_and_subscriptions: + if (!(getSupportFragmentManager().findFragmentById(R.id + .item_list) instanceof AddressListFragment)) { + changeList(new AddressListFragment()); + } else { + ((AddressListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(); + } + break; + case R.string.settings: + startActivity(new Intent(MainActivity.this, SettingsActivity + .class)); + break; + case R.string.archive: + selectedLabel = null; + showSelectedLabel(); + break; + case R.string.full_node: + return true; + } } - }) - .withShowDrawerOnFirstLaunch(true) - .build(); + return false; + } + }) + .withShowDrawerOnFirstLaunch(true) + .build(); } private void addIdentityDialog() { new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.add_identity_warning) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { + .setMessage(R.string.add_identity_warning) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + Toast.makeText(MainActivity.this, + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { @Override - public void onClick(DialogInterface dialog, - int which) { - Toast.makeText(MainActivity.this, - R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Void, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(MainActivity.this, - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - addIdentityEntry(chan); - } - }.execute(); + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(MainActivity.this, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + addIdentityEntry(chan); + } + }.execute(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); } private void addChanDialog() { @SuppressLint("InflateParams") final View dialogView = getLayoutInflater().inflate(R.layout.dialog_input_passphrase, null); new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.add_chan) - .setView(dialogView) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - Toast.makeText(MainActivity.this, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<String, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(String... args) { - String pass = args[0]; - BitmessageAddress chan = bmc.createChan(pass); - chan.setAlias(pass); - bmc.addresses().save(chan); - return chan; - } + .setMessage(R.string.add_chan) + .setView(dialogView) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + Toast.makeText(MainActivity.this, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<String, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(String... args) { + String pass = args[0]; + BitmessageAddress chan = bmc.createChan(pass); + chan.setAlias(pass); + bmc.addresses().save(chan); + return chan; + } - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(MainActivity.this, - R.string.toast_chan_created, - Toast.LENGTH_SHORT).show(); - addIdentityEntry(chan); - } - }.execute(passphrase.getText().toString()); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(MainActivity.this, + R.string.toast_chan_created, + Toast.LENGTH_SHORT).show(); + addIdentityEntry(chan); + } + }.execute(passphrase.getText().toString()); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); } @Override @@ -500,18 +498,18 @@ public class MainActivity extends AppCompatActivity private void addIdentityEntry(BitmessageAddress identity) { IProfile newProfile = new - ProfileDrawerItem() - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity); + ProfileDrawerItem() + .withName(identity.toString()) + .withEmail(identity.getAddress()) + .withTag(identity); if (accountHeader.getProfiles() != null) { // we know that there are 3 setting // elements. // Set the new profile above them ;) accountHeader.addProfile( - newProfile, accountHeader - .getProfiles().size() - - 3); + newProfile, accountHeader + .getProfiles().size() + - 3); } else { accountHeader.addProfiles(newProfile); } @@ -530,20 +528,20 @@ public class MainActivity extends AppCompatActivity service.startupNode(); } else { new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.full_node_warning) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - service.startupNode(); - } - }) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - buttonView.setChecked(false); - } - }) - .show(); + .setMessage(R.string.full_node_warning) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + service.startupNode(); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + buttonView.setChecked(false); + } + }) + .show(); } } @@ -556,7 +554,7 @@ public class MainActivity extends AppCompatActivity if (unread > 0) { ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); } else { - ((PrimaryDrawerItem) item).withBadge(null); + ((PrimaryDrawerItem) item).withBadge((String) null); } } } @@ -564,9 +562,9 @@ public class MainActivity extends AppCompatActivity private void showSelectedLabel() { if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof - MessageListFragment) { + MessageListFragment) { ((MessageListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(selectedLabel); + .findFragmentById(R.id.item_list)).updateList(selectedLabel); } else { MessageListFragment listFragment = new MessageListFragment(); changeList(listFragment); @@ -593,12 +591,12 @@ public class MainActivity extends AppCompatActivity fragment = new AddressDetailFragment(); else throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + - "was " - + item.getClass().getSimpleName()); + "was " + + item.getClass().getSimpleName()); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() - .replace(R.id.message_detail_container, fragment) - .commit(); + .replace(R.id.message_detail_container, fragment) + .commit(); } else { // In single-pane mode, simply start the detail activity // for the selected item ID. @@ -609,8 +607,8 @@ public class MainActivity extends AppCompatActivity detailIntent = new Intent(this, AddressDetailActivity.class); else throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + - "was " - + item.getClass().getSimpleName()); + "was " + + item.getClass().getSimpleName()); detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); startActivity(detailIntent); @@ -632,7 +630,7 @@ public class MainActivity extends AppCompatActivity protected void onStart() { super.onStart(); bindService(new Intent(this, BitmessageService.class), connection, Context - .BIND_AUTO_CREATE); + .BIND_AUTO_CREATE); } @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java index 7957185..dbad234 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java @@ -1,11 +1,6 @@ package ch.dissem.apps.abit; -import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.NavUtils; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; /** @@ -17,18 +12,11 @@ import android.view.MenuItem; * This activity is mostly just a 'shell' activity containing nothing * more than a {@link MessageDetailFragment}. */ -public class MessageDetailActivity extends AppCompatActivity { +public class MessageDetailActivity extends DetailActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.scrolling_toolbar_layout); - - final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - // Show the Up button in the action bar. - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(true); // savedInstanceState is non-null when there is fragment state // saved from previous configurations of this activity @@ -52,21 +40,4 @@ public class MessageDetailActivity extends AppCompatActivity { .commit(); } } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - // This ID represents the Home or Up button. In the case of this - // activity, the Up button is shown. Use NavUtils to allow users - // to navigate up one level in the application structure. For - // more details, see the Navigation pattern on Android Design: - // - // http://developer.android.com/design/patterns/navigation.html#up-vs-back - // - NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); - return true; - } - return super.onOptionsItemSelected(item); - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index 6a86ef2..07f4cc7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -7,22 +7,14 @@ import android.support.v7.widget.Toolbar; /** * @author Christian Basler */ -public class SettingsActivity extends AppCompatActivity { +public class SettingsActivity extends DetailActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.scrolling_toolbar_layout); - - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(false); // Display the fragment as the main content. getFragmentManager().beginTransaction() .replace(R.id.content, new SettingsFragment()) .commit(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java index 1babcf6..cefd609 100644 --- a/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/StatusActivity.java @@ -21,6 +21,8 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.widget.TextView; +import com.mikepenz.materialize.MaterializeBuilder; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -39,6 +41,13 @@ public class StatusActivity extends AppCompatActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(false); + new MaterializeBuilder() + .withActivity(this) + .withStatusBarColorRes(R.color.colorPrimaryDark) + .withTranslucentStatusBarProgrammatically(true) + .withStatusBarPadding(true) + .build(); + BitmessageContext bmc = Singleton.getBitmessageContext(this); StringBuilder status = new StringBuilder(); for (BitmessageAddress address : bmc.addresses().getIdentities()) { diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java index e4ea8e0..ccd81ac 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java @@ -32,20 +32,29 @@ public class WifiReceiver extends BroadcastReceiver { if (Preferences.isWifiOnly(ctx)) { BitmessageContext bmc = Singleton.getBitmessageContext(ctx); - if (!isConnectedToWifi(ctx) && bmc.isRunning()) { + if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) { bmc.shutdown(); } } } - public static boolean isConnectedToWifi(Context ctx) { + public static boolean isConnectedToMeteredNetwork(Context ctx) { NetworkInfo netInfo = getNetworkInfo(ctx); - return netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI; + if (netInfo == null || !netInfo.isConnectedOrConnecting()) { + return false; + } + switch (netInfo.getType()){ + case ConnectivityManager.TYPE_ETHERNET: + case ConnectivityManager.TYPE_WIFI: + return false; + default: + return true; + } } private static NetworkInfo getNetworkInfo(Context ctx) { ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context - .CONNECTIVITY_SERVICE); + .CONNECTIVITY_SERVICE); return conMan.getActiveNetworkInfo(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index a0cee3f..b7f3d97 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -101,7 +101,7 @@ public class Preferences { } public static boolean isConnectionAllowed(Context ctx) { - return !isWifiOnly(ctx) || WifiReceiver.isConnectedToWifi(ctx); + return !isWifiOnly(ctx) || !WifiReceiver.isConnectedToMeteredNetwork(ctx); } public static boolean isWifiOnly(Context ctx) { diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index 72150b4..0000000 --- a/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <style name="AppTheme" parent="AppTheme.Base"> - - <!-- Main theme colors --> - <!-- your app branding color for the app bar --> - <item name="android:colorPrimary">@color/colorPrimary</item> - <!-- darker variant for the status bar and contextual app bars --> - <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item> - <!-- theme UI controls like checkboxes and text fields --> - <item name="android:colorAccent">@color/colorAccent</item> - - <item name="android:windowContentTransitions">true</item> - <item name="android:windowAllowEnterTransitionOverlap">true</item> - <item name="android:windowAllowReturnTransitionOverlap">true</item> - <item name="android:windowSharedElementEnterTransition">@android:transition/move</item> - <item name="android:windowSharedElementExitTransition">@android:transition/move</item> - - <item name="android:windowDrawsSystemBarBackgrounds">true</item> - <!--<item name="android:statusBarColor">@android:color/transparent</item>--> - - </style> -</resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8958204..6e7e100 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,13 +1,48 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Palette generated by Material Palette - materialpalette.com/blue-grey/orange --> <resources> - <color name="colorPrimary">#FFC107</color> - <color name="colorPrimaryDark">#FFA000</color> - <color name="colorPrimaryDarkText">#DEFFFFFF</color> - <color name="colorPrimaryLight">#FFECB3</color> - <color name="colorAccent">#607D8B</color> - <color name="colorPrimaryText">#212121</color> - <color name="colorSecondaryText">#727272</color> - <color name="icons">#212121</color> - <color name="divider">#B6B6B6</color> + <color name="colorPrimary">#FFC107</color> + <color name="colorPrimaryDark">#FFA000</color> + <color name="colorPrimaryDarkText">#DEFFFFFF</color> + <color name="colorPrimaryLight">#FFECB3</color> + <color name="colorAccent">#607D8B</color> + <color name="colorPrimaryText">#212121</color> + <color name="colorSecondaryText">#727272</color> + <color name="icons">#212121</color> + <color name="divider">#B6B6B6</color> + + <!-- Material DEFAULT colors --> + <color name="material_drawer_primary">@color/colorPrimary</color> + <color name="material_drawer_primary_dark">@color/colorPrimaryDark</color> + <color name="material_drawer_primary_light">@color/colorPrimaryLight</color> + <color name="material_drawer_accent">@color/colorAccent</color> + + <!-- OVERWRITE THESE COLORS FOR A LIGHT THEME --> + <!-- MaterialDrawer DEFAULT colors --> + <color name="material_drawer_background">@color/colorPrimaryDark</color> + <!-- Material DEFAULT text / items colors --> + <color name="material_drawer_primary_text">@color/colorPrimaryText</color> + <color name="material_drawer_primary_icon">@color/icons</color> + <color name="material_drawer_secondary_text">@color/colorSecondaryText</color> + <color name="material_drawer_hint_text">@color/colorSecondaryText</color> + <color name="material_drawer_divider">@color/divider</color> + <!-- Material DEFAULT drawer colors --> + <color name="material_drawer_selected">@color/primary</color> + <color name="material_drawer_selected_text">@color/colorPrimaryText</color> + <color name="material_drawer_header_selection_text">@color/colorPrimaryText</color> + + <!-- OVERWRITE THESE COLORS FOR A DARK THEME --> + <!-- MaterialDrawer DEFAULT DARK colors --> + <color name="material_drawer_dark_background">#303030</color> + <!-- MaterialDrawer DEFAULT DARK text / items colors --> + <color name="material_drawer_dark_primary_text">#DEFFFFFF</color> + <color name="material_drawer_dark_primary_icon">#8AFFFFFF</color> + <color name="material_drawer_dark_secondary_text">#8AFFFFFF</color> + <color name="material_drawer_dark_hint_text">#42FFFFFF</color> + <color name="material_drawer_dark_divider">#1FFFFFFF</color> + <!-- MaterialDrawer DEFAULT DARK drawer colors --> + <color name="material_drawer_dark_selected">#202020</color> + <color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color> + <color name="material_drawer_dark_header_selection_text">#FFF</color> + </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 58f503e..a299e46 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,28 +1,12 @@ <resources> <!-- Base application theme. --> - <style name="AppTheme.Base" parent="MaterialDrawerTheme.Light.DarkToolbar.TranslucentStatus"> - + <style name="AppTheme" parent="MaterialDrawerTheme.Light.DarkToolbar.TranslucentStatus"> <item name="android:activatedBackgroundIndicator">@color/colorPrimaryLight</item> <item name="android:textColor">@color/colorPrimaryText</item> <item name="android:textColorSecondary">@color/colorSecondaryText</item> - - <!-- MaterialDrawer specific values --> - <item name="material_drawer_background">@color/colorPrimaryDark</item> - <item name="material_drawer_icons">@color/colorPrimaryText</item> - <item name="material_drawer_primary_icon">@color/icons</item> - <item name="material_drawer_primary_text">@color/colorPrimaryText</item> - <item name="material_drawer_secondary_text">@color/colorSecondaryText</item> - <item name="material_drawer_hint_text">@color/colorSecondaryText</item> - <item name="material_drawer_divider">@color/divider</item> - <item name="material_drawer_selected">@color/primary</item> - <item name="material_drawer_selected_text">@color/colorPrimaryText</item> - <item name="material_drawer_header_selection_text">@color/colorPrimaryText</item> - </style> - <style name="AppTheme" parent="AppTheme.Base"/> - <style name="CustomShowcaseTheme" parent="ShowcaseView"> <item name="sv_backgroundColor">#eeffc107</item> <item name="sv_showcaseColor">#ffc107</item> From 141c17a28cb99cc9a61eaf6ed554c1608087bb4a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 21 Sep 2016 20:39:53 +0200 Subject: [PATCH 057/110] Bumped Jabit version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f2ecf7f..364f4d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { } } -ext.jabitVersion = 'feature-nio-SNAPSHOT' +ext.jabitVersion = 'development-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.0' From 1c226a6a5b89f2341d9d443162b041d3c7231493 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 21 Sep 2016 23:46:57 +0200 Subject: [PATCH 058/110] Added swipe actions for messages. - there is a minor layout problem on pre-Lollipop devices --- .../apps/abit/AbstractItemListFragment.java | 4 +- .../dissem/apps/abit/AddressListFragment.java | 2 +- .../java/ch/dissem/apps/abit/ListHolder.java | 28 ++ .../ch/dissem/apps/abit/MainActivity.java | 2 +- .../apps/abit/MessageDetailFragment.java | 2 +- .../dissem/apps/abit/MessageListFragment.java | 177 ++++++++--- .../abit/adapter/SwipeableMessageAdapter.java | 282 ++++++++++++++++++ .../abit/synchronization/SyncAdapter.java | 110 +++---- .../ch/dissem/apps/abit/util/Preferences.java | 11 +- .../res/drawable/bg_item_normal_state.xml | 21 ++ .../drawable/bg_item_swiping_active_state.xml | 21 ++ .../res/drawable/bg_item_swiping_state.xml | 21 ++ .../main/res/drawable/bg_swipe_item_left.xml | 27 ++ .../res/drawable/bg_swipe_item_neutral.xml | 19 ++ .../main/res/drawable/bg_swipe_item_right.xml | 25 ++ .../res/drawable/ic_item_swipe_archive.xml | 9 + .../main/res/drawable/ic_item_swipe_trash.xml | 9 + app/src/main/res/drawable/list_divider_h.xml | 22 ++ .../main/res/layout/fragment_message_list.xml | 40 +-- app/src/main/res/layout/message_row.xml | 135 +++++---- app/src/main/res/values/colors.xml | 8 + 21 files changed, 790 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/ListHolder.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java create mode 100644 app/src/main/res/drawable/bg_item_normal_state.xml create mode 100644 app/src/main/res/drawable/bg_item_swiping_active_state.xml create mode 100644 app/src/main/res/drawable/bg_item_swiping_state.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_left.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_neutral.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_right.xml create mode 100644 app/src/main/res/drawable/ic_item_swipe_archive.xml create mode 100644 app/src/main/res/drawable/ic_item_swipe_trash.xml create mode 100644 app/src/main/res/drawable/list_divider_h.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 5895277..3cb1e07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -28,7 +28,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; /** * @author Christian Basler */ -public abstract class AbstractItemListFragment<T> extends ListFragment { +public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder { /** * The serialization (saved instance state) Bundle key representing the * activated item position. Only used on tablets. @@ -55,8 +55,6 @@ public abstract class AbstractItemListFragment<T> extends ListFragment { private int activatedPosition = ListView.INVALID_POSITION; private boolean activateOnItemClick; - abstract void updateList(Label label); - @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 9f0dc48..66a52aa 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -166,7 +166,7 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr } @Override - void updateList(Label label) { + public void updateList(Label label) { updateList(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ListHolder.java b/app/src/main/java/ch/dissem/apps/abit/ListHolder.java new file mode 100644 index 0000000..19f2ab6 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ListHolder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * @author Christian Basler + */ +public interface ListHolder { + void updateList(Label label); + + void setActivateOnItemClick(boolean activateOnItemClick); +} diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index b8825c8..5233403 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -216,7 +216,7 @@ public class MainActivity extends AppCompatActivity } } - private void changeList(AbstractItemListFragment<?> listFragment) { + private <F extends Fragment & ListHolder> void changeList(F listFragment) { getSupportFragmentManager() .beginTransaction() .replace(R.id.item_list, listFragment) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 6afb2d0..bce2795 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -204,7 +204,7 @@ public class MessageDetailFragment extends Fragment { } } - private boolean isInTrash(Plaintext item) { + public static boolean isInTrash(Plaintext item) { for (Label label : item.getLabels()) { if (label.getType() == Label.Type.TRASH) { return true; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 71c8b20..bf79b0d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -17,19 +17,29 @@ package ch.dissem.apps.abit; import android.content.Intent; -import android.graphics.Typeface; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; +import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; +import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; +import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; +import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; +import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; + +import java.util.List; + +import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; @@ -37,6 +47,8 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; +import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash; + /** * A list fragment representing a list of Messages. This fragment * also supports tablet devices by allowing list items to be given an @@ -46,10 +58,19 @@ import ch.dissem.bitmessage.ports.MessageRepository; * Activities containing this fragment MUST implement the {@link ListSelectionListener} * interface. */ -public class MessageListFragment extends AbstractItemListFragment<Plaintext> { +public class MessageListFragment extends Fragment implements ListHolder { + + private RecyclerView recyclerView; + private RecyclerView.LayoutManager layoutManager; + private SwipeableMessageAdapter adapter; + private RecyclerView.Adapter wrappedAdapter; + private RecyclerViewSwipeManager recyclerViewSwipeManager; + private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager; private Label currentLabel; private MenuItem emptyTrashMenuItem; + private MessageRepository messageRepo; + private List<Plaintext> messages; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -68,8 +89,10 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { @Override public void onResume() { super.onResume(); + MainActivity activity = (MainActivity) getActivity(); + messageRepo = Singleton.getMessageRepository(activity); - doUpdateList(((MainActivity) getActivity()).getSelectedLabel()); + doUpdateList(activity.getSelectedLabel()); } @Override @@ -82,34 +105,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { } private void doUpdateList(Label label) { - setListAdapter(new ArrayAdapter<Plaintext>( - getActivity(), - android.R.layout.simple_list_item_activated_1, - android.R.id.text1, - Singleton.getMessageRepository(getContext()).findMessages(label)) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.message_row, null, false); - } - Plaintext item = getItem(position); - ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item.getFrom())); - TextView sender = (TextView) convertView.findViewById(R.id.sender); - sender.setText(item.getFrom().toString()); - TextView subject = (TextView) convertView.findViewById(R.id.subject); - subject.setText(item.getSubject()); - ((TextView) convertView.findViewById(R.id.text)).setText(item.getText()); - if (item.isUnread()) { - sender.setTypeface(Typeface.DEFAULT_BOLD); - subject.setTypeface(Typeface.DEFAULT_BOLD); - } else { - sender.setTypeface(Typeface.DEFAULT); - subject.setTypeface(Typeface.DEFAULT); - } - return convertView; - } - }); + messages = Singleton.getMessageRepository(getContext()).findMessages(label); if (getActivity() instanceof ActionBarListener) { if (label != null) { ((ActionBarListener) getActivity()).updateTitle(label.toString()); @@ -120,26 +116,127 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { if (emptyTrashMenuItem != null) { emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); } + adapter.setData(label, messages); + adapter.notifyDataSetChanged(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_message_list, container, false); + recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); + layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); // Show the dummy content as text in a TextView. - FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fab_compose_message); + FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id + .fab_compose_message); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity())); + Intent intent = new Intent(getActivity().getApplicationContext(), + ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity + (getActivity())); startActivity(intent); } }); + // touch guard manager (this class is required to suppress scrolling while swipe-dismiss + // animation is running) + recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager(); + recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning + (true); + recyclerViewTouchActionGuardManager.setEnabled(true); + + // swipe manager + recyclerViewSwipeManager = new RecyclerViewSwipeManager(); + + //adapter + adapter = new SwipeableMessageAdapter(); + adapter.setEventListener(new SwipeableMessageAdapter.EventListener() { + @Override + public void onItemDeleted(Plaintext item) { + if (isInTrash(item)) { + messageRepo.remove(item); + } else { + item.getLabels().clear(); + item.addLabels(messageRepo.getLabels(Label.Type.TRASH)); + messageRepo.save(item); + } + } + + @Override + public void onItemArchived(Plaintext item) { + item.getLabels().clear(); + messageRepo.save(item); + } + + @Override + public void onItemViewClicked(View v, boolean pinned) { + int position = recyclerView.getChildAdapterPosition(v); + if (position != RecyclerView.NO_POSITION) { + Plaintext item = adapter.getItem(position); + ((MainActivity) getActivity()).onItemSelected(item); + } + } + }); + + // wrap for swiping + wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter); + + final GeneralItemAnimator animator = new SwipeDismissItemAnimator(); + + // Change animations are enabled by default since support-v7-recyclerview v22. + // Disable the change animation in order to make turning back animation of swiped item + // works properly. + animator.setSupportsChangeAnimations(false); + + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(wrappedAdapter); // requires *wrapped* adapter + recyclerView.setItemAnimator(animator); + + recyclerView.addItemDecoration(new SimpleListDividerDecorator( + ContextCompat.getDrawable(getContext(), R.drawable.list_divider_h), true)); + + // NOTE: + // The initialization order is very important! This order determines the priority of + // touch event handling. + // + // priority: TouchActionGuard > Swipe > DragAndDrop + recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView); + recyclerViewSwipeManager.attachRecyclerView(recyclerView); + return rootView; } + @Override + public void onDestroyView() { + if (recyclerViewSwipeManager != null) { + recyclerViewSwipeManager.release(); + recyclerViewSwipeManager = null; + } + + if (recyclerViewTouchActionGuardManager != null) { + recyclerViewTouchActionGuardManager.release(); + recyclerViewTouchActionGuardManager = null; + } + + if (recyclerView != null) { + recyclerView.setItemAnimator(null); + recyclerView.setAdapter(null); + recyclerView = null; + } + + if (wrappedAdapter != null) { + WrapperAdapterUtils.releaseAll(wrappedAdapter); + wrappedAdapter = null; + } + adapter = null; + layoutManager = null; + + super.onDestroyView(); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.message_list, menu); @@ -164,4 +261,8 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { } } + @Override + public void setActivateOnItemClick(boolean activateOnItemClick) { + // TODO + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java new file mode 100644 index 0000000..0958fc0 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -0,0 +1,282 @@ +package ch.dissem.apps.abit.adapter; + +import android.graphics.Typeface; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.action + .SwipeResultActionMoveToSwipedDirection; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem; +import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder; +import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils; + +import java.util.Collections; +import java.util.List; + +import ch.dissem.apps.abit.Identicon; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * @author Christian Basler + */ +public class SwipeableMessageAdapter + extends RecyclerView.Adapter<SwipeableMessageAdapter.MyViewHolder> + implements SwipeableItemAdapter<SwipeableMessageAdapter.MyViewHolder>, SwipeableItemConstants { + + private List<Plaintext> data = Collections.emptyList(); + private EventListener eventListener; + private View.OnClickListener itemViewOnClickListener; + private View.OnClickListener swipeableViewContainerOnClickListener; + + private Label label; + + public interface EventListener { + void onItemDeleted(Plaintext item); + + void onItemArchived(Plaintext item); + + void onItemViewClicked(View v, boolean pinned); + } + + public static class MyViewHolder extends AbstractSwipeableItemViewHolder { + public FrameLayout container; + public final ImageView avatar; + public final TextView sender; + public final TextView subject; + public final TextView extract; + + public MyViewHolder(View v) { + super(v); + container = (FrameLayout) v.findViewById(R.id.container); + avatar = (ImageView) v.findViewById(R.id.avatar); + sender = (TextView) v.findViewById(R.id.sender); + subject = (TextView) v.findViewById(R.id.subject); + extract = (TextView) v.findViewById(R.id.text); + } + + @Override + public View getSwipeableContainerView() { + return container; + } + } + + public SwipeableMessageAdapter() { + itemViewOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onItemViewClick(v); + } + }; + swipeableViewContainerOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onSwipeableViewContainerClick(v); + } + }; + + // SwipeableItemAdapter requires stable ID, and also + // have to implement the getItemId() method appropriately. + setHasStableIds(true); + } + + public void setData(Label label, List<Plaintext> data) { + this.label = label; + this.data = data; + } + + private void onItemViewClick(View v) { + if (eventListener != null) { + eventListener.onItemViewClicked(v, true); // pinned + } + } + + private void onSwipeableViewContainerClick(View v) { + if (eventListener != null) { + eventListener.onItemViewClicked( + RecyclerViewAdapterUtils.getParentViewHolderItemView(v), false); // not pinned + } + } + + public Plaintext getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return (long) data.get(position).getId(); + } + + @Override + public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final View v = inflater.inflate(R.layout.message_row, parent, false); + return new MyViewHolder(v); + } + + @Override + public void onBindViewHolder(MyViewHolder holder, int position) { + final Plaintext item = data.get(position); + + // set listeners + // (if the item is *pinned*, click event comes to the itemView) + holder.itemView.setOnClickListener(itemViewOnClickListener); + // (if the item is *not pinned*, click event comes to the container) + holder.container.setOnClickListener(swipeableViewContainerOnClickListener); + + // set data + holder.avatar.setImageDrawable(new Identicon(item.getFrom())); + holder.sender.setText(item.getFrom().toString()); + holder.subject.setText(item.getSubject()); + holder.extract.setText(item.getText()); + if (item.isUnread()) { + holder.sender.setTypeface(Typeface.DEFAULT_BOLD); + holder.subject.setTypeface(Typeface.DEFAULT_BOLD); + } else { + holder.sender.setTypeface(Typeface.DEFAULT); + holder.subject.setTypeface(Typeface.DEFAULT); + } + } + + @Override + public int getItemCount() { + return data.size(); + } + + @Override + public int onGetSwipeReactionType(MyViewHolder holder, int position, int x, int y) { + if (label == null) { + return REACTION_CAN_NOT_SWIPE_BOTH_H_WITH_RUBBER_BAND_EFFECT; + } + if (label.getType() == Label.Type.TRASH) { + return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; + } + return REACTION_CAN_SWIPE_BOTH_H; + } + + @Override + public void onSetSwipeBackground(MyViewHolder holder, int position, int type) { + int bgRes = 0; + if (label == null) { + bgRes = R.drawable.bg_swipe_item_neutral; + } else { + switch (type) { + case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_neutral; + break; + case DRAWABLE_SWIPE_LEFT_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_left; + break; + case DRAWABLE_SWIPE_RIGHT_BACKGROUND: + if (label.getType() == Label.Type.TRASH) { + bgRes = R.drawable.bg_swipe_item_neutral; + } else { + bgRes = R.drawable.bg_swipe_item_right; + } + break; + } + } + holder.itemView.setBackgroundResource(bgRes); + } + + @Override + public SwipeResultAction onSwipeItem(MyViewHolder holder, final int position, int result) { + switch (result) { + // swipe right + case RESULT_SWIPED_RIGHT: + return new SwipeRightResultAction(this, position); + case RESULT_SWIPED_LEFT: + return new SwipeLeftResultAction(this, position); + // other --- do nothing + case RESULT_CANCELED: + default: + return null; + } + } + + public void setEventListener(EventListener eventListener) { + this.eventListener = eventListener; + } + + private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { + private SwipeableMessageAdapter adapter; + private final int position; + private final Plaintext item; + + SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) { + this.adapter = adapter; + this.position = position; + this.item = adapter.data.get(position); + } + + @Override + protected void onPerformAction() { + super.onPerformAction(); + + adapter.data.remove(position); + adapter.notifyItemRemoved(position); + } + + @Override + protected void onSlideAnimationEnd() { + super.onSlideAnimationEnd(); + + if (adapter.eventListener != null) { + adapter.eventListener.onItemDeleted(item); + } + } + + @Override + protected void onCleanUp() { + super.onCleanUp(); + // clear the references + adapter = null; + } + } + + private static class SwipeRightResultAction extends SwipeResultActionRemoveItem { + private SwipeableMessageAdapter adapter; + private final int position; + private final Plaintext item; + + SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) { + this.adapter = adapter; + this.position = position; + this.item = adapter.data.get(position); + } + + @Override + protected void onPerformAction() { + super.onPerformAction(); + + adapter.data.remove(position); + adapter.data.remove(position); + adapter.notifyItemRemoved(position); + } + + @Override + protected void onSlideAnimationEnd() { + super.onSlideAnimationEnd(); + + if (adapter.eventListener != null) { + adapter.eventListener.onItemArchived(item); + } + } + + @Override + protected void onCleanUp() { + super.onCleanUp(); + // clear the references + adapter = null; + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index a1982c8..dd8f984 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -28,6 +28,7 @@ import android.os.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.List; import ch.dissem.apps.abit.service.Singleton; @@ -35,6 +36,7 @@ import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.extensions.CryptoCustomMessage; import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; @@ -68,18 +70,24 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - if (account.equals(Authenticator.ACCOUNT_SYNC)) { - if (Preferences.isConnectionAllowed(getContext())) { - syncData(); + try { + if (account.equals(Authenticator.ACCOUNT_SYNC)) { + if (Preferences.isConnectionAllowed(getContext())) { + syncData(); + } + } else if (account.equals(Authenticator.ACCOUNT_POW)) { + syncPOW(); + } else { + syncResult.stats.numAuthExceptions++; } - } else if (account.equals(Authenticator.ACCOUNT_POW)) { - syncPOW(); - } else { - throw new RuntimeException("Unknown " + account); + } catch (IOException e) { + syncResult.stats.numIoExceptions++; + } catch (DecryptionFailedException e) { + syncResult.stats.numAuthExceptions++; } } - private void syncData() { + private void syncData() throws IOException { // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) { LOG.info("Synchronization skipped, Abit is acting as a full node"); @@ -87,61 +95,53 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } LOG.info("Synchronizing Bitmessage"); - try { - LOG.info("Synchronization started"); - bmc.synchronize( - Preferences.getTrustedNode(getContext()), - Preferences.getTrustedNodePort(getContext()), - Preferences.getTimeoutInSeconds(getContext()), - true); - LOG.info("Synchronization finished"); - } catch (RuntimeException e) { - LOG.error(e.getMessage(), e); - } + LOG.info("Synchronization started"); + bmc.synchronize( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + Preferences.getTimeoutInSeconds(getContext()), + true); + LOG.info("Synchronization finished"); } - private void syncPOW() { + private void syncPOW() throws IOException, DecryptionFailedException { // If the Bitmessage context acts as a full node, synchronization isn't necessary LOG.info("Looking for completed POW"); - try { - BitmessageAddress identity = Singleton.getIdentity(getContext()); - byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); - byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); - ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); - ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); - List<byte[]> items = powRepo.getItems(); - for (byte[] initialHash : items) { - ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); - byte[] target = cryptography().getProofOfWorkTarget(item.object, item - .nonceTrialsPerByte, item.extraBytes); - CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( - new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); - cryptoMsg.signAndEncrypt(identity, signingKey); - CustomMessage response = bmc.send( - Preferences.getTrustedNode(getContext()), - Preferences.getTrustedNodePort(getContext()), - cryptoMsg - ); - if (response.isError()) { - LOG.error("Server responded with error: " + new String(response.getData(), - "UTF-8")); - } else { - ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( - response, reader).decrypt(privateKey); - if (decryptedResponse.getRequest() == COMPLETE) { - bmc.internals().getProofOfWorkService().onNonceCalculated( - initialHash, decryptedResponse.getData()); - } + BitmessageAddress identity = Singleton.getIdentity(getContext()); + byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); + byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); + ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); + ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); + List<byte[]> items = powRepo.getItems(); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + byte[] target = cryptography().getProofOfWorkTarget(item.object, item + .nonceTrialsPerByte, item.extraBytes); + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( + new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); + cryptoMsg.signAndEncrypt(identity, signingKey); + CustomMessage response = bmc.send( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + cryptoMsg + ); + if (response.isError()) { + LOG.error("Server responded with error: " + new String(response.getData(), + "UTF-8")); + } else { + ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( + response, reader).decrypt(privateKey); + if (decryptedResponse.getRequest() == COMPLETE) { + bmc.internals().getProofOfWorkService().onNonceCalculated( + initialHash, decryptedResponse.getData()); } } - if (items.size() == 0) { - stopPowSync(getContext()); - } - LOG.info("Synchronization finished"); - } catch (Exception e) { - LOG.error(e.getMessage(), e); } + if (items.size() == 0) { + stopPowSync(getContext()); + } + LOG.info("Synchronization finished"); } public static void startSync(Context ctx) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index b7f3d97..66923cd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -23,6 +23,7 @@ import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; @@ -49,7 +50,7 @@ public class Preferences { * Warning, this method might do a network call and therefore can't be called from * the UI thread. */ - public static InetAddress getTrustedNode(Context ctx) { + public static InetAddress getTrustedNode(Context ctx) throws IOException { String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); if (trustedNode == null) return null; trustedNode = trustedNode.trim(); @@ -59,15 +60,7 @@ public class Preferences { int index = trustedNode.lastIndexOf(':'); trustedNode = trustedNode.substring(0, index); } - try { return InetAddress.getByName(trustedNode); - } catch (UnknownHostException e) { - new ErrorNotification(ctx) - .setError(R.string.error_invalid_sync_host) - .show(); - LOG.error(e.getMessage(), e); - return null; - } } public static int getTrustedNodePort(Context ctx) { diff --git a/app/src/main/res/drawable/bg_item_normal_state.xml b/app/src/main/res/drawable/bg_item_normal_state.xml new file mode 100644 index 0000000..be3568f --- /dev/null +++ b/app/src/main/res/drawable/bg_item_normal_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_normal_state"/> + </item> +</selector> diff --git a/app/src/main/res/drawable/bg_item_swiping_active_state.xml b/app/src/main/res/drawable/bg_item_swiping_active_state.xml new file mode 100644 index 0000000..b90dbeb --- /dev/null +++ b/app/src/main/res/drawable/bg_item_swiping_active_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_swiping_active_state"/> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/bg_item_swiping_state.xml b/app/src/main/res/drawable/bg_item_swiping_state.xml new file mode 100644 index 0000000..00ca171 --- /dev/null +++ b/app/src/main/res/drawable/bg_item_swiping_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_swiping_state"/> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_left.xml b/app/src/main/res/drawable/bg_swipe_item_left.xml new file mode 100644 index 0000000..ef32c48 --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_left.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_trash"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_trash" + android:gravity="right|center_vertical" + android:width="24dp" + android:height="24dp" + android:right="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_neutral.xml b/app/src/main/res/drawable/bg_swipe_item_neutral.xml new file mode 100644 index 0000000..890788c --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_neutral.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<color + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/bg_swipe_item_neutral"/> diff --git a/app/src/main/res/drawable/bg_swipe_item_right.xml b/app/src/main/res/drawable/bg_swipe_item_right.xml new file mode 100644 index 0000000..72bd0b0 --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_archive"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_archive" + android:gravity="left|center_vertical" + android:left="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable/ic_item_swipe_archive.xml b/app/src/main/res/drawable/ic_item_swipe_archive.xml new file mode 100644 index 0000000..ce5bf8a --- /dev/null +++ b/app/src/main/res/drawable/ic_item_swipe_archive.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_item_swipe_trash.xml b/app/src/main/res/drawable/ic_item_swipe_trash.xml new file mode 100644 index 0000000..f9213d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_item_swipe_trash.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> +</vector> diff --git a/app/src/main/res/drawable/list_divider_h.xml b/app/src/main/res/drawable/list_divider_h.xml new file mode 100644 index 0000000..fd1a2c0 --- /dev/null +++ b/app/src/main/res/drawable/list_divider_h.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<shape + android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1px"/> + <solid android:color="@color/divider"/> +</shape> diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index 5aa714b..286612c 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -4,26 +4,26 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <ListView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@id/android:list" + <android.support.v7.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:clipToPadding="false" - android:paddingBottom="88dp" - android:clipToPadding="false" - android:scrollbarStyle="outsideOverlay" - - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:layout_alignParentBottom="true"/> + android:paddingBottom="88dp" + android:scrollbarStyle="outsideOverlay" + android:scrollbars="vertical"/> <android.support.design.widget.FloatingActionButton - android:id="@+id/fab_compose_message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/ic_action_compose_message" - app:elevation="8dp" - android:layout_alignParentBottom="true" - android:layout_alignParentEnd="true" - android:layout_margin="16dp"/> -</RelativeLayout> \ No newline at end of file + android:id="@+id/fab_compose_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_margin="16dp" + android:src="@drawable/ic_action_compose_message" + app:elevation="8dp"/> +</RelativeLayout> diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index b80d6f1..5e9d6a7 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -15,64 +15,85 @@ ~ limitations under the License. --> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:src="@color/colorAccent" - android:layout_margin="16dp" - tools:ignore="ContentDescription"/> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/bg_swipe_item_neutral"> - <TextView - android:id="@+id/sender" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Sender" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_alignTop="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:layout_marginTop="-5dp" - android:paddingTop="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="0dp" - android:textStyle="bold" - /> + <FrameLayout + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:foreground="?attr/selectableItemBackground" + android:background="@drawable/bg_item_normal_state" + tools:ignore="UselessParent"> - <TextView - android:id="@+id/subject" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Subject" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:layout_below="@+id/sender" - android:layout_toEndOf="@+id/avatar"/> + <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <TextView - android:id="@+id/text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Text" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:gravity="center_vertical" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="8dp" - android:layout_below="@+id/subject" - android:layout_toEndOf="@+id/avatar"/> + <ImageView + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorAccent" + tools:ignore="ContentDescription"/> -</RelativeLayout> \ No newline at end of file + <TextView + android:id="@+id/sender" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/avatar" + android:layout_marginTop="-5dp" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Sender" + /> + + <TextView + android:id="@+id/subject" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/sender" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Subject"/> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/subject" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Text"/> + + </RelativeLayout> + + </FrameLayout> + +</FrameLayout> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6e7e100..e36d1eb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -45,4 +45,12 @@ <color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color> <color name="material_drawer_dark_header_selection_text">#FFF</color> + <!-- swipeable list --> + <color name="bg_item_normal_state">#ffffffff</color> + <color name="bg_item_swiping_state">@color/colorPrimaryLight</color> + <color name="bg_item_swiping_active_state">@color/colorPrimary</color> + <color name="bg_swipe_item_neutral">@android:color/transparent</color> + <color name="bg_swipe_item_trash">#fff45f30</color> + <color name="bg_swipe_item_archive">#fff9930d</color> + </resources> From cc4c16e970a5cd84205544fcd7ed30bf7e36d1ab Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 22 Sep 2016 00:09:59 +0200 Subject: [PATCH 059/110] Fixed layout issue --- .../bg_swipe_item_left.xml | 25 +++++++++++++++++++ .../bg_swipe_item_right.xml | 25 +++++++++++++++++++ .../main/res/drawable/bg_swipe_item_left.xml | 11 ++++---- .../main/res/drawable/bg_swipe_item_right.xml | 9 ++++--- 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml create mode 100644 app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml diff --git a/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml b/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml new file mode 100644 index 0000000..c5c77fb --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_trash"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_trash" + android:gravity="right|center_vertical" + android:right="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml b/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml new file mode 100644 index 0000000..72bd0b0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_archive"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_archive" + android:gravity="left|center_vertical" + android:left="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_left.xml b/app/src/main/res/drawable/bg_swipe_item_left.xml index ef32c48..471564e 100644 --- a/app/src/main/res/drawable/bg_swipe_item_left.xml +++ b/app/src/main/res/drawable/bg_swipe_item_left.xml @@ -18,10 +18,9 @@ <item> <color android:color="@color/bg_swipe_item_trash"/> </item> - <item - android:drawable="@drawable/ic_item_swipe_trash" - android:gravity="right|center_vertical" - android:width="24dp" - android:height="24dp" - android:right="16dp"/> + <item android:right="16dp"> + <bitmap + android:gravity="right|center_vertical" + android:src="@drawable/ic_item_swipe_trash"/> + </item> </layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_right.xml b/app/src/main/res/drawable/bg_swipe_item_right.xml index 72bd0b0..22e2b4e 100644 --- a/app/src/main/res/drawable/bg_swipe_item_right.xml +++ b/app/src/main/res/drawable/bg_swipe_item_right.xml @@ -18,8 +18,9 @@ <item> <color android:color="@color/bg_swipe_item_archive"/> </item> - <item - android:drawable="@drawable/ic_item_swipe_archive" - android:gravity="left|center_vertical" - android:left="16dp"/> + <item android:left="16dp"> + <bitmap + android:gravity="left|center_vertical" + android:src="@drawable/ic_item_swipe_archive"/> + </item> </layer-list> From 5ea317c295ec6143554bcc17c5cf72282d9b5334 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 23 Sep 2016 07:51:04 +0200 Subject: [PATCH 060/110] Fixed garbage shown on older devices when the ellipsized value had line breaks in them --- .../abit/adapter/SwipeableMessageAdapter.java | 82 +++++++++++-------- .../ch/dissem/apps/abit/util/Strings.java | 31 +++++++ app/src/main/res/layout/message_row.xml | 14 ++-- 3 files changed, 89 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/Strings.java diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index 0958fc0..843266a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -1,3 +1,20 @@ +/* + * Copyright 2015 Haruki Hasegawa + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit.adapter; import android.graphics.Typeface; @@ -26,12 +43,18 @@ import ch.dissem.apps.abit.R; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; + /** + * Adapted from the basic swipeable example by Haruki Hasegawa. See + * * @author Christian Basler + * @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview"> + * https://github.com/h6ah4i/android-advancedrecyclerview</a> */ public class SwipeableMessageAdapter - extends RecyclerView.Adapter<SwipeableMessageAdapter.MyViewHolder> - implements SwipeableItemAdapter<SwipeableMessageAdapter.MyViewHolder>, SwipeableItemConstants { + extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder> + implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants { private List<Plaintext> data = Collections.emptyList(); private EventListener eventListener; @@ -48,14 +71,14 @@ public class SwipeableMessageAdapter void onItemViewClicked(View v, boolean pinned); } - public static class MyViewHolder extends AbstractSwipeableItemViewHolder { + public static class ViewHolder extends AbstractSwipeableItemViewHolder { public FrameLayout container; public final ImageView avatar; public final TextView sender; public final TextView subject; public final TextView extract; - public MyViewHolder(View v) { + public ViewHolder(View v) { super(v); container = (FrameLayout) v.findViewById(R.id.container); avatar = (ImageView) v.findViewById(R.id.avatar); @@ -117,14 +140,14 @@ public class SwipeableMessageAdapter } @Override - public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View v = inflater.inflate(R.layout.message_row, parent, false); - return new MyViewHolder(v); + return new ViewHolder(v); } @Override - public void onBindViewHolder(MyViewHolder holder, int position) { + public void onBindViewHolder(ViewHolder holder, int position) { final Plaintext item = data.get(position); // set listeners @@ -136,8 +159,8 @@ public class SwipeableMessageAdapter // set data holder.avatar.setImageDrawable(new Identicon(item.getFrom())); holder.sender.setText(item.getFrom().toString()); - holder.subject.setText(item.getSubject()); - holder.extract.setText(item.getText()); + holder.subject.setText(normalizeWhitespaces(item.getSubject())); + holder.extract.setText(normalizeWhitespaces(item.getText())); if (item.isUnread()) { holder.sender.setTypeface(Typeface.DEFAULT_BOLD); holder.subject.setTypeface(Typeface.DEFAULT_BOLD); @@ -153,43 +176,36 @@ public class SwipeableMessageAdapter } @Override - public int onGetSwipeReactionType(MyViewHolder holder, int position, int x, int y) { - if (label == null) { - return REACTION_CAN_NOT_SWIPE_BOTH_H_WITH_RUBBER_BAND_EFFECT; - } - if (label.getType() == Label.Type.TRASH) { + public int onGetSwipeReactionType(ViewHolder holder, int position, int x, int y) { + if (label == null || label.getType() == Label.Type.TRASH) { return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; } return REACTION_CAN_SWIPE_BOTH_H; } @Override - public void onSetSwipeBackground(MyViewHolder holder, int position, int type) { + public void onSetSwipeBackground(ViewHolder holder, int position, int type) { int bgRes = 0; - if (label == null) { - bgRes = R.drawable.bg_swipe_item_neutral; - } else { - switch (type) { - case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND: + switch (type) { + case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_neutral; + break; + case DRAWABLE_SWIPE_LEFT_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_left; + break; + case DRAWABLE_SWIPE_RIGHT_BACKGROUND: + if (label == null || label.getType() == Label.Type.TRASH) { bgRes = R.drawable.bg_swipe_item_neutral; - break; - case DRAWABLE_SWIPE_LEFT_BACKGROUND: - bgRes = R.drawable.bg_swipe_item_left; - break; - case DRAWABLE_SWIPE_RIGHT_BACKGROUND: - if (label.getType() == Label.Type.TRASH) { - bgRes = R.drawable.bg_swipe_item_neutral; - } else { - bgRes = R.drawable.bg_swipe_item_right; - } - break; - } + } else { + bgRes = R.drawable.bg_swipe_item_right; + } + break; } holder.itemView.setBackgroundResource(bgRes); } @Override - public SwipeResultAction onSwipeItem(MyViewHolder holder, final int position, int result) { + public SwipeResultAction onSwipeItem(ViewHolder holder, final int position, int result) { switch (result) { // swipe right case RESULT_SWIPED_RIGHT: diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Strings.java b/app/src/main/java/ch/dissem/apps/abit/util/Strings.java new file mode 100644 index 0000000..7fef838 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Strings.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.util; + +import java.util.regex.Pattern; + +/** + * @author Christian Basler + */ +public class Strings { + private final static Pattern WHITESPACES = Pattern.compile("\\s+"); + + public static String normalizeWhitespaces(CharSequence string) { + string = string.subSequence(0, Math.min(string.length(), 200)); + return WHITESPACES.matcher(string).replaceAll(" "); + } +} diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index 5e9d6a7..486df04 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -26,15 +26,16 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@drawable/bg_item_normal_state" android:clickable="true" android:foreground="?attr/selectableItemBackground" - android:background="@drawable/bg_item_normal_state" tools:ignore="UselessParent"> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground"> <ImageView android:id="@+id/avatar" @@ -48,8 +49,9 @@ <TextView android:id="@+id/sender" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_alignParentEnd="true" android:layout_alignTop="@+id/avatar" android:layout_marginTop="-5dp" android:layout_toEndOf="@+id/avatar" @@ -66,8 +68,9 @@ <TextView android:id="@+id/subject" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_alignParentEnd="true" android:layout_below="@+id/sender" android:layout_toEndOf="@+id/avatar" android:ellipsize="end" @@ -79,8 +82,9 @@ <TextView android:id="@+id/text" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_alignParentEnd="true" android:layout_below="@+id/subject" android:layout_toEndOf="@+id/avatar" android:ellipsize="end" From c1d5af003459c78c76397cbeed8d8fc3549fd4de Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 23 Sep 2016 16:30:17 +0200 Subject: [PATCH 061/110] Updated support library versions --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 364f4d3..6682841 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,9 +32,9 @@ android { ext.jabitVersion = 'development-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:24.2.0' - compile 'com.android.support:support-v4:24.2.0' - compile 'com.android.support:design:24.2.0' + compile 'com.android.support:appcompat-v7:24.2.1' + compile 'com.android.support:support-v4:24.2.1' + compile 'com.android.support:design:24.2.1' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" From 246649d0287c5cc36e2f1d3c8ff57e6756d21695 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 23 Sep 2016 16:33:19 +0200 Subject: [PATCH 062/110] Added info on supporting development to preferences screen --- app/src/main/res/values-de/strings.xml | 5 ++++- app/src/main/res/values/strings.xml | 3 +++ app/src/main/res/xml/preferences.xml | 24 ++++++++++++++++-------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9068551..96c6bdf 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -78,7 +78,10 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="confirm_export">Identität wirklich exportieren? Der Export wird die privaten Schlüssel unverschlüsselt enthalten.</string> <string name="compose_message">Schreiben</string> <string name="passphrase">Passphrase</string> + <string name="help_out">Entwicklung unterstützen</string> + <string name="help_out_summary">Erfahre wie du die Entwicklung von Abit unterstützen kannst</string> + <string name="help_out_link">https://dissem.github.io/Abit/mithelfen</string> <string name="toast_chan_created">Chan erstellt</string> <string name="toast_long_running_operation">Dies kann einige Minuten dauern</string> <string name="toast_identity_created">Identität erstellt</string> -</resources> \ No newline at end of file +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6dc6dca..31791da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,6 +81,9 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="confirm_export">Do you really want to export your identity? The export will contain the unencrypted private keys.</string> <string name="compose_message">Compose</string> <string name="passphrase">passphrase</string> + <string name="help_out">Support Development</string> + <string name="help_out_summary">Want to help out? Have a look here</string> + <string name="help_out_link">https://dissem.github.io/Abit/helping-out</string> <string name="toast_long_running_operation">This may take a few minutes</string> <string name="toast_identity_created">Identity created</string> <string name="toast_chan_created">Chan created</string> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b04f110..85568c0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -12,33 +12,41 @@ android:defaultValue="true" android:key="wifi_only" android:summary="@string/wifi_only_summary" - android:title="@string/wifi_only" /> + android:title="@string/wifi_only"/> <EditTextPreference android:inputType="textUri" android:key="trusted_node" android:summary="@string/trusted_node_summary" - android:title="@string/trusted_node" /> + android:title="@string/trusted_node"/> <EditTextPreference android:defaultValue="120" android:inputType="number" android:key="sync_timeout" android:summary="@string/sync_timeout_summary" - android:title="@string/sync_timeout" /> + android:title="@string/sync_timeout"/> <SwitchPreference android:defaultValue="false" - android:key="server_pow" android:dependency="trusted_node" - android:title="@string/server_pow" + android:key="server_pow" android:summary="@string/server_pow_summary" + android:title="@string/server_pow" /> <Preference android:key="about" - android:title="@string/about" android:summary="@string/about_summary" + android:title="@string/about" /> + <Preference + android:key="help_out" + android:summary="@string/help_out_summary" + android:title="@string/help_out"> + <intent + android:action="android.intent.action.VIEW" + android:data="@string/help_out_link"/> + </Preference> <Preference android:key="status" - android:title="@string/status" android:summary="@string/status_summary" + android:title="@string/status" /> -</PreferenceScreen> \ No newline at end of file +</PreferenceScreen> From a18ef1ac29d50d7fa68c3481db4b87057e873348 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 1 Oct 2016 10:33:11 +0200 Subject: [PATCH 063/110] Refactored adding new identities - add deterministic identities - import existing identities --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 23 ++- .../apps/abit/AddressDetailFragment.java | 78 ++++---- .../apps/abit/ImportIdentitiesFragment.java | 87 +++++++++ .../apps/abit/ImportIdentityActivity.java | 56 ++++++ .../ch/dissem/apps/abit/InputWifFragment.java | 122 +++++++++++++ .../ch/dissem/apps/abit/MainActivity.java | 110 ++---------- .../abit/adapter/AddressSelectorAdapter.java | 109 +++++++++++ .../dialog/AddIdentityDialogFragment.java | 169 ++++++++++++++++++ .../DeterministicIdentityDialogFragment.java | 138 ++++++++++++++ .../main/res/drawable/ic_action_open_file.xml | 25 +++ .../dialog_add_deterministic_identity.xml | 118 ++++++++++++ .../main/res/layout/dialog_add_identity.xml | 104 +++++++++++ .../res/layout/dialog_input_passphrase.xml | 8 +- .../main/res/layout/fragment_import_input.xml | 60 +++++++ .../fragment_import_select_identities.xml | 58 ++++++ .../main/res/layout/select_identity_row.xml | 52 ++++++ app/src/main/res/menu/import_input_data.xml | 25 +++ app/src/main/res/values-de/strings.xml | 17 +- app/src/main/res/values/strings.xml | 17 +- app/src/main/res/values/styles.xml | 3 + build.gradle | 2 +- 22 files changed, 1249 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java create mode 100644 app/src/main/res/drawable/ic_action_open_file.xml create mode 100644 app/src/main/res/layout/dialog_add_deterministic_identity.xml create mode 100644 app/src/main/res/layout/dialog_add_identity.xml create mode 100644 app/src/main/res/layout/fragment_import_input.xml create mode 100644 app/src/main/res/layout/fragment_import_select_identities.xml create mode 100644 app/src/main/res/layout/select_identity_row.xml create mode 100644 app/src/main/res/menu/import_input_data.xml diff --git a/app/build.gradle b/app/build.gradle index 6682841..1865a08 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,9 +61,11 @@ dependencies { compile ('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar'){ transitive=true } + compile 'com.github.angads25:filepicker:1.0.6' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' + compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha8' } idea.module { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a868652..90f7d6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,8 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:replace="android:allowBackup"> <activity android:name=".MainActivity" android:label="@string/app_name"> @@ -103,6 +104,26 @@ <category android:name="android.intent.category.BROWSABLE"/> </intent-filter> </activity> + <activity + android:name=".ImportIdentityActivity" + android:label="@string/title_import_identity" + android:parentActivityName=".MainActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".MainActivity"/> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + + <data + android:host="*" + android:mimeType="*/*" + android:pathPattern=".*\\.dat" + android:scheme="file"/> + + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + </intent-filter> + </activity> <service android:name=".service.BitmessageService"/> <service android:name=".service.ProofOfWorkService"/> diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 5c06208..08760d6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -106,8 +106,8 @@ public class AddressDetailFragment extends Fragment { Drawables.addIcon(getActivity(), menu, R.id.share, GoogleMaterial.Icon.gmd_share); Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); Drawables.addIcon(getActivity(), menu, R.id.export, - CommunityMaterial.Icon.cmd_export) - .setVisible(item != null && item.getPrivateKey() != null); + CommunityMaterial.Icon.cmd_export) + .setVisible(item != null && item.getPrivateKey() != null); super.onCreateOptionsMenu(menu, inflater); } @@ -130,41 +130,45 @@ public class AddressDetailFragment extends Fragment { else warning = R.string.delete_contact_warning; new AlertDialog.Builder(ctx) - .setMessage(warning) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Singleton.getAddressRepository(ctx).remove(item); - item = null; - ctx.onBackPressed(); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + .setMessage(warning) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Singleton.getAddressRepository(ctx).remove(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (item.getPrivateKey() != null && mainActivity != null) { + mainActivity.removeIdentityEntry(item); + } + item = null; + ctx.onBackPressed(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); return true; } case R.id.export: { new AlertDialog.Builder(ctx) - .setMessage(R.string.confirm_export) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_TITLE, item + - EXPORT_POSTFIX); - WifExporter exporter = new WifExporter(Singleton - .getBitmessageContext(ctx)); - exporter.addIdentity(item); - shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString - ()); - startActivity(Intent.createChooser(shareIntent, null)); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + .setMessage(R.string.confirm_export) + .setPositiveButton(android.R.string.yes, new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TITLE, item + + EXPORT_POSTFIX); + WifExporter exporter = new WifExporter(Singleton + .getBitmessageContext(ctx)); + exporter.addIdentity(item); + shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString + ()); + startActivity(Intent.createChooser(shareIntent, null)); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); return true; } case R.id.share: { @@ -208,7 +212,7 @@ public class AddressDetailFragment extends Fragment { address.setText(item.getAddress()); address.setSelected(true); ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity() - .getString(R.string.stream_number, item.getStream())); + .getString(R.string.stream_number, item.getStream())); if (item.getPrivateKey() == null) { Switch active = (Switch) rootView.findViewById(R.id.active); active.setChecked(item.isSubscribed()); @@ -220,12 +224,12 @@ public class AddressDetailFragment extends Fragment { }); ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id - .pubkey_available); + .pubkey_available); if (item.getPubkey() == null) { pubkeyAvailableImg.setAlpha(0.3f); TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id - .pubkey_available_desc); + .pubkey_available_desc); pubkeyAvailableDesc.setText(R.string.pubkey_not_available); } } else { @@ -251,7 +255,7 @@ public class AddressDetailFragment extends Fragment { BitMatrix result; try { result = new MultiFormatWriter().encode(link.toString(), - BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); + BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); } catch (WriterException e) { LOG.error(e.getMessage(), e); return null; diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java new file mode 100644 index 0000000..f3e91a3 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator; + +import java.io.IOException; + +import ch.dissem.apps.abit.adapter.AddressSelectorAdapter; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.wif.WifImporter; + +/** + * @author Christian Basler + */ + +public class ImportIdentitiesFragment extends Fragment { + public static final String WIF_DATA = "wif_data"; + private BitmessageContext bmc; + private RecyclerView recyclerView; + private LinearLayoutManager layoutManager; + private AddressSelectorAdapter adapter; + private WifImporter importer; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + String wifData = getArguments().getString(WIF_DATA); + bmc = Singleton.getBitmessageContext(getActivity()); + View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false); + try { + importer = new WifImporter(bmc, wifData); + adapter = new AddressSelectorAdapter(importer.getIdentities()); + layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, + false); + recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(adapter); + + recyclerView.addItemDecoration(new SimpleListDividerDecorator( + ContextCompat.getDrawable(getActivity(), R.drawable.list_divider_h), true)); + } catch (IOException e) { + return super.onCreateView(inflater, container, savedInstanceState); + } + view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + importer.importAll(adapter.getSelected()); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress selected : adapter.getSelected()) { + mainActivity.addIdentityEntry(selected); + } + } + getActivity().finish(); + } + }); + return view; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java new file mode 100644 index 0000000..725ae7d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.os.Bundle; + +import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; + +/** + * @author Christian Basler + */ + +public class ImportIdentityActivity extends DetailActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String wifData; + if (savedInstanceState == null) { + wifData = null; + } else { + wifData = savedInstanceState.getString(WIF_DATA); + } + if (wifData == null) { + getFragmentManager().beginTransaction() + .replace(R.id.content, new InputWifFragment()) + .commit(); + } else { + Bundle bundle = new Bundle(); + bundle.putString(WIF_DATA, wifData); + + ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); + fragment.setArguments(bundle); + + getFragmentManager().beginTransaction() + .replace(R.id.content, fragment) + .commit(); + } + } + +} diff --git a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java new file mode 100644 index 0000000..8ccdf5d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.angads25.filepicker.controller.DialogSelectionListener; +import com.github.angads25.filepicker.model.DialogConfigs; +import com.github.angads25.filepicker.model.DialogProperties; +import com.github.angads25.filepicker.view.FilePickerDialog; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; + +/** + * @author Christian Basler + */ + +public class InputWifFragment extends Fragment { + private TextView wifData; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_import_input, container, false); + wifData = (TextView) view.findViewById(R.id.wif_input); + + view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle bundle = new Bundle(); + bundle.putString(WIF_DATA, wifData.getText().toString()); + + ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); + fragment.setArguments(bundle); + + getFragmentManager().beginTransaction() + .replace(R.id.content, fragment) + .commit(); + } + }); + return view; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.import_input_data, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + DialogProperties properties = new DialogProperties(); + properties.selection_mode = DialogConfigs.SINGLE_MODE; + properties.selection_type = DialogConfigs.FILE_SELECT; + properties.root = new File(DialogConfigs.DEFAULT_DIR); + properties.error_dir = new File(DialogConfigs.DEFAULT_DIR); + properties.extensions = null; + FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties); + dialog.setTitle(getString(R.string.select_file_title)); + dialog.setDialogSelectionListener(new DialogSelectionListener() { + @Override + public void onSelectedFilePaths(String[] files) { + if (files.length > 0) { + try (InputStream in = new FileInputStream(files[0])) { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + //noinspection ConstantConditions + while ((length = in.read(buffer)) != -1) { + data.write(buffer, 0, length); + } + wifData.setText(data.toString("UTF-8")); + } catch (IOException e) { + Toast.makeText( + getActivity(), + R.string.error_loading_data, + Toast.LENGTH_SHORT + ).show(); + } + } + } + }); + dialog.show(); + return true; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 5233403..1cc4bea 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -16,7 +16,6 @@ package ch.dissem.apps.abit; -import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; @@ -24,7 +23,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Point; -import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.Fragment; @@ -34,8 +32,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.targets.Target; @@ -65,6 +61,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; @@ -75,7 +72,6 @@ import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.Label; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -143,6 +139,7 @@ public class MainActivity extends AppCompatActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + instance = new WeakReference<>(this); bmc = Singleton.getBitmessageContext(this); List<Label> labels = bmc.messages().getLabels(); if (selectedLabel == null) { @@ -261,15 +258,6 @@ public class MainActivity extends AppCompatActivity .colorRes(R.color.icons)) .withIdentifier(ADD_IDENTITY) ); - profiles.add(new ProfileSettingDrawerItem() - .withName(getString(R.string.add_chan)) - .withDescription(getString(R.string.add_chan_summary)) - .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) - .actionBar() - .paddingDp(5) - .colorRes(R.color.icons)) - .withIdentifier(ADD_CHAN) - ); profiles.add(new ProfileSettingDrawerItem() .withName(getString(R.string.manage_identity)) .withIcon(GoogleMaterial.Icon.gmd_settings) @@ -288,9 +276,6 @@ public class MainActivity extends AppCompatActivity case ADD_IDENTITY: addIdentityDialog(); break; - case ADD_CHAN: - addChanDialog(); - break; case MANAGE_IDENTITY: Intent show = new Intent(MainActivity.this, AddressDetailActivity.class); @@ -423,102 +408,45 @@ public class MainActivity extends AppCompatActivity } private void addIdentityDialog() { - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.add_identity_warning) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - Toast.makeText(MainActivity.this, - R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Void, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(MainActivity.this, - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - addIdentityEntry(chan); - } - }.execute(); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); - } - - private void addChanDialog() { - @SuppressLint("InflateParams") - final View dialogView = getLayoutInflater().inflate(R.layout.dialog_input_passphrase, null); - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.add_chan) - .setView(dialogView) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - Toast.makeText(MainActivity.this, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<String, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(String... args) { - String pass = args[0]; - BitmessageAddress chan = bmc.createChan(pass); - chan.setAlias(pass); - bmc.addresses().save(chan); - return chan; - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(MainActivity.this, - R.string.toast_chan_created, - Toast.LENGTH_SHORT).show(); - addIdentityEntry(chan); - } - }.execute(passphrase.getText().toString()); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); + AddIdentityDialogFragment dialog = new AddIdentityDialogFragment(); + dialog.show(getSupportFragmentManager(), "dialog"); } @Override protected void onResume() { - instance = new WeakReference<>(this); updateUnread(); super.onResume(); } - private void addIdentityEntry(BitmessageAddress identity) { - IProfile newProfile = new - ProfileDrawerItem() + public void addIdentityEntry(BitmessageAddress identity) { + IProfile newProfile = new ProfileDrawerItem() + .withIcon(new Identicon(identity)) .withName(identity.toString()) + .withNameShown(true) .withEmail(identity.getAddress()) .withTag(identity); if (accountHeader.getProfiles() != null) { - // we know that there are 3 setting + // we know that there are 2 setting // elements. // Set the new profile above them ;) accountHeader.addProfile( newProfile, accountHeader .getProfiles().size() - - 3); + - 2); } else { accountHeader.addProfiles(newProfile); } } - @Override - protected void onPause() { - super.onPause(); - instance = null; + public void removeIdentityEntry(BitmessageAddress identity) { + for (IProfile profile : accountHeader.getProfiles()) { + if (profile instanceof ProfileDrawerItem) { + if (identity.equals(((ProfileDrawerItem) profile).getTag())) { + accountHeader.removeProfile(profile); + return; + } + } + } } private void checkAndStartNode(final CompoundButton buttonView) { diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java new file mode 100644 index 0000000..cf23ed7 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.adapter; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.BitmessageAddress; + +/** + * @author Christian Basler + */ + +public class AddressSelectorAdapter + extends RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder> { + + private final List<Selectable<BitmessageAddress>> data; + + public AddressSelectorAdapter(List<BitmessageAddress> identities) { + data = new ArrayList<>(identities.size()); + for (BitmessageAddress identity : identities) { + data.add(new Selectable<>(identity)); + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final View v = inflater.inflate(R.layout.select_identity_row, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Selectable<BitmessageAddress> selectable = data.get(position); + holder.data = selectable; + holder.checkbox.setChecked(selectable.selected); + holder.checkbox.setText(selectable.data.toString()); + holder.address.setText(selectable.data.getAddress()); + } + + @Override + public int getItemCount() { + return data.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + public Selectable<BitmessageAddress> data; + public CheckBox checkbox; + public TextView address; + + private ViewHolder(View v) { + super(v); + checkbox = (CheckBox) v.findViewById(R.id.checkbox); + address = (TextView) v.findViewById(R.id.address); + checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (data != null) { + data.selected = isChecked; + } + } + }); + } + } + + private static class Selectable<T> { + private final T data; + private boolean selected = false; + + private Selectable(T data) { + this.data = data; + } + } + + public List<BitmessageAddress> getSelected() { + List<BitmessageAddress> result = new LinkedList<>(); + for (Selectable<BitmessageAddress> selectable : data) { + if (selectable.selected) { + result.add(selectable.data); + } + } + return result; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java new file mode 100644 index 0000000..56f0ee6 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java @@ -0,0 +1,169 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.dialog; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import ch.dissem.apps.abit.ImportIdentityActivity; +import ch.dissem.apps.abit.MainActivity; +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; + +import static android.app.Activity.RESULT_OK; + +/** + * @author Christian Basler + */ + +public class AddIdentityDialogFragment extends AppCompatDialogFragment { + private static final int IMPORT_IDENTITY_RESULT_CODE = 1; + private BitmessageContext bmc; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + bmc = Singleton.getBitmessageContext(context); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + getDialog().setTitle(R.string.add_identity); + View view = inflater.inflate(R.layout.dialog_add_identity, container, false); + view.findViewById(R.id.create_identity) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + Toast.makeText(getActivity(), + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); + } + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(getActivity(), + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); + } + } + }.execute(); + } + }); + view.findViewById(R.id.import_identity) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + startActivity(new Intent(getActivity(), ImportIdentityActivity.class)); + } + }); + view.findViewById(R.id.add_deterministic_address) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + new DeterministicIdentityDialogFragment().show(getFragmentManager(), "dialog"); + } + }); + view.findViewById(R.id.add_chan) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + addChanDialog(); + } + }); + view.findViewById(R.id.dismiss) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return view; + } + + private void addChanDialog() { + @SuppressLint("InflateParams") + final View dialogView = getActivity().getLayoutInflater() + .inflate(R.layout.dialog_input_passphrase, null); + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.add_chan) + .setView(dialogView) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + Toast.makeText(getActivity(), R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<String, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(String... args) { + String pass = args[0]; + BitmessageAddress chan = bmc.createChan(pass); + chan.setAlias(pass); + bmc.addresses().save(chan); + return chan; + } + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(getActivity(), + R.string.toast_chan_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); + } + } + }.execute(passphrase.getText().toString()); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + @Override + public int getTheme() { + return R.style.FixedDialog; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java new file mode 100644 index 0000000..9cad6ba --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.dialog; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.List; + +import ch.dissem.apps.abit.MainActivity; +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; + +/** + * @author Christian Basler + */ + +public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment { + private BitmessageContext bmc; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + bmc = Singleton.getBitmessageContext(context); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + getDialog().setTitle(R.string.add_deterministic_address); + View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false); + view.findViewById(R.id.ok) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + final Context context = getActivity().getBaseContext(); + View dialogView = getView(); + TextView label = (TextView) dialogView.findViewById(R.id.label); + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id + .number_of_identities); + Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); + + Toast.makeText(context, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Object, Void, List<BitmessageAddress>>() { + @Override + protected List<BitmessageAddress> doInBackground(Object... args) { + String label = (String) args[0]; + String pass = (String) args[1]; + int numberOfAddresses = (int) args[2]; + boolean shorter = (boolean) args[3]; + List<BitmessageAddress> identities = bmc.createDeterministicAddresses + (pass, + numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); + int i = 0; + for (BitmessageAddress identity : identities) { + i++; + if (identities.size() == 1) { + identity.setAlias(label); + } else { + identity.setAlias(label + " (" + i + ")"); + } + bmc.addresses().save(identity); + } + return identities; + } + + @Override + protected void onPostExecute(List<BitmessageAddress> identities) { + int messageRes; + if (identities.size() == 1) { + messageRes = R.string.toast_identity_created; + } else { + messageRes = R.string.toast_identities_created; + } + Toast.makeText(context, + messageRes, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress identity : identities) { + mainActivity.addIdentityEntry(identity); + } + } + } + }.execute( + label.getText().toString(), + passphrase.getText().toString(), + Integer.valueOf(numberOfAddresses.getText().toString()), + shorter.isChecked() + ); + } + }); + view.findViewById(R.id.dismiss) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + return view; + } + + @Override + public int getTheme() { + return R.style.FixedDialog; + } +} diff --git a/app/src/main/res/drawable/ic_action_open_file.xml b/app/src/main/res/drawable/ic_action_open_file.xml new file mode 100644 index 0000000..ec3a0f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_open_file.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/> +</vector> diff --git a/app/src/main/res/layout/dialog_add_deterministic_identity.xml b/app/src/main/res/layout/dialog_add_deterministic_identity.xml new file mode 100644 index 0000000..2b2fd1f --- /dev/null +++ b/app/src/main/res/layout/dialog_add_deterministic_identity.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="18dp" + android:paddingEnd="24dp" + android:paddingStart="24dp" + android:paddingTop="18dp"> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/deterministic_address_warning" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintTop_creator="1"/> + + <android.support.design.widget.TextInputLayout + android:id="@+id/label_wrapper" + android:layout_marginTop="24dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@id/description"> + + <EditText + android:id="@+id/label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/label"/> + + </android.support.design.widget.TextInputLayout> + + <android.support.design.widget.TextInputLayout + android:id="@+id/passphrase_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@id/label_wrapper"> + + <EditText + android:id="@+id/passphrase" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/passphrase" + android:inputType="textMultiLine"/> + + </android.support.design.widget.TextInputLayout> + + <android.support.design.widget.TextInputLayout + android:id="@+id/number_of_identities_wrapper" + android:layout_width="312dp" + android:layout_height="wrap_content" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@id/passphrase_wrapper"> + + <EditText + android:id="@+id/number_of_identities" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:hint="@string/number_of_identities" + android:inputType="number" + android:text="1"/> + + </android.support.design.widget.TextInputLayout> + + <Switch + android:id="@+id/shorter" + android:layout_width="312dp" + android:layout_height="wrap_content" + android:text="@string/shorter" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@id/number_of_identities_wrapper"/> + + <Button + android:id="@+id/ok" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="@string/ok" + android:textColor="@color/colorAccent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/shorter"/> + + <Button + android:id="@+id/dismiss" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/cancel" + android:textColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="@id/ok" + app:layout_constraintRight_toLeftOf="@id/ok"/> + +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/dialog_add_identity.xml b/app/src/main/res/layout/dialog_add_identity.xml new file mode 100644 index 0000000..2d62579 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_identity.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:paddingEnd="24dp" + android:paddingStart="24dp" + android:paddingTop="18dp" + android:paddingBottom="18dp" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/add_identity_warning" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/create_identity" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/create_identity" + android:textColor="@color/colorAccent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintRight_toRightOf="@+id/description" + app:layout_constraintTop_toBottomOf="@+id/description" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintRight_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/import_identity" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/import_identity" + android:textColor="@color/colorAccent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintRight_toRightOf="@+id/create_identity" + app:layout_constraintTop_toBottomOf="@+id/create_identity" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintRight_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/add_deterministic_address" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/add_deterministic_address" + android:textColor="@color/colorAccent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintRight_toRightOf="@+id/import_identity" + app:layout_constraintTop_toBottomOf="@+id/import_identity" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintRight_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/add_chan" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/add_chan" + android:textColor="@color/colorAccent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintRight_toRightOf="@+id/add_deterministic_address" + app:layout_constraintTop_toBottomOf="@+id/add_deterministic_address" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintRight_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/dismiss" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/cancel" + android:textColor="@color/colorAccent" + app:layout_constraintLeft_toLeftOf="@+id/description" + app:layout_constraintTop_toBottomOf="@+id/add_deterministic_address"/> +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/dialog_input_passphrase.xml b/app/src/main/res/layout/dialog_input_passphrase.xml index db9c2ae..c70852f 100644 --- a/app/src/main/res/layout/dialog_input_passphrase.xml +++ b/app/src/main/res/layout/dialog_input_passphrase.xml @@ -2,7 +2,11 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + android:paddingBottom="18dp" + android:paddingLeft="24dp" + android:paddingRight="24dp" + android:paddingTop="18dp"> <EditText android:id="@+id/passphrase" @@ -11,4 +15,4 @@ android:layout_gravity="center_horizontal" android:hint="@string/passphrase" android:inputType="textMultiLine"/> -</LinearLayout> \ No newline at end of file +</LinearLayout> diff --git a/app/src/main/res/layout/fragment_import_input.xml b/app/src/main/res/layout/fragment_import_input.xml new file mode 100644 index 0000000..5df2514 --- /dev/null +++ b/app/src/main/res/layout/fragment_import_input.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="64dp" + android:layout_marginEnd="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp"> + + <TextView + android:id="@+id/description" + android:layout_width="360dp" + android:layout_height="wrap_content" + android:text="@string/import_input_description" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + + <EditText + android:id="@+id/wif_input" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="24dp" + android:gravity="start|top" + android:hint="@string/wif_string" + android:inputType="textMultiLine|text" + app:layout_constraintBottom_toTopOf="@+id/next" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/description"/> + + <Button + android:id="@+id/next" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:text="@string/next" + android:textColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintRight_toRightOf="parent"/> + +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_import_select_identities.xml b/app/src/main/res/layout/fragment_import_select_identities.xml new file mode 100644 index 0000000..2c040e3 --- /dev/null +++ b/app/src/main/res/layout/fragment_import_select_identities.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="64dp" + android:layout_marginEnd="16dp" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp"> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/select_identities_to_import" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent"/> + + <android.support.v7.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="24dp" + android:inputType="textMultiLine|text" + app:layout_constraintBottom_toTopOf="@+id/finish" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/description"/> + + <Button + android:id="@+id/finish" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:text="@string/do_import" + android:textColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintRight_toRightOf="parent"/> + +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/select_identity_row.xml b/app/src/main/res/layout/select_identity_row.xml new file mode 100644 index 0000000..8324c5f --- /dev/null +++ b/app/src/main/res/layout/select_identity_row.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="64dp"> + + <CheckBox + android:id="@+id/checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:ellipsize="end" + android:paddingBottom="8dp" + android:paddingEnd="8dp" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:text="CheckBox" + android:textAppearance="?android:attr/textAppearanceMedium" + tools:text="Name"/> + + <TextView + android:id="@+id/address" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentBottom="true" + android:ellipsize="marquee" + android:lines="1" + android:paddingBottom="8dp" + android:paddingEnd="8dp" + android:paddingStart="48dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="BM-2cW0000000000000000000000000000000"/> + +</RelativeLayout> diff --git a/app/src/main/res/menu/import_input_data.xml b/app/src/main/res/menu/import_input_data.xml new file mode 100644 index 0000000..9e8665b --- /dev/null +++ b/app/src/main/res/menu/import_input_data.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/open_file" + android:title="@string/open_file" + android:icon="@drawable/ic_action_open_file" + app:showAsAction="ifRoom"/> +</menu> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9068551..66ebbe9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -81,4 +81,19 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="toast_chan_created">Chan erstellt</string> <string name="toast_long_running_operation">Dies kann einige Minuten dauern</string> <string name="toast_identity_created">Identität erstellt</string> -</resources> \ No newline at end of file + <string name="toast_identities_created">Identitäten erstellt</string> + <string name="import_identity">Importieren</string> + <string name="create_identity">Erstellen</string> + <string name="add_deterministic_address">Deterministische Identität</string> + <string name="deterministic_address_warning">Merke dir diese Enstellungen und stelle sicher dass sie korrekt sind wenn du eine deterministische Addresse wiederherstellst.</string> + <string name="shorter">Kürzere Adressen suchen</string> + <string name="number_of_identities">Anzahl zu generierender Identitäten</string> + <string name="title_import_identity">Identität importieren</string> + <string name="wif_string">WIF / Inhalt von \'keys.dat\'</string> + <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> + <string name="select_file_title">Datei auswählen</string> + <string name="open_file">Datei öffnen</string> + <string name="next">Weiter</string> + <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer \'keys.dat\'-Datei einfügen</string> + <string name="error_loading_data">Fehler beim Laden der Daten</string> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6dc6dca..5f825dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,9 @@ <string name="manage_identity">Manage Identity</string> <string name="add_identity">Add Identity</string> <string name="add_identity_summary">Create new identity</string> + <string name="create_identity">Create new</string> + <string name="import_identity">Import existing</string> + <string name="add_deterministic_address">Deterministic identity</string> <string name="add_chan">Add Chan</string> <string name="add_chan_summary">Add or create a chan</string> <string name="title_activity_open_bitmessage_link">Import Contact</string> @@ -80,8 +83,20 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="export">Export</string> <string name="confirm_export">Do you really want to export your identity? The export will contain the unencrypted private keys.</string> <string name="compose_message">Compose</string> - <string name="passphrase">passphrase</string> + <string name="passphrase">Passphrase</string> <string name="toast_long_running_operation">This may take a few minutes</string> <string name="toast_identity_created">Identity created</string> + <string name="toast_identities_created">Identities created</string> <string name="toast_chan_created">Chan created</string> + <string name="deterministic_address_warning">Be sure to remember those settings correctly when recreating a deterministic address.</string> + <string name="number_of_identities">Number of identities to create</string> + <string name="shorter">Search for shorter addresses</string> + <string name="wif_string">WIF / contents of \'keys.dat\'</string> + <string name="next">Continue</string> + <string name="title_import_identity">Import Identity</string> + <string name="open_file">Open File</string> + <string name="error_loading_data">Error loading data</string> + <string name="select_file_title">Select a File</string> + <string name="select_identities_to_import">Please select the identities you want to import:</string> + <string name="import_input_description">You can just paste the contents of an export or a \'keys.dat\' file</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a299e46..02ec048 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,4 +19,7 @@ <item name="android:textColor">@color/colorAccent</item> </style> + <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog"> + <item name="windowNoTitle">false</item> + </style> </resources> diff --git a/build.gradle b/build.gradle index de60887..c9dbf0f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.3' + classpath 'com.android.tools.build:gradle:2.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 34000e7b790eb48ed6100b6c7554531043158679 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 1 Oct 2016 17:40:38 +0200 Subject: [PATCH 064/110] Some layout improvements and fixes --- .../dissem/apps/abit/MessageListFragment.java | 11 ++-- .../abit/adapter/AddressSelectorAdapter.java | 10 ---- .../dissem/apps/abit/adapter/Selectable.java | 29 ++++++++++ .../abit/adapter/SwipeableMessageAdapter.java | 31 +++++++++-- .../bg_swipe_item_left.xml | 0 .../bg_swipe_item_right.xml | 0 .../res/drawable/bg_item_selected_state.xml | 21 ++++++++ .../layout-w720dp/activity_message_list.xml | 53 ++++++++++--------- .../main/res/layout/fragment_message_list.xml | 4 +- app/src/main/res/values/colors.xml | 2 + 10 files changed, 118 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/Selectable.java rename app/src/main/res/{drawable-anydpi-v21 => drawable-v21}/bg_swipe_item_left.xml (100%) rename app/src/main/res/{drawable-anydpi-v21 => drawable-v21}/bg_swipe_item_right.xml (100%) create mode 100644 app/src/main/res/drawable/bg_item_selected_state.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index bf79b0d..9328ce2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -70,7 +70,7 @@ public class MessageListFragment extends Fragment implements ListHolder { private Label currentLabel; private MenuItem emptyTrashMenuItem; private MessageRepository messageRepo; - private List<Plaintext> messages; + private boolean activateOnItemClick; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -105,7 +105,7 @@ public class MessageListFragment extends Fragment implements ListHolder { } private void doUpdateList(Label label) { - messages = Singleton.getMessageRepository(getContext()).findMessages(label); + List<Plaintext> messages = Singleton.getMessageRepository(getContext()).findMessages(label); if (getActivity() instanceof ActionBarListener) { if (label != null) { ((ActionBarListener) getActivity()).updateTitle(label.toString()); @@ -153,6 +153,7 @@ public class MessageListFragment extends Fragment implements ListHolder { //adapter adapter = new SwipeableMessageAdapter(); + adapter.setActivateOnItemClick(activateOnItemClick); adapter.setEventListener(new SwipeableMessageAdapter.EventListener() { @Override public void onItemDeleted(Plaintext item) { @@ -174,6 +175,7 @@ public class MessageListFragment extends Fragment implements ListHolder { @Override public void onItemViewClicked(View v, boolean pinned) { int position = recyclerView.getChildAdapterPosition(v); + adapter.setSelectedPosition(position); if (position != RecyclerView.NO_POSITION) { Plaintext item = adapter.getItem(position); ((MainActivity) getActivity()).onItemSelected(item); @@ -263,6 +265,9 @@ public class MessageListFragment extends Fragment implements ListHolder { @Override public void setActivateOnItemClick(boolean activateOnItemClick) { - // TODO + if (adapter != null) { + adapter.setActivateOnItemClick(activateOnItemClick); + } + this.activateOnItemClick = activateOnItemClick; } } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java index cf23ed7..4103097 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java @@ -34,7 +34,6 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; /** * @author Christian Basler */ - public class AddressSelectorAdapter extends RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder> { @@ -88,15 +87,6 @@ public class AddressSelectorAdapter } } - private static class Selectable<T> { - private final T data; - private boolean selected = false; - - private Selectable(T data) { - this.data = data; - } - } - public List<BitmessageAddress> getSelected() { List<BitmessageAddress> result = new LinkedList<>(); for (Selectable<BitmessageAddress> selectable : data) { diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/Selectable.java b/app/src/main/java/ch/dissem/apps/abit/adapter/Selectable.java new file mode 100644 index 0000000..ddaaf4c --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/Selectable.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.adapter; + +/** + * @author Christian Basler + */ +class Selectable<T> { + final T data; + boolean selected = false; + + Selectable(T data) { + this.data = data; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index 843266a..a0f9423 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -17,6 +17,7 @@ package ch.dissem.apps.abit.adapter; +import android.annotation.SuppressLint; import android.graphics.Typeface; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -50,7 +51,7 @@ import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; * * @author Christian Basler * @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview"> - * https://github.com/h6ah4i/android-advancedrecyclerview</a> + * https://github.com/h6ah4i/android-advancedrecyclerview</a> */ public class SwipeableMessageAdapter extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder> @@ -62,6 +63,12 @@ public class SwipeableMessageAdapter private View.OnClickListener swipeableViewContainerOnClickListener; private Label label; + private int selectedPosition; + private boolean activateOnItemClick; + + public void setActivateOnItemClick(boolean activateOnItemClick) { + this.activateOnItemClick = activateOnItemClick; + } public interface EventListener { void onItemDeleted(Plaintext item); @@ -71,14 +78,15 @@ public class SwipeableMessageAdapter void onItemViewClicked(View v, boolean pinned); } - public static class ViewHolder extends AbstractSwipeableItemViewHolder { + @SuppressWarnings("WeakerAccess") + static class ViewHolder extends AbstractSwipeableItemViewHolder { public FrameLayout container; public final ImageView avatar; public final TextView sender; public final TextView subject; public final TextView extract; - public ViewHolder(View v) { + ViewHolder(View v) { super(v); container = (FrameLayout) v.findViewById(R.id.container); avatar = (ImageView) v.findViewById(R.id.avatar); @@ -150,6 +158,14 @@ public class SwipeableMessageAdapter public void onBindViewHolder(ViewHolder holder, int position) { final Plaintext item = data.get(position); + if (activateOnItemClick) { + holder.container.setBackgroundResource( + position == selectedPosition + ? R.drawable.bg_item_selected_state + : R.drawable.bg_item_normal_state + ); + } + // set listeners // (if the item is *pinned*, click event comes to the itemView) holder.itemView.setOnClickListener(itemViewOnClickListener); @@ -184,6 +200,7 @@ public class SwipeableMessageAdapter } @Override + @SuppressLint("SwitchIntDef") public void onSetSwipeBackground(ViewHolder holder, int position, int type) { int bgRes = 0; switch (type) { @@ -205,6 +222,7 @@ public class SwipeableMessageAdapter } @Override + @SuppressLint("SwitchIntDef") public SwipeResultAction onSwipeItem(ViewHolder holder, final int position, int result) { switch (result) { // swipe right @@ -223,6 +241,13 @@ public class SwipeableMessageAdapter this.eventListener = eventListener; } + public void setSelectedPosition(int selectedPosition) { + int oldPosition = this.selectedPosition; + this.selectedPosition = selectedPosition; + notifyItemChanged(oldPosition); + notifyItemChanged(selectedPosition); + } + private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { private SwipeableMessageAdapter adapter; private final int position; diff --git a/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml b/app/src/main/res/drawable-v21/bg_swipe_item_left.xml similarity index 100% rename from app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml rename to app/src/main/res/drawable-v21/bg_swipe_item_left.xml diff --git a/app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml b/app/src/main/res/drawable-v21/bg_swipe_item_right.xml similarity index 100% rename from app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml rename to app/src/main/res/drawable-v21/bg_swipe_item_right.xml diff --git a/app/src/main/res/drawable/bg_item_selected_state.xml b/app/src/main/res/drawable/bg_item_selected_state.xml new file mode 100644 index 0000000..32a0c99 --- /dev/null +++ b/app/src/main/res/drawable/bg_item_selected_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_selected_state"/> + </item> +</layer-list> diff --git a/app/src/main/res/layout-w720dp/activity_message_list.xml b/app/src/main/res/layout-w720dp/activity_message_list.xml index 8ad5fef..a4b6855 100644 --- a/app/src/main/res/layout-w720dp/activity_message_list.xml +++ b/app/src/main/res/layout-w720dp/activity_message_list.xml @@ -1,52 +1,53 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:gravity="center" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:gravity="center"> <android.support.v7.widget.Toolbar - android:id="@+id/toolbar" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" - android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - android:elevation="4dp" - tools:ignore="UnusedAttribute"/> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + android:elevation="4dp" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + tools:ignore="UnusedAttribute"/> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - android:layout_below="@id/toolbar" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" + android:layout_below="@id/toolbar" + android:background="@color/bg_item_selected_state" android:baselineAligned="false" - android:divider="?android:attr/dividerHorizontal" android:orientation="horizontal" android:showDividers="middle" - tools:context=".MessageListActivity"> + tools:context=".MainActivity"> <!-- This layout is a two-pane layout for the Messages master/detail flow. - --> <FrameLayout - android:id="@+id/item_list" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - tools:context=".MessageListActivity" - tools:layout="@android:layout/list_content"/> + android:id="@+id/item_list" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + tools:context=".MessageListActivity" + tools:layout="@layout/fragment_message_list"/> <FrameLayout - android:id="@+id/message_detail_container" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="3"/> + android:id="@+id/message_detail_container" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_margin="16dp" + android:layout_weight="2" + android:background="@color/contentBackground" + android:elevation="2dp" + tools:layout="@layout/fragment_message_detail"/> </LinearLayout> </RelativeLayout> diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index 286612c..0fe153d 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -15,7 +16,8 @@ android:paddingBottom="88dp" android:scrollbarStyle="outsideOverlay" - android:scrollbars="vertical"/> + android:scrollbars="vertical" + tools:listitem="@layout/message_row"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab_compose_message" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e36d1eb..f2f59f8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,6 +8,7 @@ <color name="colorAccent">#607D8B</color> <color name="colorPrimaryText">#212121</color> <color name="colorSecondaryText">#727272</color> + <color name="contentBackground">#FFFFFF</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color> @@ -49,6 +50,7 @@ <color name="bg_item_normal_state">#ffffffff</color> <color name="bg_item_swiping_state">@color/colorPrimaryLight</color> <color name="bg_item_swiping_active_state">@color/colorPrimary</color> + <color name="bg_item_selected_state">@color/colorPrimaryLight</color> <color name="bg_swipe_item_neutral">@android:color/transparent</color> <color name="bg_swipe_item_trash">#fff45f30</color> <color name="bg_swipe_item_archive">#fff9930d</color> From dea9231fcf64db1c9b6a718eecf4c588a5c92efc Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 2 Oct 2016 14:19:50 +0200 Subject: [PATCH 065/110] Fixed bug where two items were removed from view with one swipe --- .../ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index a0f9423..2e4775b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -299,7 +299,6 @@ public class SwipeableMessageAdapter protected void onPerformAction() { super.onPerformAction(); - adapter.data.remove(position); adapter.data.remove(position); adapter.notifyItemRemoved(position); } From bfd5a72b5281d8e818b10a6d911cf27d50b8c620 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 2 Oct 2016 15:40:18 +0200 Subject: [PATCH 066/110] Identity creation dialog improvements --- app/build.gradle | 2 +- .../dialog/AddIdentityDialogFragment.java | 94 ++++++++---------- .../dialog_add_deterministic_identity.xml | 4 +- .../main/res/layout/dialog_add_identity.xml | 98 +++++++++---------- .../main/res/layout/fragment_import_input.xml | 2 +- .../res/values-de/library_jabit_strings.xml | 5 +- app/src/main/res/values-de/strings.xml | 85 ++++++++-------- .../main/res/values/library_jabit_strings.xml | 4 +- app/src/main/res/values/strings.xml | 11 ++- 9 files changed, 150 insertions(+), 155 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1865a08..a7c4f4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,7 +65,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' - compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha8' + compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9' } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java index 56f0ee6..f404c9c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java @@ -28,6 +28,7 @@ import android.support.v7.app.AppCompatDialogFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; @@ -39,14 +40,11 @@ import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.payload.Pubkey; -import static android.app.Activity.RESULT_OK; - /** * @author Christian Basler */ public class AddIdentityDialogFragment extends AppCompatDialogFragment { - private static final int IMPORT_IDENTITY_RESULT_CODE = 1; private BitmessageContext bmc; @Override @@ -61,57 +59,49 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { savedInstanceState) { getDialog().setTitle(R.string.add_identity); View view = inflater.inflate(R.layout.dialog_add_identity, container, false); - view.findViewById(R.id.create_identity) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - Toast.makeText(getActivity(), - R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Void, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(getActivity(), - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(chan); + final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); + view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.create_identity: + Toast.makeText(getActivity(), + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); } - } - }.execute(); + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(getActivity(), + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); + } + } + }.execute(); + break; + case R.id.import_identity: + startActivity(new Intent(getActivity(), ImportIdentityActivity.class)); + break; + case R.id.add_chan: + addChanDialog(); + break; + case R.id.add_deterministic_address: + new DeterministicIdentityDialogFragment().show(getFragmentManager(), + "dialog"); + break; + default: + return; } - }); - view.findViewById(R.id.import_identity) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - startActivity(new Intent(getActivity(), ImportIdentityActivity.class)); - } - }); - view.findViewById(R.id.add_deterministic_address) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - new DeterministicIdentityDialogFragment().show(getFragmentManager(), "dialog"); - } - }); - view.findViewById(R.id.add_chan) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - addChanDialog(); - } - }); + dismiss(); + } + }); view.findViewById(R.id.dismiss) .setOnClickListener(new View.OnClickListener() { @Override diff --git a/app/src/main/res/layout/dialog_add_deterministic_identity.xml b/app/src/main/res/layout/dialog_add_deterministic_identity.xml index 2b2fd1f..5df7bfa 100644 --- a/app/src/main/res/layout/dialog_add_deterministic_identity.xml +++ b/app/src/main/res/layout/dialog_add_deterministic_identity.xml @@ -70,7 +70,7 @@ <android.support.design.widget.TextInputLayout android:id="@+id/number_of_identities_wrapper" - android:layout_width="312dp" + android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/passphrase_wrapper"> @@ -88,7 +88,7 @@ <Switch android:id="@+id/shorter" - android:layout_width="312dp" + android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/shorter" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/dialog_add_identity.xml b/app/src/main/res/layout/dialog_add_identity.xml index 2d62579..720b1fd 100644 --- a/app/src/main/res/layout/dialog_add_identity.xml +++ b/app/src/main/res/layout/dialog_add_identity.xml @@ -19,12 +19,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="18dp" android:paddingEnd="24dp" android:paddingStart="24dp" - android:paddingTop="18dp" - android:paddingBottom="18dp" - android:layout_width="match_parent" - android:layout_height="match_parent"> + android:paddingTop="18dp"> <TextView android:id="@+id/description" @@ -36,58 +36,56 @@ tools:layout_constraintLeft_creator="1" tools:layout_constraintTop_creator="1"/> - <Button - android:id="@+id/create_identity" - style="?android:attr/borderlessButtonStyle" - android:layout_width="wrap_content" + + <RadioGroup + android:id="@+id/radioGroup" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/create_identity" - android:textColor="@color/colorAccent" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/description" - app:layout_constraintTop_toBottomOf="@+id/description" - tools:layout_constraintLeft_creator="1" - tools:layout_constraintRight_creator="1" - tools:layout_constraintTop_creator="1"/> + android:paddingBottom="24dp" + android:paddingTop="24dp" + app:layout_constraintLeft_toLeftOf="@+id/description" + app:layout_constraintTop_toBottomOf="@+id/description"> + + <RadioButton + android:id="@+id/add_chan" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:text="@string/add_chan_description"/> + + <RadioButton + android:id="@+id/add_deterministic_address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:text="@string/add_deterministic_address_description"/> + + <RadioButton + android:id="@+id/import_identity" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:text="@string/import_identity_description"/> + + <RadioButton + android:id="@+id/create_identity" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/create_identity_description"/> + + </RadioGroup> + <Button - android:id="@+id/import_identity" + android:id="@+id/ok" style="?android:attr/borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/import_identity" + android:text="@string/ok" android:textColor="@color/colorAccent" app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/create_identity" - app:layout_constraintTop_toBottomOf="@+id/create_identity" - tools:layout_constraintLeft_creator="1" - tools:layout_constraintRight_creator="1" - tools:layout_constraintTop_creator="1"/> - - <Button - android:id="@+id/add_deterministic_address" - style="?android:attr/borderlessButtonStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/add_deterministic_address" - android:textColor="@color/colorAccent" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/import_identity" - app:layout_constraintTop_toBottomOf="@+id/import_identity" - tools:layout_constraintLeft_creator="1" - tools:layout_constraintRight_creator="1" - tools:layout_constraintTop_creator="1"/> - - <Button - android:id="@+id/add_chan" - style="?android:attr/borderlessButtonStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/add_chan" - android:textColor="@color/colorAccent" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/add_deterministic_address" - app:layout_constraintTop_toBottomOf="@+id/add_deterministic_address" + app:layout_constraintRight_toRightOf="@+id/radioGroup" + app:layout_constraintTop_toBottomOf="@+id/radioGroup" tools:layout_constraintLeft_creator="1" tools:layout_constraintRight_creator="1" tools:layout_constraintTop_creator="1"/> @@ -99,6 +97,6 @@ android:layout_height="wrap_content" android:text="@string/cancel" android:textColor="@color/colorAccent" - app:layout_constraintLeft_toLeftOf="@+id/description" - app:layout_constraintTop_toBottomOf="@+id/add_deterministic_address"/> + app:layout_constraintRight_toLeftOf="@+id/ok" + app:layout_constraintTop_toBottomOf="@+id/radioGroup"/> </android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_import_input.xml b/app/src/main/res/layout/fragment_import_input.xml index 5df2514..d82392f 100644 --- a/app/src/main/res/layout/fragment_import_input.xml +++ b/app/src/main/res/layout/fragment_import_input.xml @@ -27,7 +27,7 @@ <TextView android:id="@+id/description" - android:layout_width="360dp" + android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/import_input_description" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/values-de/library_jabit_strings.xml b/app/src/main/res/values-de/library_jabit_strings.xml index e7a54a1..92ee6ec 100644 --- a/app/src/main/res/values-de/library_jabit_strings.xml +++ b/app/src/main/res/values-de/library_jabit_strings.xml @@ -1,7 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="about">Über Abit</string> - <string name="about_app">Ein Bitmessage-Client für Android</string> - <string name="about_summary">Open Source Abhängigkeiten.</string> <string name="library_jabit_libraryDescription">Jabit hat das Ziel eine einfach zu benutzende Bitmessage-Bibliothek für Java-Entwickler zu sein, um schnell eigene Bitmessage-Anwendungen zu implementieren.</string> -</resources> \ No newline at end of file +</resources> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 66ebbe9..89dec00 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,73 +1,83 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Abit</string> + <string name="about_app">Ein Bitmessage-Client für Android</string> + <string name="title_message_detail">Nachricht</string> + <string name="title_subscription_detail">Abonnement</string> + <string name="bitmessage_full_node">Bitmessage Netzknoten</string> + <string name="wifi_mode">Art der WLAN-Verbindung</string> <string name="settings">Einstellungen</string> <string name="wifi_only">Nur WLAN</string> <string name="wifi_only_summary">Nicht mit Mobilfunknetz verbinden</string> - <string name="bitmessage_full_node">Bitmessage Netzknoten</string> - <string name="subject">Betreff</string> - <string name="to">An</string> - <string name="title_message_detail">Nachricht</string> <string name="subscriptions">Abonnements</string> - <string name="wifi_mode">Art der WLAN-Verbindung</string> - <string name="action_settings">Einstellungen</string> + <string name="to">An</string> + <string name="subject">Betreff</string> + <string name="manage_identity">Identität verwalten</string> <string name="add_identity">Identität hinzufügen</string> <string name="add_identity_summary">Eine neue Identität erstellen</string> + <string name="create_identity">Erstellen</string> + <string name="create_identity_description">Eine neue, zufällige Identität erstellen</string> + <string name="import_identity">Importieren</string> + <string name="import_identity_description">Eine existierende Identität von PyBitmessage oder einem Export importieren</string> + <string name="add_deterministic_address">Deterministische Identität</string> + <string name="add_deterministic_address_description">Eine deterministische Identität erstellen oder wiederherstellen</string> <string name="add_chan">Chan hinzufügen</string> - <string name="add_chan_summary">Einen Chan hinzufügen oder erstellen</string> - <string name="broadcast">Broadcast</string> - <string name="cancel">Abbrechen</string> - <string name="do_import">Importieren</string> + <string name="add_chan_description">Einen Chan erstellen oder einem beitreten</string> + <string name="title_activity_open_bitmessage_link">Kontakt importieren</string> + + <string name="action_settings">Einstellungen</string> + <string name="connection_info_1">Stream %1$d: eine Verbindung</string> + <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> <string name="import_address">Adresse importieren</string> <string name="import_contact">Kontakt hinzufügen</string> <string name="label">Label</string> - <string name="manage_identity">Identität verwalten</string> <string name="subscribe">Abonnieren</string> - <string name="title_activity_open_bitmessage_link">Kontakt importieren</string> - <string name="delete">Löschen</string> - <string name="reply">Antworten</string> + <string name="do_import">Importieren</string> + <string name="cancel">Abbrechen</string> + <string name="broadcast">Broadcast</string> <string name="n_new_messages">%d neue Nachrichten</string> + <string name="reply">Antworten</string> + <string name="delete">Löschen</string> + <string name="mark_unread">Als ungelesen markieren</string> <string name="archive">Archiv</string> <string name="empty_trash">Papierkorb leeren</string> - <string name="mark_unread">Als ungelesen markieren</string> <string name="stream_number">Stream %d</string> <string name="enabled">Aktiv</string> - <string name="title_subscription_detail">Abonnement</string> - <string name="sync_timeout">Zeitbeschränkung der Synchronisierung</string> - <string name="sync_timeout_summary">Timeout in Sekunden</string> <string name="trusted_node">Vertrauenswürdiger Knoten</string> <string name="trusted_node_summary">Diese Adresse wird für die Synchronisation verwendet</string> + <string name="sync_timeout">Zeitbeschränkung der Synchronisierung</string> + <string name="sync_timeout_summary">Timeout in Sekunden</string> + <string name="write_message">Schreiben</string> <string name="full_node">Aktiver Knoten</string> <string name="send">Senden</string> - <string name="write_message">Schreiben</string> - <string name="connection_info_1">Stream %1$d: eine Verbindung</string> - <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> <string name="connection_info_disconnected">Getrennt</string> <string name="connection_info_pending">Verbindung wird aufgebaut…</string> + <string name="proof_of_work_title">Proof of Work</string> <string name="proof_of_work_text_0">Arbeite am Versenden</string> <string name="proof_of_work_text_n">Arbeite am Versenden (%1$d in Warteschlange)</string> - <string name="proof_of_work_title">Proof of Work</string> - <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> + <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="compose_body_hint">Nachricht schreiben</string> <string name="contacts_and_subscriptions">Kontakte</string> <string name="subscribed">Abonniert</string> <string name="server_pow">Server POW</string> <string name="server_pow_summary">Der vertrauenswürdige Knoten macht den Proof of Work</string> <string name="full_node_warning">Ein aktiver Bitmessage-Knoten muss viel hoch- und herunterladen, was auf einem mobilen Netzwerk teuer sein kann. Soll tatsächlich ein aktiver Knoten gestartet werden?</string> - <string name="status">Debugging</string> - <string name="alias_default_identity">Ich</string> + <string name="about">Über Abit</string> + <string name="about_summary">Opensource Abhängigkeiten.</string> <string name="title_activity_status">Debugging</string> + <string name="status">Debugging</string> <string name="status_summary">Technische Infos</string> + <string name="alias_default_identity">Ich</string> <string name="pubkey_available">Öffentlicher Schlüssel verfügbar</string> <string name="pubkey_not_available">Öffentlicher Schlüssel noch nicht verfügbar</string> <string name="alt_qr_code">QR-Code</string> - <string name="add_identity_warning">Mehrere Identitäten zu haben bedeutet einen höheren Resourcenverbrauch. Bist Du sicher?</string> + <string name="add_identity_warning">Mehrere Identitäten zu haben bedeutet einen höheren Resourcenverbrauch. Falls Du sicher bist dass du eine Identität hinzufügen willst, wähle bitte die Vorgehensweise:</string> <string name="share">Teilen</string> - <string name="delete_contact_warning">Bist Du sicher dass dieser Kontakt gelöscht werden soll?</string> <string name="delete_identity_warning">Bist Du sicher dass diese Identität gelöscht werden soll? Nachrichten, welche an diese Adresse gesendet werden, können nicht mehr empfangen werden und es est nicht möglich diese Aktion rückgängig zu machen.</string> - <string name="create_contact">Kontakt erfassen</string> + <string name="delete_contact_warning">Bist Du sicher dass dieser Kontakt gelöscht werden soll?</string> <string name="scan_qr_code">QR-Code scannen</string> + <string name="create_contact">Kontakt erfassen</string> <string name="full_node_description">Solange kein aktiver Knoten gestartet ist, werden keine Meldungen empfangen oder gesendet. Dies braucht jedoch viele Resourcen und Daten. Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfiguriert werden, aber im Moment muss dieser selbst bereitgestellt werden.</string> @@ -78,22 +88,19 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="confirm_export">Identität wirklich exportieren? Der Export wird die privaten Schlüssel unverschlüsselt enthalten.</string> <string name="compose_message">Schreiben</string> <string name="passphrase">Passphrase</string> - <string name="toast_chan_created">Chan erstellt</string> <string name="toast_long_running_operation">Dies kann einige Minuten dauern</string> <string name="toast_identity_created">Identität erstellt</string> <string name="toast_identities_created">Identitäten erstellt</string> - <string name="import_identity">Importieren</string> - <string name="create_identity">Erstellen</string> - <string name="add_deterministic_address">Deterministische Identität</string> + <string name="toast_chan_created">Chan erstellt</string> <string name="deterministic_address_warning">Merke dir diese Enstellungen und stelle sicher dass sie korrekt sind wenn du eine deterministische Addresse wiederherstellst.</string> - <string name="shorter">Kürzere Adressen suchen</string> <string name="number_of_identities">Anzahl zu generierender Identitäten</string> - <string name="title_import_identity">Identität importieren</string> - <string name="wif_string">WIF / Inhalt von \'keys.dat\'</string> - <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> - <string name="select_file_title">Datei auswählen</string> - <string name="open_file">Datei öffnen</string> + <string name="shorter">Kürzere Adressen suchen</string> + <string name="wif_string">WIF / Inhalt von ‘keys.dat’</string> <string name="next">Weiter</string> - <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer \'keys.dat\'-Datei einfügen</string> + <string name="title_import_identity">Identität importieren</string> + <string name="open_file">Datei öffnen</string> <string name="error_loading_data">Fehler beim Laden der Daten</string> + <string name="select_file_title">Datei auswählen</string> + <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> + <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer ‘keys.dat’-Datei einfügen</string> </resources> diff --git a/app/src/main/res/values/library_jabit_strings.xml b/app/src/main/res/values/library_jabit_strings.xml index 356e88d..c12f0b6 100644 --- a/app/src/main/res/values/library_jabit_strings.xml +++ b/app/src/main/res/values/library_jabit_strings.xml @@ -16,5 +16,5 @@ <string name="library_jabit_classPath" translatable="false">ch.dissem.bitmessage.BitmessageContext</string> <!-- License section --> <string name="library_jabit_licenseId" translatable="false">apache_2_0</string> - <!-- Custom variables section --> -</resources> \ No newline at end of file + <!-- Language specific section --> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f825dd..408e914 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,10 +15,13 @@ <string name="add_identity">Add Identity</string> <string name="add_identity_summary">Create new identity</string> <string name="create_identity">Create new</string> + <string name="create_identity_description">Create a new, random identity</string> <string name="import_identity">Import existing</string> + <string name="import_identity_description">Import an existing identity from PyBitmessage or from an export</string> <string name="add_deterministic_address">Deterministic identity</string> + <string name="add_deterministic_address_description">Create or recreate a deterministic identity</string> <string name="add_chan">Add Chan</string> - <string name="add_chan_summary">Add or create a chan</string> + <string name="add_chan_description">Create or join a chan</string> <string name="title_activity_open_bitmessage_link">Import Contact</string> <string name="action_settings">Settings</string> @@ -68,7 +71,7 @@ <string name="pubkey_available">Public key available</string> <string name="pubkey_not_available">Public key not yet available</string> <string name="alt_qr_code">QR code</string> - <string name="add_identity_warning">Having more identities will reequire more resources. Are you sure you want to add an identity?</string> + <string name="add_identity_warning">Having more identities will reequire more resources. If you are sure you want to add an identity, please select what exactly you want to do:</string> <string name="share">Share</string> <string name="delete_identity_warning">Are you sure you want to delete this identity? You won\'t be able to receive any messages sent to this address and can\'t undo this operation.</string> <string name="delete_contact_warning">Are you sure you want to delete this contact?</string> @@ -91,12 +94,12 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="deterministic_address_warning">Be sure to remember those settings correctly when recreating a deterministic address.</string> <string name="number_of_identities">Number of identities to create</string> <string name="shorter">Search for shorter addresses</string> - <string name="wif_string">WIF / contents of \'keys.dat\'</string> + <string name="wif_string">WIF / contents of ‘keys.dat’</string> <string name="next">Continue</string> <string name="title_import_identity">Import Identity</string> <string name="open_file">Open File</string> <string name="error_loading_data">Error loading data</string> <string name="select_file_title">Select a File</string> <string name="select_identities_to_import">Please select the identities you want to import:</string> - <string name="import_input_description">You can just paste the contents of an export or a \'keys.dat\' file</string> + <string name="import_input_description">You can just paste the contents of an export or a ‘keys.dat’ file</string> </resources> From da6cd43a46cfdffc96797261f3e280f785210f2e Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 5 Oct 2016 15:59:24 +0200 Subject: [PATCH 067/110] Added missing libraries for AboutLibraries dialog --- .../library_fab_speed_dial_strings.xml | 20 +++++++++++ .../values-de/library_filepicker_strings.xml | 5 +++ .../res/values-de/library_zxing_strings.xml | 20 +++++++++++ .../library_zxingandroidembedded_strings.xml | 5 +++ .../values/library_fab_speed_dial_strings.xml | 36 +++++++++++++++++++ .../res/values/library_filepicker_strings.xml | 21 +++++++++++ .../main/res/values/library_zxing_strings.xml | 36 +++++++++++++++++++ .../library_zxingandroidembedded_strings.xml | 21 +++++++++++ 8 files changed, 164 insertions(+) create mode 100644 app/src/main/res/values-de/library_fab_speed_dial_strings.xml create mode 100644 app/src/main/res/values-de/library_filepicker_strings.xml create mode 100644 app/src/main/res/values-de/library_zxing_strings.xml create mode 100644 app/src/main/res/values-de/library_zxingandroidembedded_strings.xml create mode 100644 app/src/main/res/values/library_fab_speed_dial_strings.xml create mode 100644 app/src/main/res/values/library_filepicker_strings.xml create mode 100644 app/src/main/res/values/library_zxing_strings.xml create mode 100644 app/src/main/res/values/library_zxingandroidembedded_strings.xml diff --git a/app/src/main/res/values-de/library_fab_speed_dial_strings.xml b/app/src/main/res/values-de/library_fab_speed_dial_strings.xml new file mode 100644 index 0000000..9984520 --- /dev/null +++ b/app/src/main/res/values-de/library_fab_speed_dial_strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="library_fab_speed_dial_libraryDescription">Eine einfache Bibliothek die FAB + Menü-Ressourcen + Speed Dial Metapher von Material Design vereinigt.</string> +</resources> diff --git a/app/src/main/res/values-de/library_filepicker_strings.xml b/app/src/main/res/values-de/library_filepicker_strings.xml new file mode 100644 index 0000000..2d8422f --- /dev/null +++ b/app/src/main/res/values-de/library_filepicker_strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <string name="library_filepicker_libraryDescription">Android-Bibliothek um Dateien und Verzeichnisse im Gerätespeicher auszuwählen.</string> +</resources> diff --git a/app/src/main/res/values-de/library_zxing_strings.xml b/app/src/main/res/values-de/library_zxing_strings.xml new file mode 100644 index 0000000..54c10be --- /dev/null +++ b/app/src/main/res/values-de/library_zxing_strings.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="library_zxing_libraryDescription">ZXing (“Zebrastreifen”) ist eine Open Source Bildverarbeitungs-Bibliothek für verschiedene 1D/2D Barcode-Formate.</string> +</resources> diff --git a/app/src/main/res/values-de/library_zxingandroidembedded_strings.xml b/app/src/main/res/values-de/library_zxingandroidembedded_strings.xml new file mode 100644 index 0000000..e59b006 --- /dev/null +++ b/app/src/main/res/values-de/library_zxingandroidembedded_strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <string name="library_zxingandroidembedded_libraryDescription">Barcode-Scanning-Bibliothek für Android, verwendet ZXing als Decoder.</string> +</resources> diff --git a/app/src/main/res/values/library_fab_speed_dial_strings.xml b/app/src/main/res/values/library_fab_speed_dial_strings.xml new file mode 100644 index 0000000..bdc1c1f --- /dev/null +++ b/app/src/main/res/values/library_fab_speed_dial_strings.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="define_fab_speed_dial" translatable="false"/> + <!-- Author section --> + <string name="library_fab_speed_dial_author" translatable="false">Yavor Ivanov</string> + <string name="library_fab_speed_dial_authorWebsite" translatable="false">https://github.com/yavski/</string> + <!-- Library section --> + <string name="library_fab_speed_dial_libraryName" translatable="false">fab-speed-dial</string> + <string name="library_fab_speed_dial_libraryDescription">A simple library marrying together FAB + menu resources + Speed dial metaphor from Material Design.</string> + <string name="library_fab_speed_dial_libraryWebsite" translatable="false">https://github.com/yavski/fab-speed-dial</string> + <string name="library_fab_speed_dial_libraryVersion" translatable="false">1.0.4</string> + <!-- OpenSource section --> + <string name="library_fab_speed_dial_isOpenSource" translatable="false">true</string> + <string name="library_fab_speed_dial_repositoryLink" translatable="false">https://github.com/yavski/fab-speed-dial</string> + <!-- ClassPath for autoDetect section --> + <string name="library_fab_speed_dial_classPath" translatable="false">io.github.yavski.fabspeeddial.FabSpeedDial</string> + <!-- License section --> + <string name="library_fab_speed_dial_licenseId" translatable="false">apache_2_0</string> + <!-- Language specific section --> +</resources> diff --git a/app/src/main/res/values/library_filepicker_strings.xml b/app/src/main/res/values/library_filepicker_strings.xml new file mode 100644 index 0000000..23ba3b6 --- /dev/null +++ b/app/src/main/res/values/library_filepicker_strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <string name="define_int_filepicker" translatable="false"/> + <!-- Author section --> + <string name="library_filepicker_author" translatable="false">Angad Singh</string> + <string name="library_filepicker_authorWebsite" translatable="false">https://github.com/Angads25</string> + <!-- Library section --> + <string name="library_filepicker_libraryName" translatable="false">FilePicker</string> + <string name="library_filepicker_libraryDescription">Android Library to select files/directories from Device Storage.</string> + <string name="library_filepicker_libraryWebsite" translatable="false">https://github.com/Angads25/android-filepicker</string> + <string name="library_filepicker_libraryVersion" translatable="false">1.0.6</string> + <!-- OpenSource section --> + <string name="library_filepicker_isOpenSource" translatable="false">true</string> + <string name="library_filepicker_repositoryLink" translatable="false">https://github.com/Angads25/android-filepicker</string> + <!-- ClassPath for autoDetect section --> + <string name="library_filepicker_classPath" translatable="false">com.github.angads25.filepicker.view.FilePickerDialog</string> + <!-- License section --> + <string name="library_filepicker_licenseId" translatable="false">apache_2_0</string> + <!-- Custom variables section --> +</resources> diff --git a/app/src/main/res/values/library_zxing_strings.xml b/app/src/main/res/values/library_zxing_strings.xml new file mode 100644 index 0000000..49e7177 --- /dev/null +++ b/app/src/main/res/values/library_zxing_strings.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="define_zxing" translatable="false"/> + <!-- Author section --> + <string name="library_zxing_author" translatable="false">Google, Inc.</string> + <string name="library_zxing_authorWebsite" translatable="false">github.com/zxing/zxing</string> + <!-- Library section --> + <string name="library_zxing_libraryName" translatable="false">ZXing</string> + <string name="library_zxing_libraryDescription">ZXing (“zebra crossing”) is an open-source, multi-format 1D/2D barcode image processing library.</string> + <string name="library_zxing_libraryWebsite" translatable="false">https://zxing.github.io/zxing/</string> + <string name="library_zxing_libraryVersion" translatable="false">3.3.0</string> + <!-- OpenSource section --> + <string name="library_zxing_isOpenSource" translatable="false">true</string> + <string name="library_zxing_repositoryLink" translatable="false">https://github.com/zxing/zxing</string> + <!-- ClassPath for autoDetect section --> + <string name="library_zxing_classPath" translatable="false">com.google.zxing.BarcodeFormat</string> + <!-- License section --> + <string name="library_zxing_licenseId" translatable="false">apache_2_0</string> + <!-- Language specific section --> +</resources> diff --git a/app/src/main/res/values/library_zxingandroidembedded_strings.xml b/app/src/main/res/values/library_zxingandroidembedded_strings.xml new file mode 100644 index 0000000..3540706 --- /dev/null +++ b/app/src/main/res/values/library_zxingandroidembedded_strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> + +<resources> + <string name="define_int_zxingandroidembedded" translatable="false"/> + <!-- Author section --> + <string name="library_zxingandroidembedded_author" translatable="false">JourneyApps</string> + <string name="library_zxingandroidembedded_authorWebsite" translatable="false">github.com/journeyapps</string> + <!-- Library section --> + <string name="library_zxingandroidembedded_libraryName" translatable="false">ZXing Android Embedded</string> + <string name="library_zxingandroidembedded_libraryDescription">Barcode scanning library for Android, using ZXing for decoding.</string> + <string name="library_zxingandroidembedded_libraryWebsite" translatable="false">https://github.com/journeyapps/zxing-android-embedded</string> + <string name="library_zxingandroidembedded_libraryVersion" translatable="false">3.3.0</string> + <!-- OpenSource section --> + <string name="library_zxingandroidembedded_isOpenSource" translatable="false">true</string> + <string name="library_zxingandroidembedded_repositoryLink" translatable="false">https://github.com/journeyapps/zxing-android-embedded</string> + <!-- ClassPath for autoDetect section --> + <string name="library_zxingandroidembedded_classPath" translatable="false">com.journeyapps.barcodescanner.BarcodeView</string> + <!-- License section --> + <string name="library_zxingandroidembedded_licenseId" translatable="false">apache_2_0</string> + <!-- Custom variables section --> +</resources> From 91451b0ce7e57530ff11fe190b2f23628176ba68 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 6 Oct 2016 22:01:33 +0200 Subject: [PATCH 068/110] Fixed some layouts and updated dependencies --- app/build.gradle | 14 +-- .../apps/abit/AddressDetailFragment.java | 4 + .../ch/dissem/apps/abit/MainActivity.java | 13 +++ .../main/res/drawable/bg_item_activated.xml | 21 +++++ .../layout-w720dp/activity_message_list.xml | 17 ++-- app/src/main/res/layout/contact_row.xml | 68 +++++++------- .../res/layout/fragment_address_detail.xml | 2 +- .../main/res/layout/fragment_address_list.xml | 3 +- .../res/layout/scrolling_toolbar_layout.xml | 1 - app/src/main/res/layout/subscription_row.xml | 89 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 2 +- 12 files changed, 140 insertions(+), 95 deletions(-) create mode 100644 app/src/main/res/drawable/bg_item_activated.xml diff --git a/app/build.gradle b/app/build.gradle index a7c4f4a..893c544 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,14 +11,14 @@ if (project.hasProperty("project.configs") android { compileSdkVersion 24 - buildToolsVersion "24.0.2" + buildToolsVersion "24.0.3" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 targetSdkVersion 24 - versionCode 7 - versionName "1.0-beta7" + versionCode 8 + versionName "1.0-beta8" } buildTypes { release { @@ -29,7 +29,7 @@ android { } } -ext.jabitVersion = 'development-SNAPSHOT' +ext.jabitVersion = '2.0.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1' @@ -54,9 +54,9 @@ dependencies { compile 'com.mikepenz:iconics:1.6.2@aar' compile 'com.mikepenz:community-material-typeface:1.5.54.2@aar' - compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar' - compile 'com.google.zxing:core:3.2.0' - compile 'io.github.yavski:fab-speed-dial:1.0.2' + compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' + compile 'com.google.zxing:core:3.3.0' + compile 'io.github.yavski:fab-speed-dial:1.0.4' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile ('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar'){ transitive=true diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 08760d6..d4b8b39 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -278,6 +278,10 @@ public class AddressDetailFragment extends Fragment { public void onPause() { if (item != null) { Singleton.getAddressRepository(getContext()).save(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null && item.getPrivateKey() != null) { + mainActivity.updateIdentityEntry(item); + } } super.onPause(); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 1cc4bea..0c347fd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -438,6 +438,19 @@ public class MainActivity extends AppCompatActivity } } + public void updateIdentityEntry(BitmessageAddress identity) { + for (IProfile profile : accountHeader.getProfiles()) { + if (profile instanceof ProfileDrawerItem) { + if (identity.equals(((ProfileDrawerItem) profile).getTag())) { + ((ProfileDrawerItem) profile) + .withName(identity.toString()) + .withTag(identity); + return; + } + } + } + } + public void removeIdentityEntry(BitmessageAddress identity) { for (IProfile profile : accountHeader.getProfiles()) { if (profile instanceof ProfileDrawerItem) { diff --git a/app/src/main/res/drawable/bg_item_activated.xml b/app/src/main/res/drawable/bg_item_activated.xml new file mode 100644 index 0000000..7b7e224 --- /dev/null +++ b/app/src/main/res/drawable/bg_item_activated.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_activated="true" android:drawable="@color/bg_item_selected_state" /> + <item android:state_activated="false" android:drawable="@color/contentBackground" /> +</selector> diff --git a/app/src/main/res/layout-w720dp/activity_message_list.xml b/app/src/main/res/layout-w720dp/activity_message_list.xml index a4b6855..4e2005b 100644 --- a/app/src/main/res/layout-w720dp/activity_message_list.xml +++ b/app/src/main/res/layout-w720dp/activity_message_list.xml @@ -20,7 +20,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/toolbar" - android:background="@color/bg_item_selected_state" android:baselineAligned="false" android:orientation="horizontal" android:showDividers="middle" @@ -40,14 +39,20 @@ tools:layout="@layout/fragment_message_list"/> <FrameLayout - android:id="@+id/message_detail_container" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_margin="16dp" android:layout_weight="2" - android:background="@color/contentBackground" - android:elevation="2dp" - tools:layout="@layout/fragment_message_detail"/> + android:background="@color/bg_item_selected_state"> + + <FrameLayout + android:id="@+id/message_detail_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="16dp" + android:background="@color/contentBackground" + android:elevation="2dp" + tools:layout="@layout/fragment_message_detail"/> + </FrameLayout> </LinearLayout> </RelativeLayout> diff --git a/app/src/main/res/layout/contact_row.xml b/app/src/main/res/layout/contact_row.xml index c6afc0f..af37c4a 100644 --- a/app/src/main/res/layout/contact_row.xml +++ b/app/src/main/res/layout/contact_row.xml @@ -21,42 +21,42 @@ android:layout_height="wrap_content"> <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:src="@color/colorAccent" - android:layout_margin="16dp" - tools:ignore="ContentDescription"/> + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorAccent" + tools:ignore="ContentDescription"/> <TextView - android:id="@+id/name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Name" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_alignTop="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:paddingTop="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="0dp" - android:textStyle="bold"/> + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Name"/> <TextView - android:id="@+id/address" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="BM-2cW0000000000000000000000000000000" - android:lines="1" - android:ellipsize="marquee" - android:textAppearance="?android:attr/textAppearanceSmall" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:layout_alignBottom="@+id/avatar" - android:layout_toEndOf="@+id/avatar"/> + android:id="@+id/address" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="marquee" + android:lines="1" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="BM-2cW0000000000000000000000000000000"/> -</RelativeLayout> \ No newline at end of file +</RelativeLayout> diff --git a/app/src/main/res/layout/fragment_address_detail.xml b/app/src/main/res/layout/fragment_address_detail.xml index 679aa5c..f1f1eed 100644 --- a/app/src/main/res/layout/fragment_address_detail.xml +++ b/app/src/main/res/layout/fragment_address_detail.xml @@ -116,4 +116,4 @@ android:elevation="2dp" tools:ignore="UnusedAttribute" tools:src="@drawable/public_key"/> -</RelativeLayout> \ No newline at end of file +</RelativeLayout> diff --git a/app/src/main/res/layout/fragment_address_list.xml b/app/src/main/res/layout/fragment_address_list.xml index c4a8950..018bf6f 100644 --- a/app/src/main/res/layout/fragment_address_list.xml +++ b/app/src/main/res/layout/fragment_address_list.xml @@ -8,6 +8,7 @@ android:id="@id/android:list" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:choiceMode="singleChoice" android:layout_alignParentBottom="true" android:layout_alignParentStart="true" @@ -29,4 +30,4 @@ app:elevation="8dp" app:fabGravity="bottom_end" app:fabMenu="@menu/fab_address"/> -</RelativeLayout> \ No newline at end of file +</RelativeLayout> diff --git a/app/src/main/res/layout/scrolling_toolbar_layout.xml b/app/src/main/res/layout/scrolling_toolbar_layout.xml index 9abd179..0b995b4 100644 --- a/app/src/main/res/layout/scrolling_toolbar_layout.xml +++ b/app/src/main/res/layout/scrolling_toolbar_layout.xml @@ -27,7 +27,6 @@ android:background="?attr/colorPrimary" android:elevation="4dp" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" - app:layout_scrollFlags="scroll|enterAlways" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" tools:ignore="UnusedAttribute"/> diff --git a/app/src/main/res/layout/subscription_row.xml b/app/src/main/res/layout/subscription_row.xml index 70ee7a6..c7d137d 100644 --- a/app/src/main/res/layout/subscription_row.xml +++ b/app/src/main/res/layout/subscription_row.xml @@ -19,56 +19,57 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:background="?android:attr/activatedBackgroundIndicator"> <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:src="@color/colorAccent" - android:layout_margin="16dp" - tools:ignore="ContentDescription"/> + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorAccent" + tools:ignore="ContentDescription"/> <TextView - android:id="@+id/name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Name" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_alignTop="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:paddingTop="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="0dp" - android:textStyle="bold" - /> + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Name" + /> <TextView - android:id="@+id/stream_number" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Stream #" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:layout_alignBottom="@+id/avatar" - android:layout_toEndOf="@+id/avatar"/> + android:id="@+id/stream_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/avatar" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Stream #"/> <com.mikepenz.iconics.view.IconicsImageView - android:id="@+id/subscribed" - android:layout_width="16dp" - android:layout_height="16dp" - app:iiv_color="@android:color/black" - app:iiv_icon="cmd-rss" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - android:layout_marginEnd="16dp"/> + android:id="@+id/subscribed" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_marginEnd="16dp" + app:iiv_color="@android:color/black" + app:iiv_icon="cmd-rss"/> -</RelativeLayout> \ No newline at end of file +</RelativeLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 408e914..f2c1565 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,4 +102,5 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="select_file_title">Select a File</string> <string name="select_identities_to_import">Please select the identities you want to import:</string> <string name="import_input_description">You can just paste the contents of an export or a ‘keys.dat’ file</string> + <string name="name">Name</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 02ec048..ba7dc9e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,7 +2,7 @@ <!-- Base application theme. --> <style name="AppTheme" parent="MaterialDrawerTheme.Light.DarkToolbar.TranslucentStatus"> - <item name="android:activatedBackgroundIndicator">@color/colorPrimaryLight</item> + <item name="android:activatedBackgroundIndicator">@drawable/bg_item_activated</item> <item name="android:textColor">@color/colorPrimaryText</item> <item name="android:textColorSecondary">@color/colorSecondaryText</item> </style> From a1fb11357bbd9eb614075f57f89274f5069ea2ea Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 7 Oct 2016 23:06:47 +0200 Subject: [PATCH 069/110] Fixed NPEs in identity creation dialogs --- .../abit/dialog/AddIdentityDialogFragment.java | 18 +++++++++++------- app/src/main/res/values-de/strings.xml | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java index f404c9c..710ead0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; import android.support.v7.app.AppCompatDialogFragment; import android.view.LayoutInflater; import android.view.View; @@ -63,9 +64,10 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + final Context ctx = getActivity().getBaseContext(); switch (radioGroup.getCheckedRadioButtonId()) { case R.id.create_identity: - Toast.makeText(getActivity(), + Toast.makeText(ctx, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show(); new AsyncTask<Void, Void, BitmessageAddress>() { @@ -76,7 +78,7 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { @Override protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(getActivity(), + Toast.makeText(ctx, R.string.toast_identity_created, Toast.LENGTH_SHORT).show(); MainActivity mainActivity = MainActivity.getInstance(); @@ -87,7 +89,7 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { }.execute(); break; case R.id.import_identity: - startActivity(new Intent(getActivity(), ImportIdentityActivity.class)); + startActivity(new Intent(ctx, ImportIdentityActivity.class)); break; case R.id.add_chan: addChanDialog(); @@ -113,17 +115,19 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { } private void addChanDialog() { + FragmentActivity activity = getActivity(); + final Context ctx = activity.getBaseContext(); @SuppressLint("InflateParams") - final View dialogView = getActivity().getLayoutInflater() + final View dialogView = activity.getLayoutInflater() .inflate(R.layout.dialog_input_passphrase, null); - new AlertDialog.Builder(getActivity()) + new AlertDialog.Builder(activity) .setTitle(R.string.add_chan) .setView(dialogView) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - Toast.makeText(getActivity(), R.string.toast_long_running_operation, + Toast.makeText(ctx, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show(); new AsyncTask<String, Void, BitmessageAddress>() { @Override @@ -137,7 +141,7 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { @Override protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(getActivity(), + Toast.makeText(ctx, R.string.toast_chan_created, Toast.LENGTH_SHORT).show(); MainActivity mainActivity = MainActivity.getInstance(); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 89dec00..f5b3f69 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -103,4 +103,5 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="select_file_title">Datei auswählen</string> <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer ‘keys.dat’-Datei einfügen</string> + <string name="name">Name</string> </resources> From b94e48e544aade7de08b8dd97b7e76cfcb41370a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 7 Oct 2016 23:23:01 +0200 Subject: [PATCH 070/110] Updated Jabit version to fix exception on import --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 893c544..497dfb2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { } } -ext.jabitVersion = '2.0.0' +ext.jabitVersion = '2.0.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1' From 4ab64c0ed1dc365606ab21ca2ee521b17cd630e7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 10 Oct 2016 21:56:31 +0200 Subject: [PATCH 071/110] Version 1.0-beta9 bump --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 497dfb2..da62874 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 targetSdkVersion 24 - versionCode 8 - versionName "1.0-beta8" + versionCode 9 + versionName "1.0-beta9" } buildTypes { release { From dc3bfce9a292a5eda06bfafe8b6c813507b67b14 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 10 Oct 2016 21:57:46 +0200 Subject: [PATCH 072/110] Fixed identicon rendering in identity view --- .../java/ch/dissem/apps/abit/Identicon.java | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index 288e27e..dd4d550 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -18,6 +18,8 @@ package ch.dissem.apps.abit; import android.graphics.*; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + import ch.dissem.bitmessage.entity.BitmessageAddress; /** @@ -28,11 +30,6 @@ public class Identicon extends Drawable { private static final int CENTER_COLUMN = 5; private final Paint paint; - private float width; - private float height; - - private float cellWidth; - private float cellHeight; private int color; private int background; private boolean[][] fields; @@ -45,21 +42,34 @@ public class Identicon extends Drawable { byte[] hash = input.getRipe(); fields = new boolean[SIZE][SIZE]; - color = Color.HSVToColor(new float[]{Math.abs(hash[0] * hash[1] + hash[2]) % 360, 0.8f, 1.0f}); - background = Color.HSVToColor(new float[]{Math.abs(hash[1] * hash[2] + hash[0]) % 360, 0.8f, 1.0f}); + color = Color.HSVToColor(new float[]{ + Math.abs(hash[0] * hash[1] + hash[2]) % 360, + 0.8f, + 1.0f + }); + background = Color.HSVToColor(new float[]{ + Math.abs(hash[1] * hash[2] + hash[0]) % 360, + 0.8f, + 1.0f + }); for (int row = 0; row < SIZE; row++) { for (int column = 0; column < SIZE; column++) { - fields[row][column] = hash[(row * (column < CENTER_COLUMN ? column : SIZE - column - 1)) % hash.length] >= 0; + fields[row][column] = hash[(row * (column < CENTER_COLUMN ? column : SIZE - + column - 1)) % hash.length] >= 0; } } } @Override - public void draw(Canvas canvas) { + public void draw(@NonNull Canvas canvas) { float x, y; + float width = canvas.getWidth(); + float height = canvas.getHeight(); + float cellWidth = width / (float) SIZE; + float cellHeight = height / (float) SIZE; paint.setColor(background); - canvas.drawCircle(width/2, height/2, width/2, paint); + canvas.drawCircle(width / 2, height / 2, width / 2, paint); paint.setColor(color); for (int row = 0; row < SIZE; row++) { for (int column = 0; column < SIZE; column++) { @@ -87,16 +97,4 @@ public class Identicon extends Drawable { public int getOpacity() { return PixelFormat.TRANSPARENT; } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - - width = bounds.width(); - height = bounds.height(); - - cellWidth = bounds.width() / (float) SIZE; - cellHeight = bounds.height() / (float) SIZE; - } - -} \ No newline at end of file +} From a5b3c333947b9dcf76d3609498a254bba2d10825 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Oct 2016 22:15:41 +0200 Subject: [PATCH 073/110] Improved identicon --- .../java/ch/dissem/apps/abit/Identicon.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index dd4d550..a8da2c3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit; import android.graphics.*; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; +import android.text.TextPaint; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -33,11 +34,19 @@ public class Identicon extends Drawable { private int color; private int background; private boolean[][] fields; + private boolean chan; + private final TextPaint textPaint; public Identicon(BitmessageAddress input) { paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); + textPaint = new TextPaint(); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setColor(0xFF607D8B); + textPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)); + + chan = input.isChan(); byte[] hash = input.getRipe(); @@ -54,9 +63,18 @@ public class Identicon extends Drawable { }); for (int row = 0; row < SIZE; row++) { - for (int column = 0; column < SIZE; column++) { - fields[row][column] = hash[(row * (column < CENTER_COLUMN ? column : SIZE - - column - 1)) % hash.length] >= 0; + if (!chan || row < 5 || row > 6) { + for (int column = 0; column <= CENTER_COLUMN; column++) { + if ( + (row - SIZE / 2) * (row - SIZE / 2) + + (column - SIZE / 2) * (column - SIZE / 2) + < SIZE / 2 * SIZE / 2 + ) { + fields[row][column] = hash[(row * CENTER_COLUMN + column) % hash.length] + >= 0; + fields[row][SIZE - column - 1] = fields[row][column]; + } + } } } } @@ -76,11 +94,17 @@ public class Identicon extends Drawable { if (fields[row][column]) { x = cellWidth * column; y = cellHeight * row; - - canvas.drawCircle(x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, paint); + canvas.drawCircle( + x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, + paint + ); } } } + if (chan) { + textPaint.setTextSize(2 * cellHeight); + canvas.drawText("[chan]", width / 2, 6.7f * cellHeight, textPaint); + } } @Override From 2b1fb436a9ac62acbc493a0ebf76ddda72568073 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Oct 2016 23:16:38 +0200 Subject: [PATCH 074/110] Fixed the 'full node' switch, activated the jack tools to support some Java 8 features, and fixed some lint issues --- app/build.gradle | 17 +- .../apps/abit/AbstractItemListFragment.java | 17 +- .../apps/abit/AddressDetailFragment.java | 60 ++---- .../dissem/apps/abit/AddressListFragment.java | 72 +++---- .../apps/abit/ComposeMessageFragment.java | 10 +- .../apps/abit/ImportIdentitiesFragment.java | 27 +-- .../ch/dissem/apps/abit/MainActivity.java | 201 +++++++++--------- .../ch/dissem/apps/abit/SettingsActivity.java | 2 - .../DeterministicIdentityDialogFragment.java | 118 +++++----- .../notification/NetworkNotification.java | 47 ++-- .../notification/NewMessageNotification.java | 3 +- .../repository/AndroidAddressRepository.java | 8 +- .../AndroidProofOfWorkRepository.java | 1 - .../apps/abit/service/BitmessageService.java | 30 +-- .../apps/abit/service/ProofOfWorkService.java | 45 ++-- .../dissem/apps/abit/service/Singleton.java | 38 ++-- .../ch/dissem/apps/abit/util/Preferences.java | 3 - .../layout-w720dp/activity_message_list.xml | 3 +- .../res/layout/fragment_compose_message.xml | 4 +- app/src/main/res/layout/showcase_button.xml | 11 +- app/src/main/res/layout/subscription_row.xml | 6 +- app/src/main/res/values-de/strings.xml | 15 +- app/src/main/res/values/colors.xml | 2 +- .../values/library_fab_speed_dial_strings.xml | 2 +- .../res/values/library_filepicker_strings.xml | 34 +-- .../main/res/values/library_jabit_strings.xml | 2 +- .../main/res/values/library_zxing_strings.xml | 2 +- .../library_zxingandroidembedded_strings.xml | 2 +- app/src/main/res/values/strings.xml | 19 +- build.gradle | 2 +- 30 files changed, 367 insertions(+), 436 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index da62874..81387e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,10 +5,11 @@ ext { appName = "Abit" } if (project.hasProperty("project.configs") - && new File(project.property("project.configs") + appName + ".gradle").exists()) { + && new File(project.property("project.configs") + appName + ".gradle").exists()) { apply from: project.property("project.configs") + appName + ".gradle"; } +//noinspection GroovyMissingReturnStatement android { compileSdkVersion 24 buildToolsVersion "24.0.3" @@ -19,17 +20,23 @@ android { targetSdkVersion 24 versionCode 9 versionName "1.0-beta9" + jackOptions.enabled = true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { release { - minifyEnabled false + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } } -ext.jabitVersion = '2.0.1' +ext.jabitVersion = '2.0.2' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1' @@ -58,8 +65,8 @@ dependencies { compile 'com.google.zxing:core:3.3.0' compile 'io.github.yavski:fab-speed-dial:1.0.4' compile 'com.github.amlcurran.showcaseview:library:5.4.3' - compile ('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar'){ - transitive=true + compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar') { + transitive = true } compile 'com.github.angads25:filepicker:1.0.6' diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 3cb1e07..5432210 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -23,7 +23,6 @@ import android.view.View; import android.widget.ListView; import ch.dissem.apps.abit.listener.ListSelectionListener; -import ch.dissem.bitmessage.entity.valueobject.Label; /** * @author Christian Basler @@ -38,11 +37,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement * A dummy implementation of the {@link ListSelectionListener} interface that does * nothing. Used only when this fragment is not attached to an activity. */ - private static ListSelectionListener<Object> dummyCallbacks = new - ListSelectionListener<Object>() { - @Override - public void onItemSelected(Object plaintext) { - } + private static ListSelectionListener<Object> dummyCallbacks = plaintext -> { }; /** * The fragment's current callback object, which is notified of list item @@ -61,7 +56,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement // Restore the previously serialized activated item position. if (savedInstanceState != null - && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { + && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); } } @@ -73,8 +68,8 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement // When setting CHOICE_MODE_SINGLE, ListView will automatically // give items the 'activated' state when touched. getListView().setChoiceMode(activateOnItemClick - ? ListView.CHOICE_MODE_SINGLE - : ListView.CHOICE_MODE_NONE); + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); } @Override @@ -129,8 +124,8 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement // When setting CHOICE_MODE_SINGLE, ListView will automatically // give items the 'activated' state when touched. getListView().setChoiceMode(activateOnItemClick - ? ListView.CHOICE_MODE_SINGLE - : ListView.CHOICE_MODE_NONE); + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index d4b8b39..439823f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -18,7 +18,6 @@ package ch.dissem.apps.abit; import android.app.Activity; import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; @@ -31,7 +30,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; @@ -131,19 +129,15 @@ public class AddressDetailFragment extends Fragment { warning = R.string.delete_contact_warning; new AlertDialog.Builder(ctx) .setMessage(warning) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Singleton.getAddressRepository(ctx).remove(item); - MainActivity mainActivity = MainActivity.getInstance(); - if (item.getPrivateKey() != null && mainActivity != null) { - mainActivity.removeIdentityEntry(item); - } - item = null; - ctx.onBackPressed(); - } - }) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + Singleton.getAddressRepository(ctx).remove(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (item.getPrivateKey() != null && mainActivity != null) { + mainActivity.removeIdentityEntry(item); + } + item = null; + ctx.onBackPressed(); + }) .setNegativeButton(android.R.string.no, null) .show(); return true; @@ -151,22 +145,18 @@ public class AddressDetailFragment extends Fragment { case R.id.export: { new AlertDialog.Builder(ctx) .setMessage(R.string.confirm_export) - .setPositiveButton(android.R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_TITLE, item + - EXPORT_POSTFIX); - WifExporter exporter = new WifExporter(Singleton - .getBitmessageContext(ctx)); - exporter.addIdentity(item); - shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString - ()); - startActivity(Intent.createChooser(shareIntent, null)); - } - }) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TITLE, item + + EXPORT_POSTFIX); + WifExporter exporter = new WifExporter(Singleton + .getBitmessageContext(ctx)); + exporter.addIdentity(item); + shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString + ()); + startActivity(Intent.createChooser(shareIntent, null)); + }) .setNegativeButton(android.R.string.no, null) .show(); return true; @@ -216,12 +206,8 @@ public class AddressDetailFragment extends Fragment { if (item.getPrivateKey() == null) { Switch active = (Switch) rootView.findViewById(R.id.active); active.setChecked(item.isSubscribed()); - active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - item.setSubscribed(isChecked); - } - }); + active.setOnCheckedChangeListener((buttonView, isChecked) -> + item.setSubscribed(isChecked)); ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id .pubkey_available); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 66a52aa..5168422 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.MenuItem; @@ -32,7 +33,6 @@ import android.widget.TextView; import com.google.zxing.integration.android.IntentIntegrator; import java.util.Collections; -import java.util.Comparator; import java.util.List; import ch.dissem.apps.abit.listener.ActionBarListener; @@ -55,59 +55,53 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr public void updateList() { List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()) - .getContacts(); - Collections.sort(addresses, new Comparator<BitmessageAddress>() { - /** - * Yields the following order: - * <ol> - * <li>Subscribed addresses come first</li> - * <li>Addresses with Aliases (alphabetically)</li> - * <li>Addresses (alphabetically)</li> - * </ol> - */ - @Override - public int compare(BitmessageAddress lhs, BitmessageAddress rhs) { - if (lhs.isSubscribed() == rhs.isSubscribed()) { - if (lhs.getAlias() != null) { - if (rhs.getAlias() != null) { - return lhs.getAlias().compareTo(rhs.getAlias()); - } else { - return -1; - } - } else if (rhs.getAlias() != null) { - return 1; + .getContacts(); + Collections.sort(addresses, (lhs, rhs) -> { + // Yields the following order: + // * Subscribed addresses come first + // * Addresses with Aliases (alphabetically) + // * Addresses (alphabetically) + if (lhs.isSubscribed() == rhs.isSubscribed()) { + if (lhs.getAlias() != null) { + if (rhs.getAlias() != null) { + return lhs.getAlias().compareTo(rhs.getAlias()); } else { - return lhs.getAddress().compareTo(rhs.getAddress()); + return -1; } - } - if (lhs.isSubscribed()) { - return -1; - } else { + } else if (rhs.getAlias() != null) { return 1; + } else { + return lhs.getAddress().compareTo(rhs.getAddress()); } } + if (lhs.isSubscribed()) { + return -1; + } else { + return 1; + } }); setListAdapter(new ArrayAdapter<BitmessageAddress>( - getActivity(), - android.R.layout.simple_list_item_activated_1, - android.R.id.text1, - addresses) { + getActivity(), + android.R.layout.simple_list_item_activated_1, + android.R.id.text1, + addresses) { + @NonNull @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View getView(int position, View convertView, @NonNull ViewGroup parent) { if (convertView == null) { LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.subscription_row, null, false); + convertView = inflater.inflate(R.layout.subscription_row, parent, false); } BitmessageAddress item = getItem(position); ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new - Identicon(item)); + Identicon(item)); TextView name = (TextView) convertView.findViewById(R.id.name); name.setText(item.toString()); TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number); streamNumber.setText(getContext().getString(R.string.stream_number, item - .getStream())); + .getStream())); convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? - View.VISIBLE : View.INVISIBLE); + View.VISIBLE : View.INVISIBLE); return convertView; } }); @@ -124,7 +118,7 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle - savedInstanceState) { + savedInstanceState) { View view = inflater.inflate(R.layout.fragment_address_list, container, false); FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact); @@ -134,8 +128,8 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr switch (menuItem.getItemId()) { case R.id.action_read_qr_code: IntentIntegrator.forSupportFragment(AddressListFragment.this) - .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) - .initiateScan(); + .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) + .initiateScan(); return true; case R.id.action_create_contact: Intent intent = new Intent(getActivity(), CreateAddressActivity.class); diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 5d80e31..688812a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -18,7 +18,6 @@ package ch.dissem.apps.abit; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.text.Selection; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -90,12 +89,9 @@ public class ComposeMessageFragment extends Fragment { recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient); final ContactAdapter adapter = new ContactAdapter(getContext()); recipientInput.setAdapter(adapter); - recipientInput.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - recipient = adapter.getItem(position); - } - }); + recipientInput.setOnItemClickListener( + (parent, view, position, id) -> recipient = adapter.getItem(position) + ); recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java index f3e91a3..4d335b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java @@ -42,9 +42,6 @@ import ch.dissem.bitmessage.wif.WifImporter; public class ImportIdentitiesFragment extends Fragment { public static final String WIF_DATA = "wif_data"; - private BitmessageContext bmc; - private RecyclerView recyclerView; - private LinearLayoutManager layoutManager; private AddressSelectorAdapter adapter; private WifImporter importer; @@ -53,14 +50,15 @@ public class ImportIdentitiesFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { String wifData = getArguments().getString(WIF_DATA); - bmc = Singleton.getBitmessageContext(getActivity()); + BitmessageContext bmc = Singleton.getBitmessageContext(getActivity()); View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false); try { importer = new WifImporter(bmc, wifData); adapter = new AddressSelectorAdapter(importer.getIdentities()); - layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), + LinearLayoutManager.VERTICAL, false); - recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(adapter); @@ -69,18 +67,15 @@ public class ImportIdentitiesFragment extends Fragment { } catch (IOException e) { return super.onCreateView(inflater, container, savedInstanceState); } - view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - importer.importAll(adapter.getSelected()); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - for (BitmessageAddress selected : adapter.getSelected()) { - mainActivity.addIdentityEntry(selected); - } + view.findViewById(R.id.finish).setOnClickListener(v -> { + importer.importAll(adapter.getSelected()); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress selected : adapter.getSelected()) { + mainActivity.addIdentityEntry(selected); } - getActivity().finish(); } + getActivity().finish(); }); return view; } diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 0c347fd..10dc12f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -19,7 +19,6 @@ package ch.dissem.apps.abit; import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Point; @@ -34,7 +33,6 @@ import android.widget.CompoundButton; import android.widget.RelativeLayout; import com.github.amlcurran.showcaseview.ShowcaseView; -import com.github.amlcurran.showcaseview.targets.Target; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; @@ -42,7 +40,6 @@ import com.mikepenz.materialdrawer.AccountHeader; import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; -import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener; import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; @@ -102,7 +99,8 @@ public class MainActivity extends AppCompatActivity private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final int ADD_IDENTITY = 1; private static final int MANAGE_IDENTITY = 2; - private static final int ADD_CHAN = 3; + + private static final long ID_NODE_SWITCH = 1; private static WeakReference<MainActivity> instance; @@ -134,7 +132,7 @@ public class MainActivity extends AppCompatActivity private AccountHeader accountHeader; private Drawer drawer; - private ShowcaseView showcaseView; + private SwitchDrawerItem nodeSwitch; @Override protected void onCreate(Bundle savedInstanceState) { @@ -152,7 +150,9 @@ public class MainActivity extends AppCompatActivity setSupportActionBar(toolbar); MessageListFragment listFragment = new MessageListFragment(); - getSupportFragmentManager().beginTransaction().replace(R.id.item_list, listFragment) + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.item_list, listFragment) .commit(); if (findViewById(R.id.message_detail_container) != null) { @@ -189,27 +189,23 @@ public class MainActivity extends AppCompatActivity int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue(); lps.setMargins(margin, margin, margin, margin); - showcaseView = new ShowcaseView.Builder(this) + new ShowcaseView.Builder(this) .withMaterialShowcase() .setStyle(R.style.CustomShowcaseTheme) .setContentTitle(R.string.full_node) .setContentText(R.string.full_node_description) - .setTarget(new Target() { - @Override - public Point getPoint() { - View view = drawer.getStickyFooter(); - int[] location = new int[2]; - view.getLocationInWindow(location); - int x = location[0] + 7 * view.getWidth() / 8; - int y = location[1] + view.getHeight() / 2; - return new Point(x, y); - } - } - ) + .setTarget(() -> { + View view = drawer.getStickyFooter(); + int[] location = new int[2]; + view.getLocationInWindow(location); + int x = location[0] + 7 * view.getWidth() / 8; + int y = location[1] + view.getHeight() / 2; + return new Point(x, y); + }) .replaceEndButton(R.layout.showcase_button) .hideOnTouchOutside() - .build(); - showcaseView.setButtonPosition(lps); + .build() + .setButtonPosition(lps); } } @@ -268,32 +264,28 @@ public class MainActivity extends AppCompatActivity .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) - .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { - @Override - public boolean onProfileChanged(View view, IProfile profile, boolean - currentProfile) { - switch ((int) profile.getIdentifier()) { - case ADD_IDENTITY: - addIdentityDialog(); - break; - case MANAGE_IDENTITY: - Intent show = new Intent(MainActivity.this, - AddressDetailActivity.class); - show.putExtra(AddressDetailFragment.ARG_ITEM, - Singleton.getIdentity(getApplicationContext())); - startActivity(show); - break; - default: - if (profile instanceof ProfileDrawerItem) { - Object tag = ((ProfileDrawerItem) profile).getTag(); - if (tag instanceof BitmessageAddress) { - Singleton.setIdentity((BitmessageAddress) tag); - } + .withOnAccountHeaderListener((view, profile, currentProfile) -> { + switch ((int) profile.getIdentifier()) { + case ADD_IDENTITY: + addIdentityDialog(); + break; + case MANAGE_IDENTITY: + Intent show = new Intent(MainActivity.this, + AddressDetailActivity.class); + show.putExtra(AddressDetailFragment.ARG_ITEM, + Singleton.getIdentity(getApplicationContext())); + startActivity(show); + break; + default: + if (profile instanceof ProfileDrawerItem) { + Object tag = ((ProfileDrawerItem) profile).getTag(); + if (tag instanceof BitmessageAddress) { + Singleton.setIdentity((BitmessageAddress) tag); } - } - // false if it should close the drawer - return false; + } } + // false if it should close the drawer + return false; }) .build(); if (profiles.size() > 2) { // There's always the add and manage identity items @@ -346,62 +338,55 @@ public class MainActivity extends AppCompatActivity .withName(R.string.settings) .withIcon(GoogleMaterial.Icon.gmd_settings)); + nodeSwitch = new SwitchDrawerItem() + .withIdentifier(ID_NODE_SWITCH) + .withName(R.string.full_node) + .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) + .withChecked(isRunning()) + .withOnCheckedChangeListener((drawerItem, buttonView, isChecked) -> { + if (isChecked) { + checkAndStartNode(buttonView); + } else { + service.shutdownNode(); + } + }); + drawer = new DrawerBuilder() .withActivity(this) .withToolbar(toolbar) .withAccountHeader(accountHeader) .withDrawerItems(drawerItems) - .addStickyDrawerItems( - new SwitchDrawerItem() - .withName(R.string.full_node) - .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) - .withChecked(isRunning()) - .withOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(IDrawerItem drawerItem, - CompoundButton buttonView, - boolean isChecked) { - if (isChecked) { - checkAndStartNode(buttonView); - } else { - service.shutdownNode(); - } - } - }) - ) - .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { - @Override - public boolean onItemClick(View view, int position, IDrawerItem item) { - if (item.getTag() instanceof Label) { - selectedLabel = (Label) item.getTag(); - showSelectedLabel(); - return false; - } else if (item instanceof Nameable<?>) { - Nameable<?> ni = (Nameable<?>) item; - switch (ni.getName().getTextRes()) { - case R.string.contacts_and_subscriptions: - if (!(getSupportFragmentManager().findFragmentById(R.id - .item_list) instanceof AddressListFragment)) { - changeList(new AddressListFragment()); - } else { - ((AddressListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(); - } - break; - case R.string.settings: - startActivity(new Intent(MainActivity.this, SettingsActivity - .class)); - break; - case R.string.archive: - selectedLabel = null; - showSelectedLabel(); - break; - case R.string.full_node: - return true; - } - } + .addStickyDrawerItems(nodeSwitch) + .withOnDrawerItemClickListener((view, position, item) -> { + if (item.getTag() instanceof Label) { + selectedLabel = (Label) item.getTag(); + showSelectedLabel(); return false; + } else if (item instanceof Nameable<?>) { + Nameable<?> ni = (Nameable<?>) item; + switch (ni.getName().getTextRes()) { + case R.string.contacts_and_subscriptions: + if (!(getSupportFragmentManager().findFragmentById(R.id + .item_list) instanceof AddressListFragment)) { + changeList(new AddressListFragment()); + } else { + ((AddressListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(); + } + break; + case R.string.settings: + startActivity(new Intent(MainActivity.this, SettingsActivity + .class)); + break; + case R.string.archive: + selectedLabel = null; + showSelectedLabel(); + break; + case R.string.full_node: + return true; + } } + return false; }) .withShowDrawerOnFirstLaunch(true) .build(); @@ -415,6 +400,7 @@ public class MainActivity extends AppCompatActivity @Override protected void onResume() { updateUnread(); + updateNodeSwitch(); super.onResume(); } @@ -470,18 +456,14 @@ public class MainActivity extends AppCompatActivity } else { new AlertDialog.Builder(MainActivity.this) .setMessage(R.string.full_node_warning) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - service.startupNode(); - } - }) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - buttonView.setChecked(false); - } - }) + .setPositiveButton( + android.R.string.yes, + (dialog, which) -> service.startupNode() + ) + .setNegativeButton( + android.R.string.no, + (dialog, which) -> updateNodeSwitch() + ) .show(); } } @@ -501,6 +483,13 @@ public class MainActivity extends AppCompatActivity } } + public void updateNodeSwitch() { + runOnUiThread(() -> { + nodeSwitch.withChecked(bmc.isRunning()); + drawer.updateStickyFooterItem(nodeSwitch); + }); + } + private void showSelectedLabel() { if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment) { diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java index 07f4cc7..8997349 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsActivity.java @@ -1,8 +1,6 @@ package ch.dissem.apps.abit; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; /** * @author Christian Basler diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java index 9cad6ba..a31b84b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java @@ -20,7 +20,6 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.v4.app.FragmentActivity; import android.support.v7.app.AppCompatDialogFragment; import android.view.LayoutInflater; import android.view.View; @@ -41,7 +40,6 @@ import ch.dissem.bitmessage.entity.payload.Pubkey; /** * @author Christian Basler */ - public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment { private BitmessageContext bmc; @@ -58,76 +56,68 @@ public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment getDialog().setTitle(R.string.add_deterministic_address); View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false); view.findViewById(R.id.ok) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - final Context context = getActivity().getBaseContext(); - View dialogView = getView(); - TextView label = (TextView) dialogView.findViewById(R.id.label); - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id - .number_of_identities); - Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); + .setOnClickListener(v -> { + dismiss(); + final Context context = getActivity().getBaseContext(); + View dialogView = getView(); + TextView label = (TextView) dialogView.findViewById(R.id.label); + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id + .number_of_identities); + Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); - Toast.makeText(context, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Object, Void, List<BitmessageAddress>>() { - @Override - protected List<BitmessageAddress> doInBackground(Object... args) { - String label = (String) args[0]; - String pass = (String) args[1]; - int numberOfAddresses = (int) args[2]; - boolean shorter = (boolean) args[3]; - List<BitmessageAddress> identities = bmc.createDeterministicAddresses - (pass, - numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); - int i = 0; - for (BitmessageAddress identity : identities) { - i++; - if (identities.size() == 1) { - identity.setAlias(label); - } else { - identity.setAlias(label + " (" + i + ")"); - } - bmc.addresses().save(identity); - } - return identities; - } - - @Override - protected void onPostExecute(List<BitmessageAddress> identities) { - int messageRes; + Toast.makeText(context, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Object, Void, List<BitmessageAddress>>() { + @Override + protected List<BitmessageAddress> doInBackground(Object... args) { + String label = (String) args[0]; + String pass = (String) args[1]; + int numberOfAddresses = (int) args[2]; + boolean shorter = (boolean) args[3]; + List<BitmessageAddress> identities = bmc.createDeterministicAddresses + (pass, + numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); + int i = 0; + for (BitmessageAddress identity : identities) { + i++; if (identities.size() == 1) { - messageRes = R.string.toast_identity_created; + identity.setAlias(label); } else { - messageRes = R.string.toast_identities_created; + identity.setAlias(label + " (" + i + ")"); } - Toast.makeText(context, - messageRes, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - for (BitmessageAddress identity : identities) { - mainActivity.addIdentityEntry(identity); - } + bmc.addresses().save(identity); + } + return identities; + } + + @Override + protected void onPostExecute(List<BitmessageAddress> identities) { + int messageRes; + if (identities.size() == 1) { + messageRes = R.string.toast_identity_created; + } else { + messageRes = R.string.toast_identities_created; + } + Toast.makeText(context, + messageRes, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress identity : identities) { + mainActivity.addIdentityEntry(identity); } } - }.execute( - label.getText().toString(), - passphrase.getText().toString(), - Integer.valueOf(numberOfAddresses.getText().toString()), - shorter.isChecked() - ); - } + } + }.execute( + label.getText().toString(), + passphrase.getText().toString(), + Integer.valueOf(numberOfAddresses.getText().toString()), + shorter.isChecked() + ); }); view.findViewById(R.id.dismiss) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); + .setOnClickListener(v -> dismiss()); return view; } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index d11c0c9..1377e1f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -17,7 +17,6 @@ package ch.dissem.apps.abit.notification; import android.annotation.SuppressLint; -import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -28,7 +27,7 @@ import java.util.TimerTask; import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; -import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.bitmessage.utils.Property; /** @@ -37,31 +36,32 @@ import ch.dissem.bitmessage.utils.Property; public class NetworkNotification extends AbstractNotification { public static final int ONGOING_NOTIFICATION_ID = 2; - private final BitmessageContext bmc; private NotificationCompat.Builder builder; - public NetworkNotification(Context ctx, BitmessageContext bmc) { + public NetworkNotification(Context ctx) { super(ctx); - this.bmc = bmc; + Intent showMessageIntent = new Intent(ctx, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showMessageIntent, 0); builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_full_node) - .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - } - - @Override - public Notification getNotification() { - update(); - return notification; + .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setShowWhen(false) + .setContentIntent(pendingIntent); } @SuppressLint("StringFormatMatches") + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean update() { - boolean running = bmc.isRunning(); + boolean running = BitmessageService.isRunning(); builder.setOngoing(running); - Property connections = bmc.status().getProperty("network").getProperty("connections"); + Property connections = BitmessageService.getStatus().getProperty("network", "connections"); if (!running) { builder.setContentText(ctx.getString(R.string.connection_info_disconnected)); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.updateNodeSwitch(); + } } else if (connections.getProperties().length == 0) { builder.setContentText(ctx.getString(R.string.connection_info_pending)); } else { @@ -71,29 +71,24 @@ public class NetworkNotification extends AbstractNotification { Integer nodeCount = (Integer) stream.getProperty("nodes").getValue(); if (nodeCount == 1) { info.append(ctx.getString(R.string.connection_info_1, - streamNumber)); + streamNumber)); } else { info.append(ctx.getString(R.string.connection_info_n, - streamNumber, nodeCount)); + streamNumber, nodeCount)); } info.append('\n'); } builder.setContentText(info); } - Intent showMessageIntent = new Intent(ctx, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showMessageIntent, 0); - builder.setContentIntent(pendingIntent); notification = builder.build(); return running; } @Override public void show() { - update(); super.show(); - final Timer timer = new Timer(); - timer.schedule(new TimerTask() { + new Timer().schedule(new TimerTask() { @Override public void run() { if (!update()) { @@ -108,4 +103,10 @@ public class NetworkNotification extends AbstractNotification { protected int getNotificationId() { return ONGOING_NOTIFICATION_ID; } + + public void connecting() { + builder.setOngoing(true); + builder.setContentText(ctx.getString(R.string.connection_info_pending)); + notification = builder.build(); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index 246ea6d..80ff371 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -36,7 +36,7 @@ import ch.dissem.bitmessage.entity.Plaintext; import static ch.dissem.apps.abit.util.Drawables.toBitmap; public class NewMessageNotification extends AbstractNotification { - public static final int NEW_MESSAGE_NOTIFICATION_ID = 1; + private static final int NEW_MESSAGE_NOTIFICATION_ID = 1; private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD); public NewMessageNotification(Context ctx) { @@ -62,6 +62,7 @@ public class NewMessageNotification extends AbstractNotification { PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); + // TODO: add proper intents to reply/delete builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent); builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), pendingIntent); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 4eb3374..b585aeb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -196,7 +196,9 @@ public class AndroidAddressRepository implements AddressRepository { SQLiteDatabase db = sql.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); - values.put(COLUMN_ALIAS, address.getAlias()); + if (address.getAlias() != null) { + values.put(COLUMN_ALIAS, address.getAlias()); + } if (address.getPubkey() != null) { ByteArrayOutputStream out = new ByteArrayOutputStream(); address.getPubkey().writeUnencrypted(out); @@ -207,7 +209,9 @@ public class AndroidAddressRepository implements AddressRepository { if (address.getPrivateKey() != null) { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); } - values.put(COLUMN_CHAN, address.isChan()); + if (address.isChan()) { + values.put(COLUMN_CHAN, true); + } values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); int update = db.update(TABLE_NAME, values, "address=?", diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java index d27a821..6b944d2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidProofOfWorkRepository.java @@ -33,7 +33,6 @@ import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.Strings; import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static ch.dissem.bitmessage.utils.Strings.hex; diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index d2e04d8..799aa07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -21,11 +21,9 @@ import android.content.Intent; import android.os.Binder; import android.os.IBinder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.utils.Property; import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; @@ -35,12 +33,7 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIF * onPerformSync(). */ public class BitmessageService extends Service { - public static final Logger LOG = LoggerFactory.getLogger(BitmessageService.class); - - // Object to use as a thread-safe lock - private static final Object lock = new Object(); - - private static NetworkNotification notification = null; + private NetworkNotification notification = null; private static BitmessageContext bmc = null; private static volatile boolean running = false; @@ -51,11 +44,11 @@ public class BitmessageService extends Service { @Override public void onCreate() { - synchronized (lock) { + synchronized (BitmessageService.class) { if (bmc == null) { bmc = Singleton.getBitmessageContext(this); - notification = new NetworkNotification(this, bmc); } + notification = new NetworkNotification(this); } } @@ -70,7 +63,6 @@ public class BitmessageService extends Service { running = false; } - /** * Return an object that allows the system to invoke * the sync adapter. @@ -84,6 +76,7 @@ public class BitmessageService extends Service { public void startupNode() { startService(new Intent(BitmessageService.this, BitmessageService.class)); running = true; + notification.connecting(); startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); if (!bmc.isRunning()) { bmc.startup(); @@ -96,8 +89,17 @@ public class BitmessageService extends Service { bmc.shutdown(); } running = false; - stopForeground(false); + stopForeground(true); + notification.show(); stopSelf(); } } -} \ No newline at end of file + + public static Property getStatus() { + if (bmc != null) { + return bmc.status(); + } else { + return new Property("bitmessage context", null); + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index cb61cf3..c01f5c3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -40,7 +40,7 @@ public class ProofOfWorkService extends Service { private static ProofOfWorkEngine engine = new MultiThreadedPOWEngine(); private static boolean calculating; private static final Queue<PowItem> queue = new LinkedList<>(); - private static ProofOfWorkNotification notification; + private ProofOfWorkNotification notification; @Override public void onCreate() { @@ -55,16 +55,18 @@ public class ProofOfWorkService extends Service { public static class PowBinder extends Binder { private final ProofOfWorkService service; + private final ProofOfWorkNotification notification; private PowBinder(ProofOfWorkService service) { this.service = service; + this.notification = service.notification; } - public void process(PowItem item) { + void process(PowItem item) { synchronized (queue) { service.startService(new Intent(service, ProofOfWorkService.class)); service.startForeground(ONGOING_NOTIFICATION_ID, - notification.getNotification()); + notification.getNotification()); if (!calculating) { calculating = true; service.calculateNonce(item); @@ -90,28 +92,25 @@ public class ProofOfWorkService extends Service { } private void calculateNonce(final PowItem item) { - engine.calculateNonce(item.initialHash, item.targetValue, new ProofOfWorkEngine.Callback() { - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - try { - item.callback.onNonceCalculated(initialHash, nonce); - } finally { - PowItem item; - synchronized (queue) { - item = queue.poll(); - if (item == null) { - calculating = false; - stopForeground(true); - stopSelf(); - } else { - notification.update(queue.size()).show(); - } - } - if (item != null) { - calculateNonce(item); + engine.calculateNonce(item.initialHash, item.targetValue, (initialHash, nonce) -> { + try { + item.callback.onNonceCalculated(initialHash, nonce); + } finally { + PowItem next; + synchronized (queue) { + next = queue.poll(); + if (next == null) { + calculating = false; + stopForeground(true); + stopSelf(); + } else { + notification.update(queue.size()).show(); } } + if (next != null) { + calculateNonce(next); + } } }); } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 1bc0f77..a92521d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -46,7 +46,6 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; * Provides singleton objects across the application. */ public class Singleton { - public static final Object lock = new Object(); private static BitmessageContext bitmessageContext; private static MessageListener messageListener; private static BitmessageAddress identity; @@ -54,28 +53,28 @@ public class Singleton { public static BitmessageContext getBitmessageContext(Context context) { if (bitmessageContext == null) { - synchronized (lock) { + synchronized (Singleton.class) { if (bitmessageContext == null) { final Context ctx = context.getApplicationContext(); SqlHelper sqlHelper = new SqlHelper(ctx); powRepo = new AndroidProofOfWorkRepository(sqlHelper); TTL.pubkey(2 * DAY); bitmessageContext = new BitmessageContext.Builder() - .proofOfWorkEngine(new SwitchingProofOfWorkEngine( - ctx, Constants.PREFERENCE_SERVER_POW, - new ServerPowEngine(ctx), - new ServicePowEngine(ctx) - )) - .cryptography(new AndroidCryptography()) - .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) - .inventory(new AndroidInventory(sqlHelper)) - .addressRepo(new AndroidAddressRepository(sqlHelper)) - .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) - .powRepo(powRepo) - .networkHandler(new NioNetworkHandler()) - .listener(getMessageListener(ctx)) - .doNotSendPubkeyOnIdentityCreation() - .build(); + .proofOfWorkEngine(new SwitchingProofOfWorkEngine( + ctx, Constants.PREFERENCE_SERVER_POW, + new ServerPowEngine(ctx), + new ServicePowEngine(ctx) + )) + .cryptography(new AndroidCryptography()) + .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) + .inventory(new AndroidInventory(sqlHelper)) + .addressRepo(new AndroidAddressRepository(sqlHelper)) + .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) + .powRepo(powRepo) + .networkHandler(new NioNetworkHandler()) + .listener(getMessageListener(ctx)) + .doNotSendPubkeyOnIdentityCreation() + .build(); } } } @@ -108,11 +107,12 @@ public class Singleton { public static BitmessageAddress getIdentity(Context ctx) { if (identity == null) { + BitmessageContext bmc = getBitmessageContext(ctx); synchronized (Singleton.class) { if (identity == null) { - BitmessageContext bmc = getBitmessageContext(ctx); + // FIXME: this may block the UI, there must be a better way! List<BitmessageAddress> identities = bmc.addresses() - .getIdentities(); + .getIdentities(); if (identities.size() > 0) { identity = identities.get(0); } else { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index 66923cd..0653559 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; -import java.net.UnknownHostException; import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.listener.WifiReceiver; @@ -39,8 +38,6 @@ import static ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY; * @author Christian Basler */ public class Preferences { - private static Logger LOG = LoggerFactory.getLogger(Preferences.class); - public static boolean useTrustedNode(Context ctx) { String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); return trustedNode != null && !trustedNode.trim().isEmpty(); diff --git a/app/src/main/res/layout-w720dp/activity_message_list.xml b/app/src/main/res/layout-w720dp/activity_message_list.xml index 4e2005b..8c402c6 100644 --- a/app/src/main/res/layout-w720dp/activity_message_list.xml +++ b/app/src/main/res/layout-w720dp/activity_message_list.xml @@ -51,7 +51,8 @@ android:layout_margin="16dp" android:background="@color/contentBackground" android:elevation="2dp" - tools:layout="@layout/fragment_message_detail"/> + tools:layout="@layout/fragment_message_detail" + tools:ignore="InconsistentLayout,UnusedAttribute"/> </FrameLayout> </LinearLayout> diff --git a/app/src/main/res/layout/fragment_compose_message.xml b/app/src/main/res/layout/fragment_compose_message.xml index 35153ca..32ae0c3 100644 --- a/app/src/main/res/layout/fragment_compose_message.xml +++ b/app/src/main/res/layout/fragment_compose_message.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" android:hint="@string/to" android:inputType="textNoSuggestions" - android:singleLine="true"/> + android:maxLines="1"/> </android.support.design.widget.TextInputLayout> @@ -43,4 +43,4 @@ android:inputType="textMultiLine|textCapSentences" android:scrollbars="vertical"/> -</LinearLayout> \ No newline at end of file +</LinearLayout> diff --git a/app/src/main/res/layout/showcase_button.xml b/app/src/main/res/layout/showcase_button.xml index f8b0558..5772c83 100644 --- a/app/src/main/res/layout/showcase_button.xml +++ b/app/src/main/res/layout/showcase_button.xml @@ -1,14 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Got it" - android:textStyle="bold" android:background="@drawable/material_showcase_button_bg" - android:textSize="13sp" + android:text="Got it" android:textAllCaps="true" android:textColor="@android:color/white" - > - -</Button> \ No newline at end of file + android:textSize="13sp" + android:textStyle="bold"> +</Button> diff --git a/app/src/main/res/layout/subscription_row.xml b/app/src/main/res/layout/subscription_row.xml index c7d137d..8a40938 100644 --- a/app/src/main/res/layout/subscription_row.xml +++ b/app/src/main/res/layout/subscription_row.xml @@ -34,10 +34,11 @@ <TextView android:id="@+id/name" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_alignTop="@+id/avatar" android:layout_toEndOf="@+id/avatar" + android:layout_toStartOf="@+id/subscribed" android:ellipsize="end" android:lines="1" android:paddingBottom="0dp" @@ -60,7 +61,8 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceSmall" - tools:text="Stream #"/> + tools:text="Stream #" + tools:ignore="RelativeOverlap"/> <com.mikepenz.iconics.view.IconicsImageView android:id="@+id/subscribed" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4aef7aa..ae37f4a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,19 +5,15 @@ <string name="title_message_detail">Nachricht</string> <string name="title_subscription_detail">Abonnement</string> <string name="bitmessage_full_node">Bitmessage Netzknoten</string> - <string name="wifi_mode">Art der WLAN-Verbindung</string> <string name="settings">Einstellungen</string> <string name="wifi_only">Nur WLAN</string> <string name="wifi_only_summary">Nicht mit Mobilfunknetz verbinden</string> - <string name="subscriptions">Abonnements</string> <string name="to">An</string> <string name="subject">Betreff</string> <string name="manage_identity">Identität verwalten</string> <string name="add_identity">Identität hinzufügen</string> <string name="add_identity_summary">Eine neue Identität erstellen</string> - <string name="create_identity">Erstellen</string> <string name="create_identity_description">Eine neue, zufällige Identität erstellen</string> - <string name="import_identity">Importieren</string> <string name="import_identity_description">Eine existierende Identität von PyBitmessage oder einem Export importieren</string> <string name="add_deterministic_address">Deterministische Identität</string> <string name="add_deterministic_address_description">Eine deterministische Identität erstellen oder wiederherstellen</string> @@ -25,11 +21,8 @@ <string name="add_chan_description">Einen Chan erstellen oder einem beitreten</string> <string name="title_activity_open_bitmessage_link">Kontakt importieren</string> - <string name="action_settings">Einstellungen</string> <string name="connection_info_1">Stream %1$d: eine Verbindung</string> <string name="connection_info_n">Stream %1$d: %2$d Verbindungen</string> - <string name="import_address">Adresse importieren</string> - <string name="import_contact">Kontakt hinzufügen</string> <string name="label">Label</string> <string name="subscribe">Abonnieren</string> <string name="do_import">Importieren</string> @@ -42,7 +35,6 @@ <string name="archive">Archiv</string> <string name="empty_trash">Papierkorb leeren</string> <string name="stream_number">Stream %d</string> - <string name="enabled">Aktiv</string> <string name="trusted_node">Vertrauenswürdiger Knoten</string> <string name="trusted_node_summary">Diese Adresse wird für die Synchronisation verwendet</string> <string name="sync_timeout">Zeitbeschränkung der Synchronisierung</string> @@ -56,7 +48,6 @@ <string name="proof_of_work_text_0">Arbeite am Versenden</string> <string name="proof_of_work_text_n">Arbeite am Versenden (%1$d in Warteschlange)</string> <string name="error_invalid_sync_port">Ungültiger Port in den Synchronisationseinstellungen: %s</string> - <string name="error_invalid_sync_host">Synchronisation fehlgeschlagen: der vertrauenswürdige Knoten konnte nicht erreicht werden.</string> <string name="compose_body_hint">Nachricht schreiben</string> <string name="contacts_and_subscriptions">Kontakte</string> <string name="subscribed">Abonniert</string> @@ -78,10 +69,9 @@ <string name="delete_contact_warning">Bist Du sicher dass dieser Kontakt gelöscht werden soll?</string> <string name="scan_qr_code">QR-Code scannen</string> <string name="create_contact">Kontakt erfassen</string> - <string name="full_node_description">Solange kein aktiver Knoten gestartet ist, werden keine Meldungen empfangen oder gesendet. Dies braucht jedoch viele Resourcen und Daten. + <string name="full_node_description">Solange kein aktiver Knoten gestartet ist, werden keine Meldungen empfangen oder gesendet. Dies braucht jedoch viele Ressourcen und Daten. Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfiguriert werden, aber im Moment muss dieser selbst bereitgestellt werden.</string> - <string name="got_it">Alles klar</string> <string name="address">Bitmessage-Adresse</string> <string name="error_illegal_address">Vielleicht hat es einen Tippfehler</string> <string name="export">Exportieren</string> @@ -95,7 +85,7 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="toast_identity_created">Identität erstellt</string> <string name="toast_identities_created">Identitäten erstellt</string> <string name="toast_chan_created">Chan erstellt</string> - <string name="deterministic_address_warning">Merke dir diese Enstellungen und stelle sicher dass sie korrekt sind wenn du eine deterministische Addresse wiederherstellst.</string> + <string name="deterministic_address_warning">Merke dir diese Enstellungen und stelle sicher dass sie korrekt sind wenn du eine deterministische Adresse wiederherstellst.</string> <string name="number_of_identities">Anzahl zu generierender Identitäten</string> <string name="shorter">Kürzere Adressen suchen</string> <string name="wif_string">WIF / Inhalt von ‘keys.dat’</string> @@ -106,5 +96,4 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="select_file_title">Datei auswählen</string> <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer ‘keys.dat’-Datei einfügen</string> - <string name="name">Name</string> </resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f2f59f8..ae469d9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Palette generated by Material Palette - materialpalette.com/blue-grey/orange --> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <color name="colorPrimary">#FFC107</color> <color name="colorPrimaryDark">#FFA000</color> <color name="colorPrimaryDarkText">#DEFFFFFF</color> diff --git a/app/src/main/res/values/library_fab_speed_dial_strings.xml b/app/src/main/res/values/library_fab_speed_dial_strings.xml index bdc1c1f..976a0f6 100644 --- a/app/src/main/res/values/library_fab_speed_dial_strings.xml +++ b/app/src/main/res/values/library_fab_speed_dial_strings.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <string name="define_fab_speed_dial" translatable="false"/> <!-- Author section --> <string name="library_fab_speed_dial_author" translatable="false">Yavor Ivanov</string> diff --git a/app/src/main/res/values/library_filepicker_strings.xml b/app/src/main/res/values/library_filepicker_strings.xml index 23ba3b6..aa3b993 100644 --- a/app/src/main/res/values/library_filepicker_strings.xml +++ b/app/src/main/res/values/library_filepicker_strings.xml @@ -1,21 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <string name="define_int_filepicker" translatable="false"/> - <!-- Author section --> - <string name="library_filepicker_author" translatable="false">Angad Singh</string> - <string name="library_filepicker_authorWebsite" translatable="false">https://github.com/Angads25</string> - <!-- Library section --> - <string name="library_filepicker_libraryName" translatable="false">FilePicker</string> - <string name="library_filepicker_libraryDescription">Android Library to select files/directories from Device Storage.</string> - <string name="library_filepicker_libraryWebsite" translatable="false">https://github.com/Angads25/android-filepicker</string> - <string name="library_filepicker_libraryVersion" translatable="false">1.0.6</string> - <!-- OpenSource section --> - <string name="library_filepicker_isOpenSource" translatable="false">true</string> - <string name="library_filepicker_repositoryLink" translatable="false">https://github.com/Angads25/android-filepicker</string> - <!-- ClassPath for autoDetect section --> - <string name="library_filepicker_classPath" translatable="false">com.github.angads25.filepicker.view.FilePickerDialog</string> - <!-- License section --> - <string name="library_filepicker_licenseId" translatable="false">apache_2_0</string> - <!-- Custom variables section --> + <!-- Author section --> + <string name="library_filepicker_author" translatable="false">Angad Singh</string> + <string name="library_filepicker_authorWebsite" translatable="false">https://github.com/Angads25</string> + <!-- Library section --> + <string name="library_filepicker_libraryName" translatable="false">FilePicker</string> + <string name="library_filepicker_libraryDescription">Android Library to select files/directories from Device Storage.</string> + <string name="library_filepicker_libraryWebsite" translatable="false">https://github.com/Angads25/android-filepicker</string> + <string name="library_filepicker_libraryVersion" translatable="false">1.0.6</string> + <!-- OpenSource section --> + <string name="library_filepicker_isOpenSource" translatable="false">true</string> + <string name="library_filepicker_repositoryLink" translatable="false">https://github.com/Angads25/android-filepicker</string> + <!-- ClassPath for autoDetect section --> + <string name="library_filepicker_classPath" translatable="false">com.github.angads25.filepicker.view.FilePickerDialog</string> + <!-- License section --> + <string name="library_filepicker_licenseId" translatable="false">apache_2_0</string> + <!-- Custom variables section --> </resources> diff --git a/app/src/main/res/values/library_jabit_strings.xml b/app/src/main/res/values/library_jabit_strings.xml index c12f0b6..42a2d5c 100644 --- a/app/src/main/res/values/library_jabit_strings.xml +++ b/app/src/main/res/values/library_jabit_strings.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <string name="define_jabit" translatable="false"/> <!-- Author section --> <string name="library_jabit_author" translatable="false">Christian Basler</string> diff --git a/app/src/main/res/values/library_zxing_strings.xml b/app/src/main/res/values/library_zxing_strings.xml index 49e7177..14ba997 100644 --- a/app/src/main/res/values/library_zxing_strings.xml +++ b/app/src/main/res/values/library_zxing_strings.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <string name="define_zxing" translatable="false"/> <!-- Author section --> <string name="library_zxing_author" translatable="false">Google, Inc.</string> diff --git a/app/src/main/res/values/library_zxingandroidembedded_strings.xml b/app/src/main/res/values/library_zxingandroidembedded_strings.xml index 3540706..d441c48 100644 --- a/app/src/main/res/values/library_zxingandroidembedded_strings.xml +++ b/app/src/main/res/values/library_zxingandroidembedded_strings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources"> <string name="define_int_zxingandroidembedded" translatable="false"/> <!-- Author section --> <string name="library_zxingandroidembedded_author" translatable="false">JourneyApps</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54fdc10..dd448d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,22 +1,18 @@ -<resources> +<resources xmlns:tools="http://schemas.android.com/tools"> <string name="app_name">Abit</string> <string name="about_app">A Bitmessage client for Android</string> <string name="title_message_detail">Message Detail</string> <string name="title_subscription_detail">Subscription Detail</string> <string name="bitmessage_full_node">Bitmessage Node</string> - <string name="wifi_mode">Wi-Fi Connection Mode</string> <string name="settings">Settings</string> <string name="wifi_only">Wi-Fi only</string> <string name="wifi_only_summary">Don\'t connect to the mobile network</string> - <string name="subscriptions">Subscriptions</string> <string name="to">To</string> <string name="subject">Subject</string> <string name="manage_identity">Manage Identity</string> <string name="add_identity">Add Identity</string> <string name="add_identity_summary">Create new identity</string> - <string name="create_identity">Create new</string> <string name="create_identity_description">Create a new, random identity</string> - <string name="import_identity">Import existing</string> <string name="import_identity_description">Import an existing identity from PyBitmessage or from an export</string> <string name="add_deterministic_address">Deterministic identity</string> <string name="add_deterministic_address_description">Create or recreate a deterministic identity</string> @@ -24,24 +20,20 @@ <string name="add_chan_description">Create or join a chan</string> <string name="title_activity_open_bitmessage_link">Import Contact</string> - <string name="action_settings">Settings</string> <string name="connection_info_1">Stream #%1$d: one connection</string> - <string name="connection_info_n">Stream #%1$d: %2$d connections</string> - <string name="import_address">Import Address</string> - <string name="import_contact">Add to contacts</string> + <string name="connection_info_n" tools:ignore="PluralsCandidate">Stream #%1$d: %2$d connections</string> <string name="label">Label</string> <string name="subscribe">Subscribe</string> <string name="do_import">Import</string> <string name="cancel">Cancel</string> <string name="broadcast">Broadcast</string> - <string name="n_new_messages">%d new messages</string> + <string name="n_new_messages" tools:ignore="PluralsCandidate">%d new messages</string> <string name="reply">Reply</string> <string name="delete">Delete</string> <string name="mark_unread">Mark unread</string> <string name="archive">Archive</string> <string name="empty_trash">Empty Trash</string> <string name="stream_number">Stream #%d</string> - <string name="enabled">Enabled</string> <string name="trusted_node">Trusted node</string> <string name="trusted_node_summary">Use this node for synchronization</string> <string name="sync_timeout">Synchronization Timeout</string> @@ -53,9 +45,8 @@ <string name="connection_info_pending">Connecting…</string> <string name="proof_of_work_title">Proof of Work</string> <string name="proof_of_work_text_0">Doing work to send message</string> - <string name="proof_of_work_text_n">Doing work to send message (%1$d queued)</string> + <string name="proof_of_work_text_n" tools:ignore="PluralsCandidate">Doing work to send message (%1$d queued)</string> <string name="error_invalid_sync_port">Invalid port in synchronization settings: %s</string> - <string name="error_invalid_sync_host">Synchronization failed: Trusted node could not be reached.</string> <string name="compose_body_hint">Write message</string> <string name="contacts_and_subscriptions">Contacts</string> <string name="subscribed">Subscribed</string> @@ -80,7 +71,6 @@ <string name="full_node_description">You can\'t receive or send messages unless you start a full node. But be aware that this uses a lot of resources and internet traffic. As an alternative you could configure a trusted node in the settings, but as of now you\'ll need to deploy your own.</string> - <string name="got_it">Got it</string> <string name="address">Bitmessage Address</string> <string name="error_illegal_address">There might be a typo</string> <string name="export">Export</string> @@ -105,5 +95,4 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="select_file_title">Select a File</string> <string name="select_identities_to_import">Please select the identities you want to import:</string> <string name="import_input_description">You can just paste the contents of an export or a ‘keys.dat’ file</string> - <string name="name">Name</string> </resources> diff --git a/build.gradle b/build.gradle index c9dbf0f..ec2dca6 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From b3dd53a5df82d75da05a5735f3660aea453174c5 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Oct 2016 05:49:07 +0200 Subject: [PATCH 075/110] Lint fixes --- .../apps/abit/AbstractItemListFragment.java | 2 +- .../dissem/apps/abit/AddressListFragment.java | 5 - .../apps/abit/CreateAddressActivity.java | 43 +++--- .../ch/dissem/apps/abit/DetailActivity.java | 2 +- .../java/ch/dissem/apps/abit/Identicon.java | 8 +- .../ch/dissem/apps/abit/InputWifFragment.java | 55 ++++---- .../dissem/apps/abit/MessageListFragment.java | 17 +-- .../ch/dissem/apps/abit/SettingsFragment.java | 38 +++-- .../abit/adapter/AddressSelectorAdapter.java | 14 +- .../abit/adapter/SwipeableMessageAdapter.java | 26 ++-- .../dialog/AddIdentityDialogFragment.java | 133 ++++++++---------- .../apps/abit/listener/WifiReceiver.java | 12 +- .../abit/notification/ErrorNotification.java | 2 +- .../dissem/apps/abit/pow/ServerPowEngine.java | 54 +++---- .../repository/AndroidAddressRepository.java | 16 +-- .../repository/AndroidMessageRepository.java | 6 +- .../apps/abit/repository/SqlHelper.java | 8 +- .../apps/abit/service/ProofOfWorkService.java | 4 +- .../apps/abit/service/ServicePowEngine.java | 6 +- .../ch/dissem/apps/abit/util/Preferences.java | 3 - 20 files changed, 192 insertions(+), 262 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 5432210..df96b43 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -37,7 +37,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement * A dummy implementation of the {@link ListSelectionListener} interface that does * nothing. Used only when this fragment is not attached to an activity. */ - private static ListSelectionListener<Object> dummyCallbacks = plaintext -> { + private static final ListSelectionListener<Object> dummyCallbacks = plaintext -> { }; /** * The fragment's current callback object, which is notified of list item diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 5168422..57aa3dd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -144,11 +144,6 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr return view; } - @Override - public void startActivityForResult(Intent intent, int requestCode) { - super.startActivityForResult(intent, requestCode); - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (data != null && data.hasExtra("SCAN_RESULT")) { diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index 1f6065a..eff13da 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -20,7 +20,6 @@ import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; -import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Switch; @@ -61,34 +60,28 @@ public class CreateAddressActivity extends AppCompatActivity { } final Button cancel = (Button) findViewById(R.id.cancel); - cancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setResult(Activity.RESULT_CANCELED); - finish(); - } + cancel.setOnClickListener(v -> { + setResult(Activity.RESULT_CANCELED); + finish(); }); final Button ok = (Button) findViewById(R.id.do_import); - ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String addressText = String.valueOf(address.getText()).trim(); - try { - BitmessageAddress bmAddress = new BitmessageAddress(addressText); - bmAddress.setAlias(label.getText().toString()); + ok.setOnClickListener(v -> { + String addressText = String.valueOf(address.getText()).trim(); + try { + BitmessageAddress bmAddress = new BitmessageAddress(addressText); + bmAddress.setAlias(label.getText().toString()); - BitmessageContext bmc = Singleton.getBitmessageContext - (CreateAddressActivity.this); - bmc.addContact(bmAddress); - if (subscribe.isChecked()) { - bmc.addSubscribtion(bmAddress); - } - - setResult(Activity.RESULT_OK); - finish(); - } catch (RuntimeException e) { - address.setError(getString(R.string.error_illegal_address)); + BitmessageContext bmc = Singleton.getBitmessageContext + (CreateAddressActivity.this); + bmc.addContact(bmAddress); + if (subscribe.isChecked()) { + bmc.addSubscribtion(bmAddress); } + + setResult(Activity.RESULT_OK); + finish(); + } catch (RuntimeException e) { + address.setError(getString(R.string.error_illegal_address)); } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java index fb94ef2..f725c46 100644 --- a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java @@ -12,7 +12,7 @@ import com.mikepenz.materialize.MaterializeBuilder; /** * @author Christian Basler */ -public class DetailActivity extends AppCompatActivity { +public abstract class DetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index a8da2c3..fc6122c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -31,10 +31,10 @@ public class Identicon extends Drawable { private static final int CENTER_COLUMN = 5; private final Paint paint; - private int color; - private int background; - private boolean[][] fields; - private boolean chan; + private final int color; + private final int background; + private final boolean[][] fields; + private final boolean chan; private final TextPaint textPaint; public Identicon(BitmessageAddress input) { diff --git a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java index 8ccdf5d..d1498ed 100644 --- a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java @@ -28,7 +28,6 @@ import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; -import com.github.angads25.filepicker.controller.DialogSelectionListener; import com.github.angads25.filepicker.model.DialogConfigs; import com.github.angads25.filepicker.model.DialogProperties; import com.github.angads25.filepicker.view.FilePickerDialog; @@ -61,19 +60,16 @@ public class InputWifFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_import_input, container, false); wifData = (TextView) view.findViewById(R.id.wif_input); - view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Bundle bundle = new Bundle(); - bundle.putString(WIF_DATA, wifData.getText().toString()); + view.findViewById(R.id.next).setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(WIF_DATA, wifData.getText().toString()); - ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); - fragment.setArguments(bundle); + ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); + fragment.setArguments(bundle); - getFragmentManager().beginTransaction() - .replace(R.id.content, fragment) - .commit(); - } + getFragmentManager().beginTransaction() + .replace(R.id.content, fragment) + .commit(); }); return view; } @@ -93,26 +89,23 @@ public class InputWifFragment extends Fragment { properties.extensions = null; FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties); dialog.setTitle(getString(R.string.select_file_title)); - dialog.setDialogSelectionListener(new DialogSelectionListener() { - @Override - public void onSelectedFilePaths(String[] files) { - if (files.length > 0) { - try (InputStream in = new FileInputStream(files[0])) { - ByteArrayOutputStream data = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - //noinspection ConstantConditions - while ((length = in.read(buffer)) != -1) { - data.write(buffer, 0, length); - } - wifData.setText(data.toString("UTF-8")); - } catch (IOException e) { - Toast.makeText( - getActivity(), - R.string.error_loading_data, - Toast.LENGTH_SHORT - ).show(); + dialog.setDialogSelectionListener(files -> { + if (files.length > 0) { + try (InputStream in = new FileInputStream(files[0])) { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + //noinspection ConstantConditions + while ((length = in.read(buffer)) != -1) { + data.write(buffer, 0, length); } + wifData.setText(data.toString("UTF-8")); + } catch (IOException e) { + Toast.makeText( + getActivity(), + R.string.error_loading_data, + Toast.LENGTH_SHORT + ).show(); } } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 9328ce2..0d22d98 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -130,15 +130,12 @@ public class MessageListFragment extends Fragment implements ListHolder { // Show the dummy content as text in a TextView. FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id .fab_compose_message); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(getActivity().getApplicationContext(), - ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity - (getActivity())); - startActivity(intent); - } + fab.setOnClickListener(view -> { + Intent intent = new Intent(getActivity().getApplicationContext(), + ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity + (getActivity())); + startActivity(intent); }); // touch guard manager (this class is required to suppress scrolling while swipe-dismiss @@ -173,7 +170,7 @@ public class MessageListFragment extends Fragment implements ListHolder { } @Override - public void onItemViewClicked(View v, boolean pinned) { + public void onItemViewClicked(View v) { int position = recyclerView.getChildAdapterPosition(v); adapter.setSelectedPosition(position); if (position != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 60ecbeb..c905f65 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -36,8 +36,8 @@ import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; * @author Christian Basler */ public class SettingsFragment - extends PreferenceFragment - implements SharedPreferences.OnSharedPreferenceChangeListener { + extends PreferenceFragment + implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -46,27 +46,21 @@ public class SettingsFragment addPreferencesFromResource(R.xml.preferences); Preference about = findPreference("about"); - about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new LibsBuilder() - .withActivityTitle(getActivity().getString(R.string.about)) - .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) - .withAboutIconShown(true) - .withAboutVersionShown(true) - .withAboutDescription(getString(R.string.about_app)) - .start(getActivity()); - return true; - } + about.setOnPreferenceClickListener(preference -> { + new LibsBuilder() + .withActivityTitle(getActivity().getString(R.string.about)) + .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .withAboutDescription(getString(R.string.about_app)) + .start(getActivity()); + return true; }); Preference status = findPreference("status"); - status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - startActivity(new Intent(getActivity(), StatusActivity.class)); - return true; - } + status.setOnPreferenceClickListener(preference -> { + startActivity(new Intent(getActivity(), StatusActivity.class)); + return true; }); } @@ -74,7 +68,7 @@ public class SettingsFragment public void onAttach(Context ctx) { super.onAttach(ctx); PreferenceManager.getDefaultSharedPreferences(ctx) - .registerOnSharedPreferenceChangeListener(this); + .registerOnSharedPreferenceChangeListener(this); } @Override @@ -97,4 +91,4 @@ public class SettingsFragment break; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java index 4103097..f35a52a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java @@ -21,7 +21,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.TextView; import java.util.ArrayList; @@ -69,19 +68,16 @@ public class AddressSelectorAdapter static class ViewHolder extends RecyclerView.ViewHolder { public Selectable<BitmessageAddress> data; - public CheckBox checkbox; - public TextView address; + public final CheckBox checkbox; + public final TextView address; private ViewHolder(View v) { super(v); checkbox = (CheckBox) v.findViewById(R.id.checkbox); address = (TextView) v.findViewById(R.id.address); - checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (data != null) { - data.selected = isChecked; - } + checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (data != null) { + data.selected = isChecked; } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index 2e4775b..53db232 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -59,8 +59,8 @@ public class SwipeableMessageAdapter private List<Plaintext> data = Collections.emptyList(); private EventListener eventListener; - private View.OnClickListener itemViewOnClickListener; - private View.OnClickListener swipeableViewContainerOnClickListener; + private final View.OnClickListener itemViewOnClickListener; + private final View.OnClickListener swipeableViewContainerOnClickListener; private Label label; private int selectedPosition; @@ -75,12 +75,12 @@ public class SwipeableMessageAdapter void onItemArchived(Plaintext item); - void onItemViewClicked(View v, boolean pinned); + void onItemViewClicked(View v); } @SuppressWarnings("WeakerAccess") static class ViewHolder extends AbstractSwipeableItemViewHolder { - public FrameLayout container; + public final FrameLayout container; public final ImageView avatar; public final TextView sender; public final TextView subject; @@ -102,18 +102,8 @@ public class SwipeableMessageAdapter } public SwipeableMessageAdapter() { - itemViewOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - onItemViewClick(v); - } - }; - swipeableViewContainerOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - onSwipeableViewContainerClick(v); - } - }; + itemViewOnClickListener = this::onItemViewClick; + swipeableViewContainerOnClickListener = this::onSwipeableViewContainerClick; // SwipeableItemAdapter requires stable ID, and also // have to implement the getItemId() method appropriately. @@ -127,14 +117,14 @@ public class SwipeableMessageAdapter private void onItemViewClick(View v) { if (eventListener != null) { - eventListener.onItemViewClicked(v, true); // pinned + eventListener.onItemViewClicked(v); } } private void onSwipeableViewContainerClick(View v) { if (eventListener != null) { eventListener.onItemViewClicked( - RecyclerViewAdapterUtils.getParentViewHolderItemView(v), false); // not pinned + RecyclerViewAdapterUtils.getParentViewHolderItemView(v)); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java index 710ead0..59e16b6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java @@ -19,7 +19,6 @@ package ch.dissem.apps.abit.dialog; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -61,56 +60,47 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { getDialog().setTitle(R.string.add_identity); View view = inflater.inflate(R.layout.dialog_add_identity, container, false); final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); - view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - final Context ctx = getActivity().getBaseContext(); - switch (radioGroup.getCheckedRadioButtonId()) { - case R.id.create_identity: - Toast.makeText(ctx, - R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Void, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); - } + view.findViewById(R.id.ok).setOnClickListener(v -> { + final Context ctx = getActivity().getBaseContext(); + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.create_identity: + Toast.makeText(ctx, + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); + } - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(ctx, - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(chan); - } + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(ctx, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); } - }.execute(); - break; - case R.id.import_identity: - startActivity(new Intent(ctx, ImportIdentityActivity.class)); - break; - case R.id.add_chan: - addChanDialog(); - break; - case R.id.add_deterministic_address: - new DeterministicIdentityDialogFragment().show(getFragmentManager(), - "dialog"); - break; - default: - return; - } - dismiss(); + } + }.execute(); + break; + case R.id.import_identity: + startActivity(new Intent(ctx, ImportIdentityActivity.class)); + break; + case R.id.add_chan: + addChanDialog(); + break; + case R.id.add_deterministic_address: + new DeterministicIdentityDialogFragment().show(getFragmentManager(), + "dialog"); + break; + default: + return; } + dismiss(); }); - view.findViewById(R.id.dismiss) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); + view.findViewById(R.id.dismiss).setOnClickListener(v -> dismiss()); return view; } @@ -123,34 +113,31 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { new AlertDialog.Builder(activity) .setTitle(R.string.add_chan) .setView(dialogView) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - Toast.makeText(ctx, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<String, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(String... args) { - String pass = args[0]; - BitmessageAddress chan = bmc.createChan(pass); - chan.setAlias(pass); - bmc.addresses().save(chan); - return chan; - } + .setPositiveButton(R.string.ok, (dialogInterface, i) -> { + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + Toast.makeText(ctx, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<String, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(String... args) { + String pass = args[0]; + BitmessageAddress chan = bmc.createChan(pass); + chan.setAlias(pass); + bmc.addresses().save(chan); + return chan; + } - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(ctx, - R.string.toast_chan_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(chan); - } + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(ctx, + R.string.toast_chan_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); } - }.execute(passphrase.getText().toString()); - } + } + }.execute(passphrase.getText().toString()); }) .setNegativeButton(R.string.cancel, null) .show(); diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java index ccd81ac..329d903 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/WifiReceiver.java @@ -29,11 +29,13 @@ import ch.dissem.bitmessage.BitmessageContext; public class WifiReceiver extends BroadcastReceiver { @Override public void onReceive(Context ctx, Intent intent) { - if (Preferences.isWifiOnly(ctx)) { - BitmessageContext bmc = Singleton.getBitmessageContext(ctx); + if ("android.net.conn.CONNECTIVITY_CHANGE".equals(intent.getAction())) { + if (Preferences.isWifiOnly(ctx)) { + BitmessageContext bmc = Singleton.getBitmessageContext(ctx); - if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) { - bmc.shutdown(); + if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) { + bmc.shutdown(); + } } } } @@ -43,7 +45,7 @@ public class WifiReceiver extends BroadcastReceiver { if (netInfo == null || !netInfo.isConnectedOrConnecting()) { return false; } - switch (netInfo.getType()){ + switch (netInfo.getType()) { case ConnectivityManager.TYPE_ETHERNET: case ConnectivityManager.TYPE_WIFI: return false; diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java index 0efdc66..e7a0654 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java @@ -31,7 +31,7 @@ import ch.dissem.apps.abit.R; public class ErrorNotification extends AbstractNotification { public static final int ERROR_NOTIFICATION_ID = 4; - private NotificationCompat.Builder builder; + private final NotificationCompat.Builder builder; public ErrorNotification(Context ctx) { super(ctx); diff --git a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java index 1dd81bb..e8812ea 100644 --- a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java @@ -17,14 +17,12 @@ package ch.dissem.apps.abit.pow; import android.content.Context; -import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; @@ -42,7 +40,7 @@ import static ch.dissem.bitmessage.utils.Singleton.cryptography; * @author Christian Basler */ public class ServerPowEngine implements ProofOfWorkEngine, InternalContext - .ContextHolder { + .ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(ServerPowEngine.class); private final Context ctx; @@ -52,40 +50,34 @@ public class ServerPowEngine implements ProofOfWorkEngine, InternalContext public ServerPowEngine(Context ctx) { this.ctx = ctx; - pool = Executors.newCachedThreadPool(new ThreadFactory() { - @Override - public Thread newThread(@NonNull Runnable r) { - Thread thread = Executors.defaultThreadFactory().newThread(r); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - } + pool = Executors.newCachedThreadPool(r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; }); } @Override public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) { - pool.execute(new Runnable() { - @Override - public void run() { - BitmessageAddress identity = Singleton.getIdentity(ctx); - if (identity == null) throw new RuntimeException("No Identity for calculating POW"); + pool.execute(() -> { + BitmessageAddress identity = Singleton.getIdentity(ctx); + if (identity == null) throw new RuntimeException("No Identity for calculating POW"); - ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, - CALCULATE, target); - SyncAdapter.startPowSync(ctx); - try { - CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> - (request); - cryptoMsg.signAndEncrypt( - identity, - cryptography().createPublicKey(identity.getPublicDecryptionKey()) - ); - context.getNetworkHandler().send( - Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), - cryptoMsg); - } catch (Exception e) { - LOG.error(e.getMessage(), e); - } + ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, + CALCULATE, target); + SyncAdapter.startPowSync(ctx); + try { + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> + (request); + cryptoMsg.signAndEncrypt( + identity, + cryptography().createPublicKey(identity.getPublicDecryptionKey()) + ); + context.getNetworkHandler().send( + Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), + cryptoMsg); + } catch (Exception e) { + LOG.error(e.getMessage(), e); } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index b585aeb..5557112 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -169,14 +169,10 @@ public class AndroidAddressRepository implements AddressRepository { @Override public void save(BitmessageAddress address) { - try { - if (exists(address)) { - update(address); - } else { - insert(address); - } - } catch (IOException e) { - LOG.error(e.getMessage(), e); + if (exists(address)) { + update(address); + } else { + insert(address); } } @@ -191,7 +187,7 @@ public class AndroidAddressRepository implements AddressRepository { } } - private void update(BitmessageAddress address) throws IOException { + private void update(BitmessageAddress address) { try { SQLiteDatabase db = sql.getWritableDatabase(); // Create a new map of values, where column names are the keys @@ -224,7 +220,7 @@ public class AndroidAddressRepository implements AddressRepository { } } - private void insert(BitmessageAddress address) throws IOException { + private void insert(BitmessageAddress address) { try { SQLiteDatabase db = sql.getWritableDatabase(); // Create a new map of values, where column names are the keys diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index f430f66..8c6aad8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -275,14 +275,12 @@ public class AndroidMessageRepository extends AbstractMessageRepository { db.setTransactionSuccessful(); } catch (SQLiteConstraintException e) { LOG.trace(e.getMessage(), e); - } catch (IOException e) { - LOG.error(e.getMessage(), e); } finally { db.endTransaction(); } } - private void insert(SQLiteDatabase db, Plaintext message) throws IOException { + private void insert(SQLiteDatabase db, Plaintext message) { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message .getInventoryVector().getHash()); @@ -302,7 +300,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { message.setId(id); } - private void update(SQLiteDatabase db, Plaintext message) throws IOException { + private void update(SQLiteDatabase db, Plaintext message) { ContentValues values = new ContentValues(); values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message .getInventoryVector().getHash()); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 87a2bc2..5e6a68c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -27,10 +27,10 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - public static final int DATABASE_VERSION = 5; - public static final String DATABASE_NAME = "jabit.db"; + private static final int DATABASE_VERSION = 5; + private static final String DATABASE_NAME = "jabit.db"; - protected final Context ctx; + private final Context ctx; public SqlHelper(Context ctx) { super(ctx, DATABASE_NAME, null, DATABASE_VERSION); @@ -65,7 +65,7 @@ public class SqlHelper extends SQLiteOpenHelper { } } - protected void executeMigration(SQLiteDatabase db, String name) { + private void executeMigration(SQLiteDatabase db, String name) { for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) { db.execSQL(statement); } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index c01f5c3..3b41df7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -37,9 +37,9 @@ import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_N */ public class ProofOfWorkService extends Service { // Object to use as a thread-safe lock - private static ProofOfWorkEngine engine = new MultiThreadedPOWEngine(); - private static boolean calculating; + private static final ProofOfWorkEngine engine = new MultiThreadedPOWEngine(); private static final Queue<PowItem> queue = new LinkedList<>(); + private static boolean calculating; private ProofOfWorkNotification notification; @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java index 7211dcf..b7f0a84 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ServicePowEngine.java @@ -38,14 +38,14 @@ public class ServicePowEngine implements ProofOfWorkEngine { private final Context ctx; private static final Object lock = new Object(); - private Queue<PowItem> queue = new LinkedList<>(); + private final Queue<PowItem> queue = new LinkedList<>(); private PowBinder service; public ServicePowEngine(Context ctx) { this.ctx = ctx; } - private ServiceConnection connection = new ServiceConnection() { + private final ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (lock) { @@ -75,4 +75,4 @@ public class ServicePowEngine implements ProofOfWorkEngine { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index 0653559..e5d52ba 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -20,9 +20,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.InetAddress; From 6a8648ca288a14029b7115d13611b14e461239e7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Oct 2016 12:53:35 +0200 Subject: [PATCH 076/110] Added actions to notifications (this required a slight detour) --- app/src/main/AndroidManifest.xml | 16 +++- .../apps/abit/ComposeMessageActivity.java | 36 +++++++- .../ch/dissem/apps/abit/MainActivity.java | 85 +++++-------------- .../apps/abit/MessageDetailFragment.java | 31 +------ .../abit/dialog/FullNodeDialogActivity.java | 44 ++++++++++ .../notification/NetworkNotification.java | 50 ++++++++--- .../notification/NewMessageNotification.java | 55 +++++++----- .../abit/service/BitmessageIntentService.java | 72 ++++++++++++++++ .../apps/abit/service/BitmessageService.java | 60 +++++-------- .../drawable/ic_notification_node_start.xml | 25 ++++++ .../drawable/ic_notification_node_stop.xml | 25 ++++++ .../main/res/layout/dialog_add_identity.xml | 5 +- app/src/main/res/layout/dialog_full_node.xml | 60 +++++++++++++ app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 15 files changed, 405 insertions(+), 165 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java create mode 100644 app/src/main/res/drawable/ic_notification_node_start.xml create mode 100644 app/src/main/res/drawable/ic_notification_node_stop.xml create mode 100644 app/src/main/res/layout/dialog_full_node.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90f7d6f..a7a2f74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,6 +47,10 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity"/> </activity> + <activity + android:name=".dialog.FullNodeDialogActivity" + android:label="@string/full_node" + android:theme="@style/Theme.AppCompat.Light.Dialog"/> <activity android:name=".ComposeMessageActivity" android:label="@string/compose_message" @@ -125,8 +129,12 @@ </intent-filter> </activity> - <service android:name=".service.BitmessageService"/> - <service android:name=".service.ProofOfWorkService"/> + <service + android:name=".service.BitmessageService" + android:exported="false"/> + <service + android:name=".service.ProofOfWorkService" + android:exported="false"/> <!-- Synchronization --> <provider @@ -137,6 +145,7 @@ <service android:name=".synchronization.AuthenticatorService" + android:exported="true" tools:ignore="ExportedService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator"/> @@ -158,6 +167,9 @@ android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter"/> </service> + <service + android:name=".service.BitmessageIntentService" + android:exported="false"/> <!-- Receive Wi-Fi connection state changes --> <receiver android:name=".listener.WifiReceiver"> diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index c5f2032..97a18c1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -16,10 +16,16 @@ package ch.dissem.apps.abit; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import ch.dissem.bitmessage.entity.Plaintext; + /** * Compose a new message. */ @@ -46,7 +52,33 @@ public class ComposeMessageActivity extends AppCompatActivity { ComposeMessageFragment fragment = new ComposeMessageFragment(); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager().beginTransaction() - .replace(R.id.content, fragment) - .commit(); + .replace(R.id.content, fragment) + .commit(); + } + + public static void launchReplyTo(Fragment fragment, Plaintext item) { + fragment.startActivity(getReplyIntent(fragment.getActivity(), item)); + } + + public static void launchReplyTo(Activity activity, Plaintext item) { + activity.startActivity(getReplyIntent(activity, item)); + } + + private static Intent getReplyIntent(Context ctx, Plaintext item) { + Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class); + replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); + replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); + String prefix; + if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) + .equalsIgnoreCase("RE:")) { + prefix = ""; + } else { + prefix = "RE: "; + } + replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); + replyIntent.putExtra(EXTRA_CONTENT, + "\n\n------------------------------------------------------\n" + + item.getText()); + return replyIntent; } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 10dc12f..7e418c2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -17,19 +17,14 @@ package ch.dissem.apps.abit; import android.app.AlertDialog; -import android.content.ComponentName; -import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; import android.graphics.Point; import android.os.Bundle; -import android.os.IBinder; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; import android.widget.RelativeLayout; import com.github.amlcurran.showcaseview.ShowcaseView; @@ -59,10 +54,10 @@ import java.util.Collection; import java.util.List; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; +import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.BitmessageService; -import ch.dissem.apps.abit.service.BitmessageService.BitmessageBinder; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; import ch.dissem.apps.abit.util.Preferences; @@ -71,6 +66,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -94,6 +90,7 @@ import static ch.dissem.apps.abit.service.BitmessageService.isRunning; public class MainActivity extends AppCompatActivity implements ListSelectionListener<Serializable>, ActionBarListener { public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"; + public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); @@ -110,22 +107,6 @@ public class MainActivity extends AppCompatActivity */ private boolean twoPane; - private static BitmessageBinder service; - private static boolean bound; - private static ServiceConnection connection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - MainActivity.service = (BitmessageBinder) service; - MainActivity.bound = true; - } - - @Override - public void onServiceDisconnected(ComponentName name) { - service = null; - bound = false; - } - }; - private Label selectedLabel; private BitmessageContext bmc; @@ -172,8 +153,13 @@ public class MainActivity extends AppCompatActivity Singleton.getMessageListener(this).resetNotification(); // handle intents - if (getIntent().hasExtra(EXTRA_SHOW_MESSAGE)) { - onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); + Intent intent = getIntent(); + if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) { + onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE)); + } + if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) { + Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE); + launchReplyTo(this, item); } if (Preferences.useTrustedNode(this)) { @@ -345,9 +331,9 @@ public class MainActivity extends AppCompatActivity .withChecked(isRunning()) .withOnCheckedChangeListener((drawerItem, buttonView, isChecked) -> { if (isChecked) { - checkAndStartNode(buttonView); + checkAndStartNode(); } else { - service.shutdownNode(); + stopService(new Intent(this, BitmessageService.class)); } }); @@ -448,23 +434,11 @@ public class MainActivity extends AppCompatActivity } } - private void checkAndStartNode(final CompoundButton buttonView) { - if (service == null) return; - + private void checkAndStartNode() { if (Preferences.isConnectionAllowed(MainActivity.this)) { - service.startupNode(); + startService(new Intent(this, BitmessageService.class)); } else { - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.full_node_warning) - .setPositiveButton( - android.R.string.yes, - (dialog, which) -> service.startupNode() - ) - .setNegativeButton( - android.R.string.no, - (dialog, which) -> updateNodeSwitch() - ) - .show(); + startActivity(new Intent(this, FullNodeDialogActivity.class)); } } @@ -483,11 +457,14 @@ public class MainActivity extends AppCompatActivity } } - public void updateNodeSwitch() { - runOnUiThread(() -> { - nodeSwitch.withChecked(bmc.isRunning()); - drawer.updateStickyFooterItem(nodeSwitch); - }); + public static void updateNodeSwitch() { + MainActivity i = getInstance(); + if (i != null) { + i.runOnUiThread(() -> { + i.nodeSwitch.withChecked(i.bmc.isRunning()); + i.drawer.updateStickyFooterItem(i.nodeSwitch); + }); + } } private void showSelectedLabel() { @@ -556,22 +533,6 @@ public class MainActivity extends AppCompatActivity return selectedLabel; } - @Override - protected void onStart() { - super.onStart(); - bindService(new Intent(this, BitmessageService.class), connection, Context - .BIND_AUTO_CREATE); - } - - @Override - protected void onStop() { - if (bound) { - unbindService(connection); - bound = false; - } - super.onStop(); - } - public static MainActivity getInstance() { if (instance == null) return null; return instance.get(); diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index bce2795..d3d2689 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -16,11 +16,9 @@ package ch.dissem.apps.abit; -import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.util.Linkify; -import android.text.util.Linkify.TransformFilter; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -33,7 +31,6 @@ import android.widget.TextView; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import java.util.Iterator; -import java.util.regex.Matcher; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; @@ -44,10 +41,6 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; -import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; -import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; -import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; -import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; @@ -113,11 +106,8 @@ public class MessageDetailFragment extends Fragment { Linkify.addLinks(messageBody, WEB_URLS); Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, - new TransformFilter() { - public final String transformUrl(final Matcher match, String url) { - return match.group(); - } - }); + (match, url) -> match.group() + ); messageBody.setLinksClickable(true); messageBody.setTextIsSelectable(true); @@ -158,22 +148,7 @@ public class MessageDetailFragment extends Fragment { MessageRepository messageRepo = Singleton.getMessageRepository(getContext()); switch (menuItem.getItemId()) { case R.id.reply: - Intent replyIntent = new Intent(getActivity().getApplicationContext(), - ComposeMessageActivity.class); - replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); - replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); - String prefix; - if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) - .equalsIgnoreCase("RE:")) { - prefix = ""; - } else { - prefix = "RE: "; - } - replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); - replyIntent.putExtra(EXTRA_CONTENT, - "\n\n------------------------------------------------------\n" - + item.getText()); - startActivity(replyIntent); + ComposeMessageActivity.launchReplyTo(this, item); return true; case R.id.delete: if (isInTrash(item)) { diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java b/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java new file mode 100644 index 0000000..2f573b2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.dialog; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.BitmessageService; + +import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; + +/** + * @author Christian Basler + */ + +public class FullNodeDialogActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.dialog_full_node); + findViewById(R.id.ok).setOnClickListener(v -> { + startService(new Intent(this, BitmessageService.class)); + updateNodeSwitch(); + finish(); + }); + findViewById(R.id.dismiss).setOnClickListener(v -> finish()); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 1377e1f..606c389 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -27,21 +27,26 @@ import java.util.TimerTask; import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.BitmessageIntentService; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.bitmessage.utils.Property; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; + /** * Shows the network status (as long as the client is connected as a full node) */ public class NetworkNotification extends AbstractNotification { - public static final int ONGOING_NOTIFICATION_ID = 2; + public static final int NETWORK_NOTIFICATION_ID = 2; - private NotificationCompat.Builder builder; + private final NotificationCompat.Builder builder; + private Timer timer; public NetworkNotification(Context ctx) { super(ctx); - Intent showMessageIntent = new Intent(ctx, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showMessageIntent, 0); + Intent showAppIntent = new Intent(ctx, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0); builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_full_node) .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) @@ -58,10 +63,7 @@ public class NetworkNotification extends AbstractNotification { Property connections = BitmessageService.getStatus().getProperty("network", "connections"); if (!running) { builder.setContentText(ctx.getString(R.string.connection_info_disconnected)); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.updateNodeSwitch(); - } + updateNodeSwitch(); } else if (connections.getProperties().length == 0) { builder.setContentText(ctx.getString(R.string.connection_info_pending)); } else { @@ -80,6 +82,19 @@ public class NetworkNotification extends AbstractNotification { } builder.setContentText(info); } + builder.mActions.clear(); + Intent intent = new Intent(ctx, BitmessageIntentService.class); + if (running) { + intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true); + builder.addAction(R.drawable.ic_notification_node_stop, + ctx.getString(R.string.full_node_stop), + PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)); + } else { + intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true); + builder.addAction(R.drawable.ic_notification_node_start, + ctx.getString(R.string.full_node_restart), + PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)); + } notification = builder.build(); return running; } @@ -88,7 +103,8 @@ public class NetworkNotification extends AbstractNotification { public void show() { super.show(); - new Timer().schedule(new TimerTask() { + timer = new Timer(); + timer.schedule(new TimerTask() { @Override public void run() { if (!update()) { @@ -99,14 +115,28 @@ public class NetworkNotification extends AbstractNotification { }, 10_000, 10_000); } + public void showShutdown() { + if (timer != null) { + timer.cancel(); + } + update(); + super.show(); + } + @Override protected int getNotificationId() { - return ONGOING_NOTIFICATION_ID; + return NETWORK_NOTIFICATION_ID; } public void connecting() { builder.setOngoing(true); builder.setContentText(ctx.getString(R.string.connection_info_pending)); + Intent intent = new Intent(ctx, BitmessageIntentService.class); + intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true); + builder.mActions.clear(); + builder.addAction(R.drawable.ic_notification_node_stop, + ctx.getString(R.string.full_node_stop), + PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)); notification = builder.build(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index 80ff371..06e4c02 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -31,8 +31,13 @@ import java.util.Collection; import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.service.BitmessageIntentService; import ch.dissem.bitmessage.entity.Plaintext; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static ch.dissem.apps.abit.MainActivity.EXTRA_REPLY_TO_MESSAGE; +import static ch.dissem.apps.abit.MainActivity.EXTRA_SHOW_MESSAGE; +import static ch.dissem.apps.abit.service.BitmessageIntentService.EXTRA_DELETE_MESSAGE; import static ch.dissem.apps.abit.util.Drawables.toBitmap; public class NewMessageNotification extends AbstractNotification { @@ -46,51 +51,59 @@ public class NewMessageNotification extends AbstractNotification { public NewMessageNotification singleNotification(Plaintext plaintext) { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText - ()); + ()); bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned - .SPAN_INCLUSIVE_EXCLUSIVE); + .SPAN_INCLUSIVE_EXCLUSIVE); builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192)) - .setContentTitle(plaintext.getFrom().toString()) - .setContentText(plaintext.getSubject()) - .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) - .setContentInfo("Info"); + .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192)) + .setContentTitle(plaintext.getFrom().toString()) + .setContentText(plaintext.getSubject()) + .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) + .setContentInfo("Info"); - Intent showMessageIntent = new Intent(ctx, MainActivity.class); - showMessageIntent.putExtra(MainActivity.EXTRA_SHOW_MESSAGE, plaintext); - PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentIntent(pendingIntent); - - // TODO: add proper intents to reply/delete - builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent); + builder.setContentIntent( + createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)); + builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), + createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)); builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), - pendingIntent); + createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)); notification = builder.build(); return this; } + private PendingIntent createActivityIntent(String action, Plaintext message) { + Intent intent = new Intent(ctx, MainActivity.class); + intent.putExtra(action, message); + return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT); + } + + private PendingIntent createServiceIntent(Context ctx, String action, Plaintext message) { + Intent intent = new Intent(ctx, BitmessageIntentService.class); + intent.putExtra(action, message); + return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT); + } + /** * @param unacknowledged will be accessed from different threads, so make sure wherever it's * accessed it will be in a <code>synchronized(unacknowledged) * {}</code> block */ public NewMessageNotification multiNotification(Collection<Plaintext> unacknowledged, int - numberOfUnacknowledgedMessages) { + numberOfUnacknowledgedMessages) { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setContentTitle(ctx.getString(R.string.n_new_messages, unacknowledged.size())) - .setContentText(ctx.getString(R.string.app_name)); + .setContentTitle(ctx.getString(R.string.n_new_messages, unacknowledged.size())) + .setContentText(ctx.getString(R.string.app_name)); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (unacknowledged) { inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, - numberOfUnacknowledgedMessages)); + numberOfUnacknowledgedMessages)); for (Plaintext msg : unacknowledged) { Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable - .SPAN_INCLUSIVE_EXCLUSIVE); + .SPAN_INCLUSIVE_EXCLUSIVE); inboxStyle.addLine(sb); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java new file mode 100644 index 0000000..ecce911 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.service; + +import android.app.IntentService; +import android.content.Intent; + +import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; +import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.Plaintext; + +import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; + +/** + * @author Christian Basler + */ + +public class BitmessageIntentService extends IntentService { + public static final String EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"; + public static final String EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"; + public static final String EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"; + + private BitmessageContext bmc; + + public BitmessageIntentService() { + super("BitmessageIntentService"); + } + + @Override + public void onCreate() { + super.onCreate(); + bmc = Singleton.getBitmessageContext(this); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent.hasExtra(EXTRA_DELETE_MESSAGE)) { + Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_DELETE_MESSAGE); + bmc.labeler().delete(item); + bmc.messages().save(item); + } + if (intent.hasExtra(EXTRA_STARTUP_NODE)) { + if (Preferences.isConnectionAllowed(this)) { + startService(new Intent(this, BitmessageService.class)); + updateNodeSwitch(); + } else { + Intent dialogIntent = new Intent(this, FullNodeDialogActivity.class); + dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(dialogIntent); + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + } + if (intent.hasExtra(EXTRA_SHUTDOWN_NODE)) { + stopService(new Intent(this, BitmessageService.class)); + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 799aa07..1b8c645 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -18,14 +18,14 @@ package ch.dissem.apps.abit.service; import android.app.Service; import android.content.Intent; -import android.os.Binder; import android.os.IBinder; +import android.support.annotation.Nullable; import ch.dissem.apps.abit.notification.NetworkNotification; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.utils.Property; -import static ch.dissem.apps.abit.notification.NetworkNotification.ONGOING_NOTIFICATION_ID; +import static ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIFICATION_ID; /** * Define a Service that returns an IBinder for the @@ -44,55 +44,41 @@ public class BitmessageService extends Service { @Override public void onCreate() { - synchronized (BitmessageService.class) { - if (bmc == null) { - bmc = Singleton.getBitmessageContext(this); - } - notification = new NetworkNotification(this); + if (bmc == null) { + bmc = Singleton.getBitmessageContext(this); } + notification = new NetworkNotification(this); + running = false; } @Override public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - @Override - public void onDestroy() { - if (bmc.isRunning()) bmc.shutdown(); - running = false; - } - - /** - * Return an object that allows the system to invoke - * the sync adapter. - */ - @Override - public IBinder onBind(Intent intent) { - return new BitmessageBinder(); - } - - public class BitmessageBinder extends Binder { - public void startupNode() { - startService(new Intent(BitmessageService.this, BitmessageService.class)); + if (!isRunning()) { running = true; notification.connecting(); - startForeground(ONGOING_NOTIFICATION_ID, notification.getNotification()); + startForeground(NETWORK_NOTIFICATION_ID, notification.getNotification()); if (!bmc.isRunning()) { bmc.startup(); } notification.show(); } + return Service.START_STICKY; + } - public void shutdownNode() { - if (bmc.isRunning()) { - bmc.shutdown(); - } - running = false; - stopForeground(true); - notification.show(); - stopSelf(); + @Override + public void onDestroy() { + if (bmc.isRunning()) { + bmc.shutdown(); } + running = false; + notification.showShutdown(); + stopSelf(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; } public static Property getStatus() { diff --git a/app/src/main/res/drawable/ic_notification_node_start.xml b/app/src/main/res/drawable/ic_notification_node_start.xml new file mode 100644 index 0000000..4b219f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_node_start.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M8,5v14l11,-7z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notification_node_stop.xml b/app/src/main/res/drawable/ic_notification_node_stop.xml new file mode 100644 index 0000000..302844d --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_node_stop.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/> +</vector> diff --git a/app/src/main/res/layout/dialog_add_identity.xml b/app/src/main/res/layout/dialog_add_identity.xml index 720b1fd..cae15ba 100644 --- a/app/src/main/res/layout/dialog_add_identity.xml +++ b/app/src/main/res/layout/dialog_add_identity.xml @@ -28,7 +28,7 @@ <TextView android:id="@+id/description" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:text="@string/add_identity_warning" app:layout_constraintLeft_toLeftOf="parent" @@ -39,7 +39,7 @@ <RadioGroup android:id="@+id/radioGroup" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:paddingBottom="24dp" android:paddingTop="24dp" @@ -86,7 +86,6 @@ app:layout_constraintHorizontal_bias="1.0" app:layout_constraintRight_toRightOf="@+id/radioGroup" app:layout_constraintTop_toBottomOf="@+id/radioGroup" - tools:layout_constraintLeft_creator="1" tools:layout_constraintRight_creator="1" tools:layout_constraintTop_creator="1"/> diff --git a/app/src/main/res/layout/dialog_full_node.xml b/app/src/main/res/layout/dialog_full_node.xml new file mode 100644 index 0000000..9c8edd9 --- /dev/null +++ b/app/src/main/res/layout/dialog_full_node.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="336dp" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/description" + android:layout_width="320dp" + android:layout_height="wrap_content" + android:padding="24dp" + android:text="@string/full_node_warning" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_editor_absoluteX="0dp"/> + + <Button + android:id="@+id/ok" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:text="@string/startup_node" + android:textColor="@color/colorAccent" + app:layout_constraintEnd_toEndOf="@+id/description" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintTop_toBottomOf="@+id/description" + tools:layout_editor_absoluteX="184dp"/> + + <Button + android:id="@+id/dismiss" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:text="@string/cancel" + android:textColor="@color/colorAccent" + app:layout_constraintEnd_toStartOf="@+id/ok" + app:layout_constraintTop_toBottomOf="@+id/description" + tools:ignore="RtlSymmetry" + tools:layout_editor_absoluteX="87dp"/> +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ae37f4a..a72fb64 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -96,4 +96,7 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="select_file_title">Datei auswählen</string> <string name="select_identities_to_import">Bitte wähle die zu importierenden Identitäten:</string> <string name="import_input_description">Du kannst einfach den Inhalt eines Exports oder einer ‘keys.dat’-Datei einfügen</string> + <string name="full_node_restart">Knoten starten</string> + <string name="full_node_stop">Knoten beenden</string> + <string name="startup_node">Knoten starten</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd448d9..4fb16d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,4 +95,7 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="select_file_title">Select a File</string> <string name="select_identities_to_import">Please select the identities you want to import:</string> <string name="import_input_description">You can just paste the contents of an export or a ‘keys.dat’ file</string> + <string name="full_node_stop">shutdown node</string> + <string name="full_node_restart">restart node</string> + <string name="startup_node">Startup node</string> </resources> From 733288678698bd42ff9bb72aace785690b0bb0ae Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 25 Oct 2016 07:30:16 +0200 Subject: [PATCH 077/110] Added UI for sending broadcasts - UI shouldn't block until the first identity is ready anymore --- app/build.gradle | 16 ++-- .../apps/abit/AddressDetailFragment.java | 14 +++- .../apps/abit/ComposeMessageActivity.java | 1 + .../apps/abit/ComposeMessageFragment.java | 74 +++++++++++-------- .../ch/dissem/apps/abit/MainActivity.java | 25 +++---- .../dissem/apps/abit/MessageListFragment.java | 43 +++++++++-- .../repository/AndroidMessageRepository.java | 15 ++-- .../abit/service/BitmessageIntentService.java | 1 + .../dissem/apps/abit/service/Singleton.java | 37 +++++++++- .../main/res/drawable/ic_action_broadcast.xml | 24 ++++++ .../main/res/drawable/ic_action_personal.xml | 25 +++++++ .../main/res/drawable/ic_action_qr_code.xml | 4 +- .../main/res/layout/fragment_address_list.xml | 6 +- .../main/res/layout/fragment_message_list.xml | 9 ++- app/src/main/res/menu/fab_message.xml | 27 +++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + build.gradle | 2 +- 18 files changed, 244 insertions(+), 83 deletions(-) create mode 100644 app/src/main/res/drawable/ic_action_broadcast.xml create mode 100644 app/src/main/res/drawable/ic_action_personal.xml create mode 100644 app/src/main/res/menu/fab_message.xml diff --git a/app/build.gradle b/app/build.gradle index 81387e4..1f657e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,13 +11,13 @@ if (project.hasProperty("project.configs") //noinspection GroovyMissingReturnStatement android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + compileSdkVersion 25 + buildToolsVersion "25.0.0" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 - targetSdkVersion 24 + targetSdkVersion 25 versionCode 9 versionName "1.0-beta9" jackOptions.enabled = true @@ -36,12 +36,12 @@ android { } } -ext.jabitVersion = '2.0.2' +ext.jabitVersion = '2.0.3' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:24.2.1' - compile 'com.android.support:support-v4:24.2.1' - compile 'com.android.support:design:24.2.1' + compile 'com.android.support:appcompat-v7:25.0.0' + compile 'com.android.support:support-v4:25.0.0' + compile 'com.android.support:design:25.0.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -63,7 +63,7 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' compile 'com.google.zxing:core:3.3.0' - compile 'io.github.yavski:fab-speed-dial:1.0.4' + compile 'io.github.yavski:fab-speed-dial:1.0.6' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar') { transitive = true diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 439823f..eff915a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -33,6 +33,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; +import android.widget.Toast; import com.google.zxing.BarcodeFormat; import com.google.zxing.MultiFormatWriter; @@ -115,10 +116,15 @@ public class AddressDetailFragment extends Fragment { final Activity ctx = getActivity(); switch (menuItem.getItemId()) { case R.id.write_message: { - Intent intent = new Intent(ctx, ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(ctx)); - intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); - startActivity(intent); + BitmessageAddress identity = Singleton.getIdentity(ctx); + if (identity == null) { + Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show(); + } else { + Intent intent = new Intent(ctx, ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity); + intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); + startActivity(intent); + } return true; } case R.id.delete: { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 97a18c1..6bcccf6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -34,6 +34,7 @@ public class ComposeMessageActivity extends AppCompatActivity { public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"; public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"; + public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 688812a..0308ced 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -34,6 +34,7 @@ import ch.dissem.apps.abit.adapter.ContactAdapter; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; @@ -50,6 +51,7 @@ public class ComposeMessageFragment extends Fragment { private AutoCompleteTextView recipientInput; private EditText subjectInput; private EditText bodyInput; + private boolean broadcast; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -67,6 +69,7 @@ public class ComposeMessageFragment extends Fragment { } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } + broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false); if (getArguments().containsKey(EXTRA_RECIPIENT)) { recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT); } @@ -87,23 +90,28 @@ public class ComposeMessageFragment extends Fragment { Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false); recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient); - final ContactAdapter adapter = new ContactAdapter(getContext()); - recipientInput.setAdapter(adapter); - recipientInput.setOnItemClickListener( - (parent, view, position, id) -> recipient = adapter.getItem(position) - ); - recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - recipient = adapter.getItem(position); - } + if (broadcast) { + recipientInput.setVisibility(View.GONE); + } else { + final ContactAdapter adapter = new ContactAdapter(getContext()); + recipientInput.setAdapter(adapter); + recipientInput.setOnItemClickListener( + (parent, view, position, id) -> recipient = adapter.getItem(position) + ); + recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long + id) { + recipient = adapter.getItem(position); + } - @Override - public void onNothingSelected(AdapterView<?> parent) { + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + if (recipient != null) { + recipientInput.setText(recipient.toString()); } - }); - if (recipient != null) { - recipientInput.setText(recipient.toString()); } subjectInput = (EditText) rootView.findViewById(R.id.subject); subjectInput.setText(subject); @@ -132,25 +140,31 @@ public class ComposeMessageFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.send: - String inputString = recipientInput.getText().toString(); - if (recipient == null || !recipient.toString().equals(inputString)) { - try { - recipient = new BitmessageAddress(inputString); - } catch (Exception e) { - List<BitmessageAddress> contacts = Singleton.getAddressRepository - (getContext()).getContacts(); - for (BitmessageAddress contact : contacts) { - if (inputString.equalsIgnoreCase(contact.getAlias())) { - recipient = contact; - if (inputString.equals(contact.getAlias())) - break; + if (broadcast) { + Singleton.getBitmessageContext(getContext()).broadcast(identity, + subjectInput.getText().toString(), + bodyInput.getText().toString()); + } else { + String inputString = recipientInput.getText().toString(); + if (recipient == null || !recipient.toString().equals(inputString)) { + try { + recipient = new BitmessageAddress(inputString); + } catch (Exception e) { + List<BitmessageAddress> contacts = Singleton.getAddressRepository + (getContext()).getContacts(); + for (BitmessageAddress contact : contacts) { + if (inputString.equalsIgnoreCase(contact.getAlias())) { + recipient = contact; + if (inputString.equals(contact.getAlias())) + break; + } } } } + Singleton.getBitmessageContext(getContext()).send(identity, recipient, + subjectInput.getText().toString(), + bodyInput.getText().toString()); } - Singleton.getBitmessageContext(getContext()).send(identity, recipient, - subjectInput.getText().toString(), - bodyInput.getText().toString()); getActivity().finish(); return true; default: diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 7e418c2..d90194b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -16,7 +16,6 @@ package ch.dissem.apps.abit; -import android.app.AlertDialog; import android.content.Intent; import android.graphics.Point; import android.os.Bundle; @@ -26,6 +25,7 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; +import android.widget.Toast; import com.github.amlcurran.showcaseview.ShowcaseView; import com.mikepenz.community_material_typeface_library.CommunityMaterial; @@ -66,6 +66,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import static android.widget.Toast.LENGTH_LONG; import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -223,13 +224,7 @@ public class MainActivity extends AppCompatActivity } if (profiles.isEmpty()) { // Create an initial identity - BitmessageAddress identity = Singleton.getIdentity(this); - profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withEmail(identity.getAddress()) - .withTag(identity) - ); + Singleton.getIdentity(this); } profiles.add(new ProfileSettingDrawerItem() .withName(getString(R.string.add_identity)) @@ -256,11 +251,15 @@ public class MainActivity extends AppCompatActivity addIdentityDialog(); break; case MANAGE_IDENTITY: - Intent show = new Intent(MainActivity.this, - AddressDetailActivity.class); - show.putExtra(AddressDetailFragment.ARG_ITEM, - Singleton.getIdentity(getApplicationContext())); - startActivity(show); + BitmessageAddress identity = Singleton.getIdentity(this); + if (identity == null) { + Toast.makeText(this, R.string.no_identity_warning, LENGTH_LONG).show(); + } else { + Intent show = new Intent(MainActivity.this, + AddressDetailActivity.class); + show.putExtra(AddressDetailFragment.ARG_ITEM, identity); + startActivity(show); + } break; default: if (profile instanceof ProfileDrawerItem) { diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 0d22d98..fbd2f00 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -18,7 +18,6 @@ package ch.dissem.apps.abit; import android.content.Intent; import android.os.Bundle; -import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; @@ -29,6 +28,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; @@ -43,10 +43,15 @@ import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; +import io.github.yavski.fabspeeddial.FabSpeedDial; +import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash; /** @@ -128,14 +133,36 @@ public class MessageListFragment extends Fragment implements ListHolder { layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); // Show the dummy content as text in a TextView. - FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id + FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id .fab_compose_message); - fab.setOnClickListener(view -> { - Intent intent = new Intent(getActivity().getApplicationContext(), - ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity - (getActivity())); - startActivity(intent); + fab.setMenuListener(new SimpleMenuListenerAdapter() { + @Override + public boolean onMenuItemSelected(MenuItem menuItem) { + BitmessageAddress identity = Singleton.getIdentity(getActivity()); + if (identity == null) { + Toast.makeText(getActivity(), R.string.no_identity_warning, + Toast.LENGTH_LONG).show(); + return false; + } else { + switch (menuItem.getItemId()) { + case R.id.action_compose_message: { + Intent intent = new Intent(getActivity(), ComposeMessageActivity.class); + intent.putExtra(EXTRA_IDENTITY, identity); + startActivity(intent); + return true; + } + case R.id.action_compose_broadcast: { + Intent intent = new Intent(getActivity(), ComposeMessageActivity.class); + intent.putExtra(EXTRA_IDENTITY, identity); + intent.putExtra(EXTRA_BROADCAST, true); + startActivity(intent); + return true; + } + default: + return false; + } + } + } }); // touch guard manager (this class is required to suppress scrolling while swipe-dismiss diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 8c6aad8..03420bd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -244,16 +244,19 @@ public class AndroidMessageRepository extends AbstractMessageRepository { db.beginTransaction(); // save from address if necessary + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message + .getFrom().getAddress()); if (message.getId() == null) { - BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message - .getFrom().getAddress()); - if (savedAddress == null || savedAddress.getPrivateKey() == null) { - if (savedAddress != null && savedAddress.getAlias() != null) { - message.getFrom().setAlias(savedAddress.getAlias()); - } + if (savedAddress == null) { ctx.getAddressRepository().save(message.getFrom()); + } else if (savedAddress.getPubkey() == null) { + savedAddress.setPubkey(message.getFrom().getPubkey()); + ctx.getAddressRepository().save(savedAddress); } } + if (savedAddress != null) { + message.getFrom().setAlias(savedAddress.getAlias()); + } // save message if (message.getId() == null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java index ecce911..84a5ed8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageIntentService.java @@ -53,6 +53,7 @@ public class BitmessageIntentService extends IntentService { Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_DELETE_MESSAGE); bmc.labeler().delete(item); bmc.messages().save(item); + Singleton.getMessageListener(this).resetNotification(); } if (intent.hasExtra(EXTRA_STARTUP_NODE)) { if (Preferences.isConnectionAllowed(this)) { diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index a92521d..db3360b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -17,9 +17,12 @@ package ch.dissem.apps.abit.service; import android.content.Context; +import android.os.AsyncTask; +import android.widget.Toast; import java.util.List; +import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.adapter.AndroidCryptography; import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; @@ -34,6 +37,7 @@ import ch.dissem.apps.abit.repository.SqlHelper; import ch.dissem.apps.abit.util.Constants; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; @@ -50,6 +54,7 @@ public class Singleton { private static MessageListener messageListener; private static BitmessageAddress identity; private static AndroidProofOfWorkRepository powRepo; + private static boolean creatingIdentity; public static BitmessageContext getBitmessageContext(Context context) { if (bitmessageContext == null) { @@ -110,15 +115,39 @@ public class Singleton { BitmessageContext bmc = getBitmessageContext(ctx); synchronized (Singleton.class) { if (identity == null) { - // FIXME: this may block the UI, there must be a better way! List<BitmessageAddress> identities = bmc.addresses() .getIdentities(); if (identities.size() > 0) { identity = identities.get(0); } else { - identity = bmc.createIdentity(false); - identity.setAlias(ctx.getString(R.string.alias_default_identity)); - bmc.addresses().save(identity); + if (!creatingIdentity) { + creatingIdentity = true; + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + BitmessageAddress identity = bmc.createIdentity(false, + Pubkey.Feature.DOES_ACK); + identity.setAlias( + ctx.getString(R.string.alias_default_identity) + ); + bmc.addresses().save(identity); + return identity; + } + + @Override + protected void onPostExecute(BitmessageAddress identity) { + Singleton.identity = identity; + Toast.makeText(ctx, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(identity); + } + } + }.execute(); + } + return null; } } } diff --git a/app/src/main/res/drawable/ic_action_broadcast.xml b/app/src/main/res/drawable/ic_action_broadcast.xml new file mode 100644 index 0000000..2fc1345 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_broadcast.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- drawable/radio_tower.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#DEFFFFFF" android:pathData="M12,10A2,2 0 0,1 14,12C14,12.5 13.82,12.94 13.53,13.29L16.7,22H14.57L12,14.93L9.43,22H7.3L10.47,13.29C10.18,12.94 10,12.5 10,12A2,2 0 0,1 12,10M12,8A4,4 0 0,0 8,12C8,12.5 8.1,13 8.28,13.46L7.4,15.86C6.53,14.81 6,13.47 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12C18,13.47 17.47,14.81 16.6,15.86L15.72,13.46C15.9,13 16,12.5 16,12A4,4 0 0,0 12,8M12,4A8,8 0 0,0 4,12C4,14.36 5,16.5 6.64,17.94L5.92,19.94C3.54,18.11 2,15.23 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12C22,15.23 20.46,18.11 18.08,19.94L17.36,17.94C19,16.5 20,14.36 20,12A8,8 0 0,0 12,4Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_action_personal.xml b/app/src/main/res/drawable/ic_action_personal.xml new file mode 100644 index 0000000..74c7332 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_personal.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#DEFFFFFF" + android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_action_qr_code.xml b/app/src/main/res/drawable/ic_action_qr_code.xml index ee40a97..6a0ad45 100644 --- a/app/src/main/res/drawable/ic_action_qr_code.xml +++ b/app/src/main/res/drawable/ic_action_qr_code.xml @@ -3,5 +3,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - <path android:fillColor="#000" android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /> -</vector> \ No newline at end of file + <path android:fillColor="#DEFFFFFF" android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /> +</vector> diff --git a/app/src/main/res/layout/fragment_address_list.xml b/app/src/main/res/layout/fragment_address_list.xml index 018bf6f..ad4b671 100644 --- a/app/src/main/res/layout/fragment_address_list.xml +++ b/app/src/main/res/layout/fragment_address_list.xml @@ -8,12 +8,10 @@ android:id="@id/android:list" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:choiceMode="singleChoice" - android:layout_alignParentBottom="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" - + android:choiceMode="singleChoice" android:clipToPadding="false" android:paddingBottom="88dp" android:scrollbarStyle="outsideOverlay"/> @@ -26,8 +24,8 @@ android:layout_alignParentEnd="true" android:layout_gravity="bottom|end" android:layout_margin="16dp" - android:src="@drawable/ic_action_add_contact" app:elevation="8dp" + app:fabDrawable="@drawable/ic_action_add_contact" app:fabGravity="bottom_end" app:fabMenu="@menu/fab_address"/> </RelativeLayout> diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index 0fe153d..2cbf5e8 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -19,13 +19,16 @@ android:scrollbars="vertical" tools:listitem="@layout/message_row"/> - <android.support.design.widget.FloatingActionButton + <io.github.yavski.fabspeeddial.FabSpeedDial android:id="@+id/fab_compose_message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" + android:layout_gravity="bottom|end" android:layout_margin="16dp" - android:src="@drawable/ic_action_compose_message" - app:elevation="8dp"/> + app:elevation="8dp" + app:fabDrawable="@drawable/ic_action_compose_message" + app:fabGravity="bottom_end" + app:fabMenu="@menu/fab_message"/> </RelativeLayout> diff --git a/app/src/main/res/menu/fab_message.xml b/app/src/main/res/menu/fab_message.xml new file mode 100644 index 0000000..839de8d --- /dev/null +++ b/app/src/main/res/menu/fab_message.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/action_compose_broadcast" + android:icon="@drawable/ic_action_broadcast" + android:title="@string/broadcast"/> + <item + android:id="@+id/action_compose_message" + android:icon="@drawable/ic_action_personal" + android:title="@string/personal_message"/> +</menu> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a72fb64..3a5b347 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -99,4 +99,6 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="full_node_restart">Knoten starten</string> <string name="full_node_stop">Knoten beenden</string> <string name="startup_node">Knoten starten</string> + <string name="personal_message">Nachricht</string> + <string name="no_identity_warning">Bitte versuchs nochmals wenn eine Identität verfügbar ist.</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb16d4..68f92a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,4 +98,6 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="full_node_stop">shutdown node</string> <string name="full_node_restart">restart node</string> <string name="startup_node">Startup node</string> + <string name="personal_message">Message</string> + <string name="no_identity_warning">Please try again once an identity is available.</string> </resources> diff --git a/build.gradle b/build.gradle index ec2dca6..554b46e 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' + classpath 'com.android.tools.build:gradle:2.2.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From e249c86b79460ccf5c9a79d566ab30564b508bd8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 27 Oct 2016 17:37:34 +0200 Subject: [PATCH 078/110] Minor UI improvements - added outbox label - notification is now always removed when on main activity - send status is now shown in message list and detail view --- .../db/migration/V3.4__Add_label_outbox.sql | 1 + .../apps/abit/AddressDetailFragment.java | 25 ++++++++--- .../dissem/apps/abit/AddressListFragment.java | 5 ++- .../java/ch/dissem/apps/abit/Identicon.java | 2 +- .../ch/dissem/apps/abit/MainActivity.java | 16 +++---- .../apps/abit/MessageDetailFragment.java | 8 +++- .../abit/adapter/SwipeableMessageAdapter.java | 6 +++ .../apps/abit/listener/MessageListener.java | 36 ++++++++------- .../repository/AndroidMessageRepository.java | 5 ++- .../apps/abit/repository/SqlHelper.java | 4 +- .../java/ch/dissem/apps/abit/util/Assets.java | 45 +++++++++++++++++++ app/src/main/res/drawable/draft.xml | 25 +++++++++++ app/src/main/res/drawable/sent.xml | 25 +++++++++++ .../main/res/drawable/sent_acknowledged.xml | 25 +++++++++++ .../res/layout/fragment_message_detail.xml | 33 +++++++++----- app/src/main/res/layout/message_row.xml | 14 +++++- app/src/main/res/values-de/strings.xml | 9 ++++ app/src/main/res/values/strings.xml | 13 +++++- 18 files changed, 246 insertions(+), 51 deletions(-) create mode 100644 app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql create mode 100644 app/src/main/res/drawable/draft.xml create mode 100644 app/src/main/res/drawable/sent.xml create mode 100644 app/src/main/res/drawable/sent_acknowledged.xml diff --git a/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql b/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql new file mode 100644 index 0000000..d61bcc9 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql @@ -0,0 +1 @@ +INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index eff915a..5af7c3d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -101,10 +102,11 @@ public class AddressDetailFragment extends Fragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.address, menu); - Drawables.addIcon(getActivity(), menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); - Drawables.addIcon(getActivity(), menu, R.id.share, GoogleMaterial.Icon.gmd_share); - Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); - Drawables.addIcon(getActivity(), menu, R.id.export, + FragmentActivity activity = getActivity(); + Drawables.addIcon(activity, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); + Drawables.addIcon(activity, menu, R.id.share, GoogleMaterial.Icon.gmd_share); + Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); + Drawables.addIcon(activity, menu, R.id.export, CommunityMaterial.Icon.cmd_export) .setVisible(item != null && item.getPrivateKey() != null); @@ -185,6 +187,17 @@ public class AddressDetailFragment extends Fragment { // Show the dummy content as text in a TextView. if (item != null) { + FragmentActivity activity = getActivity(); + if (item.isChan()) { + activity.setTitle(R.string.title_chan_detail); + } else if (item.getPrivateKey() != null) { + activity.setTitle(R.string.title_identity_detail); + } else if (item.isSubscribed()) { + activity.setTitle(R.string.title_subscription_detail); + } else { + activity.setTitle(R.string.title_contact_detail); + } + ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); TextView name = (TextView) rootView.findViewById(R.id.name); name.setText(item.toString()); @@ -207,8 +220,8 @@ public class AddressDetailFragment extends Fragment { TextView address = (TextView) rootView.findViewById(R.id.address); address.setText(item.getAddress()); address.setSelected(true); - ((TextView) rootView.findViewById(R.id.stream_number)).setText(getActivity() - .getString(R.string.stream_number, item.getStream())); + ((TextView) rootView.findViewById(R.id.stream_number)).setText( + getString(R.string.stream_number, item.getStream())); if (item.getPrivateKey() == null) { Switch active = (Switch) rootView.findViewById(R.id.active); active.setChecked(item.isSubscribed()); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 57aa3dd..df6785c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -93,13 +93,14 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr convertView = inflater.inflate(R.layout.subscription_row, parent, false); } BitmessageAddress item = getItem(position); + assert item != null; ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); TextView name = (TextView) convertView.findViewById(R.id.name); name.setText(item.toString()); TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number); - streamNumber.setText(getContext().getString(R.string.stream_number, item - .getStream())); + streamNumber.setText(getContext().getString(R.string.stream_number, + item.getStream())); convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? View.VISIBLE : View.INVISIBLE); return convertView; diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index fc6122c..a6f5fcd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -37,7 +37,7 @@ public class Identicon extends Drawable { private final boolean chan; private final TextPaint textPaint; - public Identicon(BitmessageAddress input) { + public Identicon(@NonNull BitmessageAddress input) { paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index d90194b..37c18d3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -151,8 +151,6 @@ public class MainActivity extends AppCompatActivity createDrawer(toolbar, labels); - Singleton.getMessageListener(this).resetNotification(); - // handle intents Intent intent = getIntent(); if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) { @@ -283,7 +281,8 @@ public class MainActivity extends AppCompatActivity .withName(label.toString()) .withTag(label); if (label.getType() == null) { - item.withIcon(CommunityMaterial.Icon.cmd_label); + item.withIcon(CommunityMaterial.Icon.cmd_label) + .withIconColor(label.getColor()); } else { switch (label.getType()) { case INBOX: @@ -292,6 +291,9 @@ public class MainActivity extends AppCompatActivity case DRAFT: item.withIcon(CommunityMaterial.Icon.cmd_file); break; + case OUTBOX: + item.withIcon(CommunityMaterial.Icon.cmd_outbox); + break; case SENT: item.withIcon(CommunityMaterial.Icon.cmd_send); break; @@ -386,6 +388,7 @@ public class MainActivity extends AppCompatActivity protected void onResume() { updateUnread(); updateNodeSwitch(); + Singleton.getMessageListener(this).resetNotification(); super.onResume(); } @@ -397,13 +400,10 @@ public class MainActivity extends AppCompatActivity .withEmail(identity.getAddress()) .withTag(identity); if (accountHeader.getProfiles() != null) { - // we know that there are 2 setting - // elements. + // we know that there are 2 setting elements. // Set the new profile above them ;) accountHeader.addProfile( - newProfile, accountHeader - .getProfiles().size() - - 2); + newProfile, accountHeader.getProfiles().size() - 2); } else { accountHeader.addProfiles(newProfile); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index d3d2689..25239f6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -34,6 +34,7 @@ import java.util.Iterator; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Assets; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; @@ -92,9 +93,12 @@ public class MessageDetailFragment extends Fragment { // Show the dummy content as text in a TextView. if (item != null) { ((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); + ImageView status = (ImageView) rootView.findViewById(R.id.status); + status.setImageResource(Assets.getStatusDrawable(item.getStatus())); + status.setContentDescription(getString(Assets.getStatusString(item.getStatus()))); BitmessageAddress sender = item.getFrom(); - ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon - (sender)); + ((ImageView) rootView.findViewById(R.id.avatar)) + .setImageDrawable(new Identicon(sender)); ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString()); if (item.getTo() != null) { ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index 53db232..6beb402 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -41,6 +41,7 @@ import java.util.List; import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.util.Assets; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -82,6 +83,7 @@ public class SwipeableMessageAdapter static class ViewHolder extends AbstractSwipeableItemViewHolder { public final FrameLayout container; public final ImageView avatar; + public final ImageView status; public final TextView sender; public final TextView subject; public final TextView extract; @@ -90,6 +92,7 @@ public class SwipeableMessageAdapter super(v); container = (FrameLayout) v.findViewById(R.id.container); avatar = (ImageView) v.findViewById(R.id.avatar); + status = (ImageView) v.findViewById(R.id.status); sender = (TextView) v.findViewById(R.id.sender); subject = (TextView) v.findViewById(R.id.subject); extract = (TextView) v.findViewById(R.id.text); @@ -164,6 +167,9 @@ public class SwipeableMessageAdapter // set data holder.avatar.setImageDrawable(new Identicon(item.getFrom())); + holder.status.setImageResource(Assets.getStatusDrawable(item.getStatus())); + holder.status.setContentDescription( + holder.status.getContext().getString(Assets.getStatusString(item.getStatus()))); holder.sender.setText(item.getFrom().toString()); holder.subject.setText(normalizeWhitespaces(item.getSubject())); holder.extract.setText(normalizeWhitespaces(item.getText())); diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index ddb99f5..e980d93 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -20,6 +20,8 @@ import android.content.Context; import java.util.Deque; import java.util.LinkedList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import ch.dissem.apps.abit.MainActivity; import ch.dissem.apps.abit.notification.NewMessageNotification; @@ -38,6 +40,7 @@ public class MessageListener implements BitmessageContext.Listener { private final Deque<Plaintext> unacknowledged = new LinkedList<>(); private int numberOfUnacknowledgedMessages = 0; private final NewMessageNotification notification; + private final ExecutorService pool = Executors.newSingleThreadExecutor(); public MessageListener(Context ctx) { this.notification = new NewMessageNotification(ctx); @@ -45,33 +48,32 @@ public class MessageListener implements BitmessageContext.Listener { @Override public void receive(final Plaintext plaintext) { - synchronized (unacknowledged) { + pool.submit(() -> { unacknowledged.addFirst(plaintext); numberOfUnacknowledgedMessages++; if (unacknowledged.size() > 5) { unacknowledged.removeLast(); } - } + if (numberOfUnacknowledgedMessages == 1) { + notification.singleNotification(plaintext); + } else { + notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); + } + notification.show(); - if (numberOfUnacknowledgedMessages == 1) { - notification.singleNotification(plaintext); - } else { - notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); - } - notification.show(); - - // If MainActivity is shown, update the sidebar badges - MainActivity main = MainActivity.getInstance(); - if (main != null) { - main.updateUnread(); - } + // If MainActivity is shown, update the sidebar badges + MainActivity main = MainActivity.getInstance(); + if (main != null) { + main.updateUnread(); + } + }); } public void resetNotification() { - notification.hide(); - synchronized (unacknowledged) { + pool.submit(() -> { + notification.hide(); unacknowledged.clear(); numberOfUnacknowledgedMessages = 0; - } + }); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 03420bd..228c67e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -123,6 +123,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { case DRAFT: text = context.getString(R.string.draft); break; + case OUTBOX: + text = context.getString(R.string.outbox); + break; case SENT: text = context.getString(R.string.sent); break; @@ -197,7 +200,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { TABLE_NAME, projection, where, null, null, null, - COLUMN_RECEIVED + " DESC" + COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC" )) { while (c.moveToNext()) { byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV)); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 5e6a68c..0e95170 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -27,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 6; private static final String DATABASE_NAME = "jabit.db"; private final Context ctx; @@ -59,6 +59,8 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V3.2__Update_table_message"); case 4: executeMigration(db, "V3.3__Create_table_node"); + case 5: + executeMigration(db, "V3.4__Add_label_outbox"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than // DATABASE_VERSION. diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Assets.java b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java index db9b54c..5d07f6f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Assets.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Assets.java @@ -17,6 +17,8 @@ package ch.dissem.apps.abit.util; import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; import java.io.IOException; import java.io.InputStream; @@ -24,6 +26,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Scanner; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.Plaintext; + /** * Helper class to work with Assets. */ @@ -44,4 +49,44 @@ public class Assets { throw new RuntimeException(e); } } + + @DrawableRes + public static int getStatusDrawable(Plaintext.Status status) { + switch (status) { + case RECEIVED: + return 0; + case DRAFT: + return R.drawable.draft; + case PUBKEY_REQUESTED: + return R.drawable.public_key; + case DOING_PROOF_OF_WORK: + return R.drawable.ic_notification_proof_of_work; + case SENT: + return R.drawable.sent; + case SENT_ACKNOWLEDGED: + return R.drawable.sent_acknowledged; + default: + return 0; + } + } + + @StringRes + public static int getStatusString(Plaintext.Status status) { + switch (status) { + case RECEIVED: + return R.string.status_received; + case DRAFT: + return R.string.status_draft; + case PUBKEY_REQUESTED: + return R.string.status_public_key; + case DOING_PROOF_OF_WORK: + return R.string.proof_of_work_title; + case SENT: + return R.string.status_sent; + case SENT_ACKNOWLEDGED: + return R.string.status_sent_acknowledged; + default: + return 0; + } + } } diff --git a/app/src/main/res/drawable/draft.xml b/app/src/main/res/drawable/draft.xml new file mode 100644 index 0000000..64dd791 --- /dev/null +++ b/app/src/main/res/drawable/draft.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/sent.xml b/app/src/main/res/drawable/sent.xml new file mode 100644 index 0000000..0f4e328 --- /dev/null +++ b/app/src/main/res/drawable/sent.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> +</vector> diff --git a/app/src/main/res/drawable/sent_acknowledged.xml b/app/src/main/res/drawable/sent_acknowledged.xml new file mode 100644 index 0000000..1970559 --- /dev/null +++ b/app/src/main/res/drawable/sent_acknowledged.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/> +</vector> diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 053a8d4..b0a7ef5 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -2,34 +2,47 @@ <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" + android:focusableInTouchMode="true" android:orientation="vertical"> <TextView android:id="@+id/subject" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentTop="true" + android:layout_toStartOf="@+id/status" android:elegantTextHeight="false" android:enabled="false" android:gravity="center_vertical" android:padding="16dp" - tools:text="Subject" android:textAppearance="?android:attr/textAppearanceLarge" - tools:ignore="UnusedAttribute"/> + tools:ignore="UnusedAttribute" + tools:text="Subject"/> + + <ImageView + android:id="@+id/status" + android:layout_width="wrap_content" + android:layout_height="60dp" + android:layout_alignParentEnd="true" + android:layout_alignParentTop="true" + android:tint="@color/colorAccent" + tools:src="@drawable/ic_notification_proof_of_work" + android:padding="16dp" + tools:ignore="ContentDescription"/> <View android:id="@+id/divider" android:layout_width="fill_parent" android:layout_height="2dip" android:layout_below="@id/subject" - android:background="@color/divider" /> + android:background="@color/divider"/> <ImageView android:id="@+id/avatar" @@ -50,8 +63,8 @@ android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - tools:text="Sender" - android:textStyle="bold" /> + android:textStyle="bold" + tools:text="Sender"/> <TextView android:id="@+id/recipient" @@ -62,7 +75,7 @@ android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - tools:text="Recipient" /> + tools:text="Recipient"/> <TextView android:id="@+id/text" @@ -75,6 +88,6 @@ android:layout_marginTop="32dp" android:paddingBottom="64dp" android:text="New Text" - android:textIsSelectable="true" /> + android:textIsSelectable="true"/> </RelativeLayout> -</ScrollView> \ No newline at end of file +</ScrollView> diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index 486df04..f23d5c1 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -44,7 +44,7 @@ android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:layout_margin="16dp" - android:src="@color/colorAccent" + android:src="@color/colorPrimaryDark" tools:ignore="ContentDescription"/> <TextView @@ -96,6 +96,18 @@ android:textAppearance="?android:attr/textAppearanceSmall" tools:text="Text"/> + <ImageView + android:id="@+id/status" + android:layout_width="24dp" + android:layout_height="wrap_content" + android:layout_alignBottom="@id/avatar" + android:layout_alignEnd="@+id/avatar" + android:layout_marginBottom="-8dp" + android:layout_marginEnd="-8dp" + android:tint="@color/colorAccent" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_notification_proof_of_work"/> + </RelativeLayout> </FrameLayout> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3a5b347..ec78307 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -101,4 +101,13 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="startup_node">Knoten starten</string> <string name="personal_message">Nachricht</string> <string name="no_identity_warning">Bitte versuchs nochmals wenn eine Identität verfügbar ist.</string> + <string name="title_chan_detail">Chan</string> + <string name="title_contact_detail">Kontakt</string> + <string name="title_identity_detail">Identität</string> + <string name="outbox">Postausgang</string> + <string name="status_draft">Entwurf</string> + <string name="status_public_key">öffentlicher Schlüssel angefordert</string> + <string name="status_received">empfangen</string> + <string name="status_sent">gesendet</string> + <string name="status_sent_acknowledged">Empfang bestätigt</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68f92a0..262b398 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,11 @@ <resources xmlns:tools="http://schemas.android.com/tools"> <string name="app_name">Abit</string> <string name="about_app">A Bitmessage client for Android</string> - <string name="title_message_detail">Message Detail</string> - <string name="title_subscription_detail">Subscription Detail</string> + <string name="title_message_detail">Message</string> + <string name="title_subscription_detail">Subscription</string> + <string name="title_chan_detail">Chan</string> + <string name="title_identity_detail">Identity</string> + <string name="title_contact_detail">Contact</string> <string name="bitmessage_full_node">Bitmessage Node</string> <string name="settings">Settings</string> <string name="wifi_only">Wi-Fi only</string> @@ -100,4 +103,10 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="startup_node">Startup node</string> <string name="personal_message">Message</string> <string name="no_identity_warning">Please try again once an identity is available.</string> + <string name="status_public_key">public key requested</string> + <string name="status_sent_acknowledged">acknowledged</string> + <string name="status_draft">draft</string> + <string name="status_sent">sent</string> + <string name="status_received">received</string> + <string name="outbox">Outbox</string> </resources> From edd1124c32e0447375542cea678a56eeb3bc3db4 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 2 Nov 2016 07:32:13 +0100 Subject: [PATCH 079/110] Updated Jabit and fixed the update of the "unread messages" badge --- app/build.gradle | 4 ++-- app/src/main/java/ch/dissem/apps/abit/MainActivity.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1f657e3..7571cbe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,12 +36,13 @@ android { } } -ext.jabitVersion = '2.0.3' +ext.jabitVersion = '2.0.4' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:25.0.0' compile 'com.android.support:support-v4:25.0.0' compile 'com.android.support:design:25.0.0' + compile 'com.android.support.constraint:constraint-layout:1.0.0-beta1' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -72,7 +73,6 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' - compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9' } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 37c18d3..e49e5a3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -452,6 +452,7 @@ public class MainActivity extends AppCompatActivity } else { ((PrimaryDrawerItem) item).withBadge((String) null); } + drawer.updateItem(item); } } } From 9af80f008df6c039d211b9771f8b8cf6389d18a0 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 2 Nov 2016 20:55:56 +0100 Subject: [PATCH 080/110] "Up" navigation now brings you back to the selected label --- .../ch/dissem/apps/abit/DetailActivity.java | 25 +++++++++--------- .../ch/dissem/apps/abit/MainActivity.java | 14 ++++++---- .../apps/abit/MessageDetailActivity.java | 26 ++++++++++++++++--- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java index f725c46..0289b64 100644 --- a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java @@ -35,18 +35,19 @@ public abstract class DetailActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - // This ID represents the Home or Up button. In the case of this - // activity, the Up button is shown. Use NavUtils to allow users - // to navigate up one level in the application structure. For - // more details, see the Navigation pattern on Android Design: - // - // http://developer.android.com/design/patterns/navigation.html#up-vs-back - // - NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); - return true; + switch (item.getItemId()) { + case android.R.id.home: + // This ID represents the Home or Up button. In the case of this + // activity, the Up button is shown. Use NavUtils to allow users + // to navigate up one level in the application structure. For + // more details, see the Navigation pattern on Android Design: + // + // http://developer.android.com/design/patterns/navigation.html#up-vs-back + // + NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); } - return super.onOptionsItemSelected(item); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index e49e5a3..af705e0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -91,6 +91,7 @@ import static ch.dissem.apps.abit.service.BitmessageService.isRunning; public class MainActivity extends AppCompatActivity implements ListSelectionListener<Serializable>, ActionBarListener { public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"; + public static final String EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"; public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; @@ -122,7 +123,9 @@ public class MainActivity extends AppCompatActivity instance = new WeakReference<>(this); bmc = Singleton.getBitmessageContext(this); List<Label> labels = bmc.messages().getLabels(); - if (selectedLabel == null) { + if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) { + selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL); + } else if (selectedLabel == null) { selectedLabel = labels.get(0); } @@ -508,15 +511,16 @@ public class MainActivity extends AppCompatActivity // In single-pane mode, simply start the detail activity // for the selected item ID. Intent detailIntent; - if (item instanceof Plaintext) + if (item instanceof Plaintext) { detailIntent = new Intent(this, MessageDetailActivity.class); - else if (item instanceof BitmessageAddress) + detailIntent.putExtra(EXTRA_SHOW_LABEL, selectedLabel); + } else if (item instanceof BitmessageAddress) { detailIntent = new Intent(this, AddressDetailActivity.class); - else + } else { throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + "was " + item.getClass().getSimpleName()); - + } detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); startActivity(detailIntent); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java index dbad234..41fb5fd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailActivity.java @@ -1,6 +1,11 @@ package ch.dissem.apps.abit; +import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; + +import ch.dissem.bitmessage.entity.valueobject.Label; /** @@ -13,6 +18,7 @@ import android.os.Bundle; * more than a {@link MessageDetailFragment}. */ public class MessageDetailActivity extends DetailActivity { + private Label label; @Override protected void onCreate(Bundle savedInstanceState) { @@ -28,16 +34,30 @@ public class MessageDetailActivity extends DetailActivity { // http://developer.android.com/guide/components/fragments.html // if (savedInstanceState == null) { + label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL); // Create the detail fragment and add it to the activity // using a fragment transaction. Bundle arguments = new Bundle(); arguments.putSerializable(MessageDetailFragment.ARG_ITEM, - getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM)); + getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM)); MessageDetailFragment fragment = new MessageDetailFragment(); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() - .add(R.id.content, fragment) - .commit(); + .add(R.id.content, fragment) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent parentIntent = new Intent(this, MainActivity.class); + parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label); + NavUtils.navigateUpTo(this, parentIntent); + return true; + default: + return super.onOptionsItemSelected(item); } } } From b34e678c685c5cb1a503f5ac1f9859cb612a23a9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 3 Nov 2016 23:04:41 +0100 Subject: [PATCH 081/110] Disabled Jack I'd love to use Java 8 features, but Jack just isn't ready yet. --- app/build.gradle | 6 +- .../apps/abit/AbstractItemListFragment.java | 9 +- .../apps/abit/AddressDetailFragment.java | 52 +++--- .../dissem/apps/abit/AddressListFragment.java | 42 ++--- .../apps/abit/ComposeMessageFragment.java | 7 +- .../apps/abit/CreateAddressActivity.java | 43 +++-- .../apps/abit/ImportIdentitiesFragment.java | 17 +- .../ch/dissem/apps/abit/InputWifFragment.java | 55 +++--- .../ch/dissem/apps/abit/MainActivity.java | 156 ++++++++++-------- .../apps/abit/MessageDetailFragment.java | 8 +- .../ch/dissem/apps/abit/SettingsFragment.java | 30 ++-- .../abit/adapter/AddressSelectorAdapter.java | 10 +- .../abit/adapter/SwipeableMessageAdapter.java | 14 +- .../dialog/AddIdentityDialogFragment.java | 136 ++++++++------- .../DeterministicIdentityDialogFragment.java | 122 +++++++------- .../abit/dialog/FullNodeDialogActivity.java | 19 ++- .../apps/abit/listener/MessageListener.java | 46 +++--- .../dissem/apps/abit/pow/ServerPowEngine.java | 52 +++--- .../apps/abit/service/ProofOfWorkService.java | 35 ++-- .../dissem/apps/abit/service/Singleton.java | 4 +- 20 files changed, 499 insertions(+), 364 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7571cbe..cf33c47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,11 +20,11 @@ android { targetSdkVersion 25 versionCode 9 versionName "1.0-beta9" - jackOptions.enabled = true + jackOptions.enabled = false } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 } buildTypes { release { diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index df96b43..b61de46 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -37,8 +37,13 @@ public abstract class AbstractItemListFragment<T> extends ListFragment implement * A dummy implementation of the {@link ListSelectionListener} interface that does * nothing. Used only when this fragment is not attached to an activity. */ - private static final ListSelectionListener<Object> dummyCallbacks = plaintext -> { - }; + private static final ListSelectionListener<Object> dummyCallbacks = + new ListSelectionListener<Object>() { + @Override + public void onItemSelected(Object item) { + // NO OP + } + }; /** * The fragment's current callback object, which is notified of list item * clicks. diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 5af7c3d..40b2410 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit; import android.app.Activity; import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; @@ -31,6 +32,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; @@ -137,14 +139,17 @@ public class AddressDetailFragment extends Fragment { warning = R.string.delete_contact_warning; new AlertDialog.Builder(ctx) .setMessage(warning) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - Singleton.getAddressRepository(ctx).remove(item); - MainActivity mainActivity = MainActivity.getInstance(); - if (item.getPrivateKey() != null && mainActivity != null) { - mainActivity.removeIdentityEntry(item); + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Singleton.getAddressRepository(ctx).remove(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (item.getPrivateKey() != null && mainActivity != null) { + mainActivity.removeIdentityEntry(item); + } + item = null; + ctx.onBackPressed(); } - item = null; - ctx.onBackPressed(); }) .setNegativeButton(android.R.string.no, null) .show(); @@ -153,17 +158,20 @@ public class AddressDetailFragment extends Fragment { case R.id.export: { new AlertDialog.Builder(ctx) .setMessage(R.string.confirm_export) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_TITLE, item + - EXPORT_POSTFIX); - WifExporter exporter = new WifExporter(Singleton - .getBitmessageContext(ctx)); - exporter.addIdentity(item); - shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString - ()); - startActivity(Intent.createChooser(shareIntent, null)); + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TITLE, item + + EXPORT_POSTFIX); + WifExporter exporter = new WifExporter(Singleton + .getBitmessageContext(ctx)); + exporter.addIdentity(item); + shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString + ()); + startActivity(Intent.createChooser(shareIntent, null)); + } }) .setNegativeButton(android.R.string.no, null) .show(); @@ -225,8 +233,12 @@ public class AddressDetailFragment extends Fragment { if (item.getPrivateKey() == null) { Switch active = (Switch) rootView.findViewById(R.id.active); active.setChecked(item.isSubscribed()); - active.setOnCheckedChangeListener((buttonView, isChecked) -> - item.setSubscribed(isChecked)); + active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton button, boolean checked) { + item.setSubscribed(checked); + } + }); ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id .pubkey_available); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index df6785c..93e1f2e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -33,6 +33,7 @@ import android.widget.TextView; import com.google.zxing.integration.android.IntentIntegrator; import java.util.Collections; +import java.util.Comparator; import java.util.List; import ch.dissem.apps.abit.listener.ActionBarListener; @@ -56,28 +57,31 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr public void updateList() { List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()) .getContacts(); - Collections.sort(addresses, (lhs, rhs) -> { - // Yields the following order: - // * Subscribed addresses come first - // * Addresses with Aliases (alphabetically) - // * Addresses (alphabetically) - if (lhs.isSubscribed() == rhs.isSubscribed()) { - if (lhs.getAlias() != null) { - if (rhs.getAlias() != null) { - return lhs.getAlias().compareTo(rhs.getAlias()); + Collections.sort(addresses, new Comparator<BitmessageAddress>() { + @Override + public int compare(BitmessageAddress lhs, BitmessageAddress rhs) { + // Yields the following order: + // * Subscribed addresses come first + // * Addresses with Aliases (alphabetically) + // * Addresses (alphabetically) + if (lhs.isSubscribed() == rhs.isSubscribed()) { + if (lhs.getAlias() != null) { + if (rhs.getAlias() != null) { + return lhs.getAlias().compareTo(rhs.getAlias()); + } else { + return -1; + } + } else if (rhs.getAlias() != null) { + return 1; } else { - return -1; + return lhs.getAddress().compareTo(rhs.getAddress()); } - } else if (rhs.getAlias() != null) { - return 1; - } else { - return lhs.getAddress().compareTo(rhs.getAddress()); } - } - if (lhs.isSubscribed()) { - return -1; - } else { - return 1; + if (lhs.isSubscribed()) { + return -1; + } else { + return 1; + } } }); setListAdapter(new ArrayAdapter<BitmessageAddress>( diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 0308ced..a73a566 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -96,7 +96,12 @@ public class ComposeMessageFragment extends Fragment { final ContactAdapter adapter = new ContactAdapter(getContext()); recipientInput.setAdapter(adapter); recipientInput.setOnItemClickListener( - (parent, view, position, id) -> recipient = adapter.getItem(position) + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { + adapter.getItem(pos); + } + } ); recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index eff13da..cf5d51f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Switch; @@ -60,28 +61,34 @@ public class CreateAddressActivity extends AppCompatActivity { } final Button cancel = (Button) findViewById(R.id.cancel); - cancel.setOnClickListener(v -> { - setResult(Activity.RESULT_CANCELED); - finish(); + cancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setResult(Activity.RESULT_CANCELED); + finish(); + } }); final Button ok = (Button) findViewById(R.id.do_import); - ok.setOnClickListener(v -> { - String addressText = String.valueOf(address.getText()).trim(); - try { - BitmessageAddress bmAddress = new BitmessageAddress(addressText); - bmAddress.setAlias(label.getText().toString()); + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String addressText = String.valueOf(address.getText()).trim(); + try { + BitmessageAddress bmAddress = new BitmessageAddress(addressText); + bmAddress.setAlias(label.getText().toString()); - BitmessageContext bmc = Singleton.getBitmessageContext - (CreateAddressActivity.this); - bmc.addContact(bmAddress); - if (subscribe.isChecked()) { - bmc.addSubscribtion(bmAddress); + BitmessageContext bmc = Singleton.getBitmessageContext + (CreateAddressActivity.this); + bmc.addContact(bmAddress); + if (subscribe.isChecked()) { + bmc.addSubscribtion(bmAddress); + } + + setResult(Activity.RESULT_OK); + finish(); + } catch (RuntimeException e) { + address.setError(getString(R.string.error_illegal_address)); } - - setResult(Activity.RESULT_OK); - finish(); - } catch (RuntimeException e) { - address.setError(getString(R.string.error_illegal_address)); } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java index 4d335b0..be9d954 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java @@ -67,15 +67,18 @@ public class ImportIdentitiesFragment extends Fragment { } catch (IOException e) { return super.onCreateView(inflater, container, savedInstanceState); } - view.findViewById(R.id.finish).setOnClickListener(v -> { - importer.importAll(adapter.getSelected()); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - for (BitmessageAddress selected : adapter.getSelected()) { - mainActivity.addIdentityEntry(selected); + view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + importer.importAll(adapter.getSelected()); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress selected : adapter.getSelected()) { + mainActivity.addIdentityEntry(selected); + } } + getActivity().finish(); } - getActivity().finish(); }); return view; } diff --git a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java index d1498ed..2e6f227 100644 --- a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java @@ -28,6 +28,7 @@ import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; +import com.github.angads25.filepicker.controller.DialogSelectionListener; import com.github.angads25.filepicker.model.DialogConfigs; import com.github.angads25.filepicker.model.DialogProperties; import com.github.angads25.filepicker.view.FilePickerDialog; @@ -60,16 +61,19 @@ public class InputWifFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_import_input, container, false); wifData = (TextView) view.findViewById(R.id.wif_input); - view.findViewById(R.id.next).setOnClickListener(v -> { - Bundle bundle = new Bundle(); - bundle.putString(WIF_DATA, wifData.getText().toString()); + view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Bundle bundle = new Bundle(); + bundle.putString(WIF_DATA, wifData.getText().toString()); - ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); - fragment.setArguments(bundle); + ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); + fragment.setArguments(bundle); - getFragmentManager().beginTransaction() - .replace(R.id.content, fragment) - .commit(); + getFragmentManager().beginTransaction() + .replace(R.id.content, fragment) + .commit(); + } }); return view; } @@ -89,23 +93,26 @@ public class InputWifFragment extends Fragment { properties.extensions = null; FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties); dialog.setTitle(getString(R.string.select_file_title)); - dialog.setDialogSelectionListener(files -> { - if (files.length > 0) { - try (InputStream in = new FileInputStream(files[0])) { - ByteArrayOutputStream data = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - //noinspection ConstantConditions - while ((length = in.read(buffer)) != -1) { - data.write(buffer, 0, length); + dialog.setDialogSelectionListener(new DialogSelectionListener() { + @Override + public void onSelectedFilePaths(String[] files) { + if (files.length > 0) { + try (InputStream in = new FileInputStream(files[0])) { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + //noinspection ConstantConditions + while ((length = in.read(buffer)) != -1) { + data.write(buffer, 0, length); + } + wifData.setText(data.toString("UTF-8")); + } catch (IOException e) { + Toast.makeText( + getActivity(), + R.string.error_loading_data, + Toast.LENGTH_SHORT + ).show(); } - wifData.setText(data.toString("UTF-8")); - } catch (IOException e) { - Toast.makeText( - getActivity(), - R.string.error_loading_data, - Toast.LENGTH_SHORT - ).show(); } } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index af705e0..812f6cf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -24,10 +24,12 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; import android.widget.RelativeLayout; import android.widget.Toast; import com.github.amlcurran.showcaseview.ShowcaseView; +import com.github.amlcurran.showcaseview.targets.Target; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.IconicsDrawable; @@ -35,6 +37,7 @@ import com.mikepenz.materialdrawer.AccountHeader; import com.mikepenz.materialdrawer.AccountHeaderBuilder; import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; +import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener; import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.ProfileDrawerItem; @@ -182,13 +185,16 @@ public class MainActivity extends AppCompatActivity .setStyle(R.style.CustomShowcaseTheme) .setContentTitle(R.string.full_node) .setContentText(R.string.full_node_description) - .setTarget(() -> { - View view = drawer.getStickyFooter(); - int[] location = new int[2]; - view.getLocationInWindow(location); - int x = location[0] + 7 * view.getWidth() / 8; - int y = location[1] + view.getHeight() / 2; - return new Point(x, y); + .setTarget(new Target() { + @Override + public Point getPoint() { + View view = drawer.getStickyFooter(); + int[] location = new int[2]; + view.getLocationInWindow(location); + int x = location[0] + 7 * view.getWidth() / 8; + int y = location[1] + view.getHeight() / 2; + return new Point(x, y); + } }) .replaceEndButton(R.layout.showcase_button) .hideOnTouchOutside() @@ -246,32 +252,36 @@ public class MainActivity extends AppCompatActivity .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) - .withOnAccountHeaderListener((view, profile, currentProfile) -> { - switch ((int) profile.getIdentifier()) { - case ADD_IDENTITY: - addIdentityDialog(); - break; - case MANAGE_IDENTITY: - BitmessageAddress identity = Singleton.getIdentity(this); - if (identity == null) { - Toast.makeText(this, R.string.no_identity_warning, LENGTH_LONG).show(); - } else { - Intent show = new Intent(MainActivity.this, - AddressDetailActivity.class); - show.putExtra(AddressDetailFragment.ARG_ITEM, identity); - startActivity(show); - } - break; - default: - if (profile instanceof ProfileDrawerItem) { - Object tag = ((ProfileDrawerItem) profile).getTag(); - if (tag instanceof BitmessageAddress) { - Singleton.setIdentity((BitmessageAddress) tag); + .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { + @Override + public boolean onProfileChanged(View view, IProfile profile, boolean current) { + switch ((int) profile.getIdentifier()) { + case ADD_IDENTITY: + addIdentityDialog(); + break; + case MANAGE_IDENTITY: + BitmessageAddress identity = Singleton.getIdentity(MainActivity.this); + if (identity == null) { + Toast.makeText(MainActivity.this, + R.string.no_identity_warning, LENGTH_LONG).show(); + } else { + Intent show = new Intent(MainActivity.this, + AddressDetailActivity.class); + show.putExtra(AddressDetailFragment.ARG_ITEM, identity); + startActivity(show); } - } + break; + default: + if (profile instanceof ProfileDrawerItem) { + Object tag = ((ProfileDrawerItem) profile).getTag(); + if (tag instanceof BitmessageAddress) { + Singleton.setIdentity((BitmessageAddress) tag); + } + } + } + // false if it should close the drawer + return false; } - // false if it should close the drawer - return false; }) .build(); if (profiles.size() > 2) { // There's always the add and manage identity items @@ -333,11 +343,15 @@ public class MainActivity extends AppCompatActivity .withName(R.string.full_node) .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) .withChecked(isRunning()) - .withOnCheckedChangeListener((drawerItem, buttonView, isChecked) -> { - if (isChecked) { - checkAndStartNode(); - } else { - stopService(new Intent(this, BitmessageService.class)); + .withOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + checkAndStartNode(); + } else { + stopService(new Intent(MainActivity.this, BitmessageService.class)); + } } }); @@ -347,36 +361,39 @@ public class MainActivity extends AppCompatActivity .withAccountHeader(accountHeader) .withDrawerItems(drawerItems) .addStickyDrawerItems(nodeSwitch) - .withOnDrawerItemClickListener((view, position, item) -> { - if (item.getTag() instanceof Label) { - selectedLabel = (Label) item.getTag(); - showSelectedLabel(); - return false; - } else if (item instanceof Nameable<?>) { - Nameable<?> ni = (Nameable<?>) item; - switch (ni.getName().getTextRes()) { - case R.string.contacts_and_subscriptions: - if (!(getSupportFragmentManager().findFragmentById(R.id - .item_list) instanceof AddressListFragment)) { - changeList(new AddressListFragment()); - } else { - ((AddressListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(); - } - break; - case R.string.settings: - startActivity(new Intent(MainActivity.this, SettingsActivity - .class)); - break; - case R.string.archive: - selectedLabel = null; - showSelectedLabel(); - break; - case R.string.full_node: - return true; + .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { + @Override + public boolean onItemClick(View view, int position, IDrawerItem item) { + if (item.getTag() instanceof Label) { + selectedLabel = (Label) item.getTag(); + showSelectedLabel(); + return false; + } else if (item instanceof Nameable<?>) { + Nameable<?> ni = (Nameable<?>) item; + switch (ni.getName().getTextRes()) { + case R.string.contacts_and_subscriptions: + if (!(getSupportFragmentManager().findFragmentById(R.id + .item_list) instanceof AddressListFragment)) { + changeList(new AddressListFragment()); + } else { + ((AddressListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(); + } + break; + case R.string.settings: + startActivity(new Intent(MainActivity.this, SettingsActivity + .class)); + break; + case R.string.archive: + selectedLabel = null; + showSelectedLabel(); + break; + case R.string.full_node: + return true; + } } + return false; } - return false; }) .withShowDrawerOnFirstLaunch(true) .build(); @@ -461,11 +478,14 @@ public class MainActivity extends AppCompatActivity } public static void updateNodeSwitch() { - MainActivity i = getInstance(); + final MainActivity i = getInstance(); if (i != null) { - i.runOnUiThread(() -> { - i.nodeSwitch.withChecked(i.bmc.isRunning()); - i.drawer.updateStickyFooterItem(i.nodeSwitch); + i.runOnUiThread(new Runnable() { + @Override + public void run() { + i.nodeSwitch.withChecked(i.bmc.isRunning()); + i.drawer.updateStickyFooterItem(i.nodeSwitch); + } }); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 25239f6..5dced97 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -31,6 +31,7 @@ import android.widget.TextView; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import java.util.Iterator; +import java.util.regex.Matcher; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; @@ -110,7 +111,12 @@ public class MessageDetailFragment extends Fragment { Linkify.addLinks(messageBody, WEB_URLS); Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, - (match, url) -> match.group() + new Linkify.TransformFilter() { + @Override + public String transformUrl(Matcher match, String url) { + return match.group(); + } + } ); messageBody.setLinksClickable(true); diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index c905f65..223e8de 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -46,21 +46,27 @@ public class SettingsFragment addPreferencesFromResource(R.xml.preferences); Preference about = findPreference("about"); - about.setOnPreferenceClickListener(preference -> { - new LibsBuilder() - .withActivityTitle(getActivity().getString(R.string.about)) - .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) - .withAboutIconShown(true) - .withAboutVersionShown(true) - .withAboutDescription(getString(R.string.about_app)) - .start(getActivity()); - return true; + about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new LibsBuilder() + .withActivityTitle(getActivity().getString(R.string.about)) + .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .withAboutDescription(getString(R.string.about_app)) + .start(getActivity()); + return true; + } }); Preference status = findPreference("status"); - status.setOnPreferenceClickListener(preference -> { - startActivity(new Intent(getActivity(), StatusActivity.class)); - return true; + status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivity(new Intent(getActivity(), StatusActivity.class)); + return true; + } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java index f35a52a..ffc7a4e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/AddressSelectorAdapter.java @@ -21,6 +21,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.TextView; import java.util.ArrayList; @@ -75,9 +76,12 @@ public class AddressSelectorAdapter super(v); checkbox = (CheckBox) v.findViewById(R.id.checkbox); address = (TextView) v.findViewById(R.id.address); - checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (data != null) { - data.selected = isChecked; + checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if (data != null) { + data.selected = isChecked; + } } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index 6beb402..f50a3f6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -105,8 +105,18 @@ public class SwipeableMessageAdapter } public SwipeableMessageAdapter() { - itemViewOnClickListener = this::onItemViewClick; - swipeableViewContainerOnClickListener = this::onSwipeableViewContainerClick; + itemViewOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemViewClick(view); + } + }; + swipeableViewContainerOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + onSwipeableViewContainerClick(view); + } + }; // SwipeableItemAdapter requires stable ID, and also // have to implement the getItemId() method appropriately. diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java index 59e16b6..76158e3 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/AddIdentityDialogFragment.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.dialog; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -60,47 +61,55 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { getDialog().setTitle(R.string.add_identity); View view = inflater.inflate(R.layout.dialog_add_identity, container, false); final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); - view.findViewById(R.id.ok).setOnClickListener(v -> { - final Context ctx = getActivity().getBaseContext(); - switch (radioGroup.getCheckedRadioButtonId()) { - case R.id.create_identity: - Toast.makeText(ctx, - R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Void, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(Void... args) { - return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(ctx, - R.string.toast_identity_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(chan); + view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final Context ctx = getActivity().getBaseContext(); + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.create_identity: + Toast.makeText(ctx, + R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Void, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(Void... args) { + return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); } - } - }.execute(); - break; - case R.id.import_identity: - startActivity(new Intent(ctx, ImportIdentityActivity.class)); - break; - case R.id.add_chan: - addChanDialog(); - break; - case R.id.add_deterministic_address: - new DeterministicIdentityDialogFragment().show(getFragmentManager(), - "dialog"); - break; - default: - return; + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(ctx, + R.string.toast_identity_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); + } + } + }.execute(); + break; + case R.id.import_identity: + startActivity(new Intent(ctx, ImportIdentityActivity.class)); + break; + case R.id.add_chan: + addChanDialog(); + break; + case R.id.add_deterministic_address: + new DeterministicIdentityDialogFragment().show(getFragmentManager(), + "dialog"); + break; + default: + return; + } + dismiss(); + } + }); + view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); } - dismiss(); }); - view.findViewById(R.id.dismiss).setOnClickListener(v -> dismiss()); return view; } @@ -113,31 +122,34 @@ public class AddIdentityDialogFragment extends AppCompatDialogFragment { new AlertDialog.Builder(activity) .setTitle(R.string.add_chan) .setView(dialogView) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> { - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - Toast.makeText(ctx, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<String, Void, BitmessageAddress>() { - @Override - protected BitmessageAddress doInBackground(String... args) { - String pass = args[0]; - BitmessageAddress chan = bmc.createChan(pass); - chan.setAlias(pass); - bmc.addresses().save(chan); - return chan; - } - - @Override - protected void onPostExecute(BitmessageAddress chan) { - Toast.makeText(ctx, - R.string.toast_chan_created, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { - mainActivity.addIdentityEntry(chan); + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + Toast.makeText(ctx, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<String, Void, BitmessageAddress>() { + @Override + protected BitmessageAddress doInBackground(String... args) { + String pass = args[0]; + BitmessageAddress chan = bmc.createChan(pass); + chan.setAlias(pass); + bmc.addresses().save(chan); + return chan; } - } - }.execute(passphrase.getText().toString()); + + @Override + protected void onPostExecute(BitmessageAddress chan) { + Toast.makeText(ctx, + R.string.toast_chan_created, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + mainActivity.addIdentityEntry(chan); + } + } + }.execute(passphrase.getText().toString()); + } }) .setNegativeButton(R.string.cancel, null) .show(); diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java index a31b84b..d0832c0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/DeterministicIdentityDialogFragment.java @@ -56,68 +56,76 @@ public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment getDialog().setTitle(R.string.add_deterministic_address); View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false); view.findViewById(R.id.ok) - .setOnClickListener(v -> { - dismiss(); - final Context context = getActivity().getBaseContext(); - View dialogView = getView(); - TextView label = (TextView) dialogView.findViewById(R.id.label); - TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); - TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id - .number_of_identities); - Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); + final Context context = getActivity().getBaseContext(); + View dialogView = getView(); + assert dialogView != null; + TextView label = (TextView) dialogView.findViewById(R.id.label); + TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); + TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id + .number_of_identities); + Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); - Toast.makeText(context, R.string.toast_long_running_operation, - Toast.LENGTH_SHORT).show(); - new AsyncTask<Object, Void, List<BitmessageAddress>>() { - @Override - protected List<BitmessageAddress> doInBackground(Object... args) { - String label = (String) args[0]; - String pass = (String) args[1]; - int numberOfAddresses = (int) args[2]; - boolean shorter = (boolean) args[3]; - List<BitmessageAddress> identities = bmc.createDeterministicAddresses - (pass, - numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); - int i = 0; - for (BitmessageAddress identity : identities) { - i++; - if (identities.size() == 1) { - identity.setAlias(label); - } else { - identity.setAlias(label + " (" + i + ")"); - } - bmc.addresses().save(identity); - } - return identities; - } - - @Override - protected void onPostExecute(List<BitmessageAddress> identities) { - int messageRes; - if (identities.size() == 1) { - messageRes = R.string.toast_identity_created; - } else { - messageRes = R.string.toast_identities_created; - } - Toast.makeText(context, - messageRes, - Toast.LENGTH_SHORT).show(); - MainActivity mainActivity = MainActivity.getInstance(); - if (mainActivity != null) { + Toast.makeText(context, R.string.toast_long_running_operation, + Toast.LENGTH_SHORT).show(); + new AsyncTask<Object, Void, List<BitmessageAddress>>() { + @Override + protected List<BitmessageAddress> doInBackground(Object... args) { + String label = (String) args[0]; + String pass = (String) args[1]; + int numberOfAddresses = (int) args[2]; + boolean shorter = (boolean) args[3]; + List<BitmessageAddress> identities = bmc.createDeterministicAddresses + (pass, + numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); + int i = 0; for (BitmessageAddress identity : identities) { - mainActivity.addIdentityEntry(identity); + i++; + if (identities.size() == 1) { + identity.setAlias(label); + } else { + identity.setAlias(label + " (" + i + ")"); + } + bmc.addresses().save(identity); + } + return identities; + } + + @Override + protected void onPostExecute(List<BitmessageAddress> identities) { + int messageRes; + if (identities.size() == 1) { + messageRes = R.string.toast_identity_created; + } else { + messageRes = R.string.toast_identities_created; + } + Toast.makeText(context, + messageRes, + Toast.LENGTH_SHORT).show(); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress identity : identities) { + mainActivity.addIdentityEntry(identity); + } } } - } - }.execute( - label.getText().toString(), - passphrase.getText().toString(), - Integer.valueOf(numberOfAddresses.getText().toString()), - shorter.isChecked() - ); + }.execute( + label.getText().toString(), + passphrase.getText().toString(), + Integer.valueOf(numberOfAddresses.getText().toString()), + shorter.isChecked() + ); + } }); - view.findViewById(R.id.dismiss) - .setOnClickListener(v -> dismiss()); + view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); + } + }); return view; } diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java b/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java index 2f573b2..a0f48f5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/FullNodeDialogActivity.java @@ -19,6 +19,7 @@ package ch.dissem.apps.abit.dialog; import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.view.View; import ch.dissem.apps.abit.R; import ch.dissem.apps.abit.service.BitmessageService; @@ -34,11 +35,19 @@ public class FullNodeDialogActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.dialog_full_node); - findViewById(R.id.ok).setOnClickListener(v -> { - startService(new Intent(this, BitmessageService.class)); - updateNodeSwitch(); - finish(); + findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startService(new Intent(FullNodeDialogActivity.this, BitmessageService.class)); + updateNodeSwitch(); + finish(); + } + }); + findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } }); - findViewById(R.id.dismiss).setOnClickListener(v -> finish()); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java index e980d93..d9a3f7b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java +++ b/app/src/main/java/ch/dissem/apps/abit/listener/MessageListener.java @@ -48,32 +48,38 @@ public class MessageListener implements BitmessageContext.Listener { @Override public void receive(final Plaintext plaintext) { - pool.submit(() -> { - unacknowledged.addFirst(plaintext); - numberOfUnacknowledgedMessages++; - if (unacknowledged.size() > 5) { - unacknowledged.removeLast(); - } - if (numberOfUnacknowledgedMessages == 1) { - notification.singleNotification(plaintext); - } else { - notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); - } - notification.show(); + pool.submit(new Runnable() { + @Override + public void run() { + unacknowledged.addFirst(plaintext); + numberOfUnacknowledgedMessages++; + if (unacknowledged.size() > 5) { + unacknowledged.removeLast(); + } + if (numberOfUnacknowledgedMessages == 1) { + notification.singleNotification(plaintext); + } else { + notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); + } + notification.show(); - // If MainActivity is shown, update the sidebar badges - MainActivity main = MainActivity.getInstance(); - if (main != null) { - main.updateUnread(); + // If MainActivity is shown, update the sidebar badges + MainActivity main = MainActivity.getInstance(); + if (main != null) { + main.updateUnread(); + } } }); } public void resetNotification() { - pool.submit(() -> { - notification.hide(); - unacknowledged.clear(); - numberOfUnacknowledgedMessages = 0; + pool.submit(new Runnable() { + @Override + public void run() { + notification.hide(); + unacknowledged.clear(); + numberOfUnacknowledgedMessages = 0; + } }); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java index e8812ea..fa91b5d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java +++ b/app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.java @@ -17,12 +17,14 @@ package ch.dissem.apps.abit.pow; import android.content.Context; +import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; @@ -50,34 +52,40 @@ public class ServerPowEngine implements ProofOfWorkEngine, InternalContext public ServerPowEngine(Context ctx) { this.ctx = ctx; - pool = Executors.newCachedThreadPool(r -> { - Thread thread = Executors.defaultThreadFactory().newThread(r); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; + pool = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(@NonNull Runnable r) { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + } }); } @Override public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) { - pool.execute(() -> { - BitmessageAddress identity = Singleton.getIdentity(ctx); - if (identity == null) throw new RuntimeException("No Identity for calculating POW"); + pool.execute(new Runnable() { + @Override + public void run() { + BitmessageAddress identity = Singleton.getIdentity(ctx); + if (identity == null) throw new RuntimeException("No Identity for calculating POW"); - ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, - CALCULATE, target); - SyncAdapter.startPowSync(ctx); - try { - CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> - (request); - cryptoMsg.signAndEncrypt( - identity, - cryptography().createPublicKey(identity.getPublicDecryptionKey()) - ); - context.getNetworkHandler().send( - Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), - cryptoMsg); - } catch (Exception e) { - LOG.error(e.getMessage(), e); + ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, + CALCULATE, target); + SyncAdapter.startPowSync(ctx); + try { + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> + (request); + cryptoMsg.signAndEncrypt( + identity, + cryptography().createPublicKey(identity.getPublicDecryptionKey()) + ); + context.getNetworkHandler().send( + Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), + cryptoMsg); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } } }); } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java index 3b41df7..e171edf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/ProofOfWorkService.java @@ -92,23 +92,26 @@ public class ProofOfWorkService extends Service { } private void calculateNonce(final PowItem item) { - engine.calculateNonce(item.initialHash, item.targetValue, (initialHash, nonce) -> { - try { - item.callback.onNonceCalculated(initialHash, nonce); - } finally { - PowItem next; - synchronized (queue) { - next = queue.poll(); - if (next == null) { - calculating = false; - stopForeground(true); - stopSelf(); - } else { - notification.update(queue.size()).show(); + engine.calculateNonce(item.initialHash, item.targetValue, new ProofOfWorkEngine.Callback() { + @Override + public void onNonceCalculated(byte[] initialHash, byte[] nonce) { + try { + item.callback.onNonceCalculated(initialHash, nonce); + } finally { + PowItem next; + synchronized (queue) { + next = queue.poll(); + if (next == null) { + calculating = false; + stopForeground(true); + stopSelf(); + } else { + notification.update(queue.size()).show(); + } + } + if (next != null) { + calculateNonce(next); } - } - if (next != null) { - calculateNonce(next); } } }); diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index db3360b..1bceb0b 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -110,9 +110,9 @@ public class Singleton { return powRepo; } - public static BitmessageAddress getIdentity(Context ctx) { + public static BitmessageAddress getIdentity(final Context ctx) { if (identity == null) { - BitmessageContext bmc = getBitmessageContext(ctx); + final BitmessageContext bmc = getBitmessageContext(ctx); synchronized (Singleton.class) { if (identity == null) { List<BitmessageAddress> identities = bmc.addresses() From 96af8e0750773bedc2b9a9124c15c7b7a4d3aac4 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 4 Nov 2016 22:03:04 +0100 Subject: [PATCH 082/110] Some chan improvements - When replying on a chan, the receiving address isn't used as the sending identity - Chans are listed as contacts as well --- app/build.gradle | 8 ++++---- .../ch/dissem/apps/abit/ComposeMessageActivity.java | 10 +++++++++- .../apps/abit/repository/AndroidAddressRepository.java | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cf33c47..7401c9b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 targetSdkVersion 25 - versionCode 9 - versionName "1.0-beta9" + versionCode 11 + versionName "1.0-beta11" jackOptions.enabled = false } compileOptions { @@ -28,8 +28,8 @@ android { } buildTypes { release { - minifyEnabled true - shrinkResources true + minifyEnabled false + shrinkResources false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 6bcccf6..270c6ba 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -24,6 +24,8 @@ import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; /** @@ -68,7 +70,13 @@ public class ComposeMessageActivity extends AppCompatActivity { private static Intent getReplyIntent(Context ctx, Plaintext item) { Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class); replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); - replyIntent.putExtra(EXTRA_IDENTITY, item.getTo()); + BitmessageAddress receivingIdentity = item.getTo(); + if (receivingIdentity.isChan()) { + // I hate when people send as chan, so it won't be the default behaviour. + replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx)); + } else { + replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity); + } String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) .equalsIgnoreCase("RE:")) { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 5557112..24cc1bb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -110,7 +110,7 @@ public class AndroidAddressRepository implements AddressRepository { @Override public List<BitmessageAddress> getContacts() { - return find("private_key IS NULL"); + return find("private_key IS NULL OR chan = '1'"); } private List<BitmessageAddress> find(String where) { From 4be1f1e5058fbd44d6b44f97403ac7fb557bd4b9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 5 Dec 2016 12:37:32 +0100 Subject: [PATCH 083/110] Added code to send messages with extended encoding --- app/build.gradle | 11 +- .../apps/abit/ComposeMessageActivity.java | 14 +- .../apps/abit/ComposeMessageFragment.java | 120 ++++++++++++++---- .../dialog/SelectEncodingDialogFragment.java | 98 ++++++++++++++ .../repository/AndroidAddressRepository.java | 2 - .../main/res/drawable/ic_action_encoding.xml | 24 ++++ .../layout/dialog_select_message_encoding.xml | 90 +++++++++++++ app/src/main/res/menu/compose.xml | 8 +- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 3 + 10 files changed, 336 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java create mode 100644 app/src/main/res/drawable/ic_action_encoding.xml create mode 100644 app/src/main/res/layout/dialog_select_message_encoding.xml diff --git a/app/build.gradle b/app/build.gradle index 7401c9b..20fa736 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,13 +36,13 @@ android { } } -ext.jabitVersion = '2.0.4' +//ext.jabitVersion = '2.0.4' +ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.0.0' - compile 'com.android.support:support-v4:25.0.0' - compile 'com.android.support:design:25.0.0' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta1' + compile 'com.android.support:appcompat-v7:25.0.1' + compile 'com.android.support:support-v4:25.0.1' + compile 'com.android.support:design:25.0.1' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -73,6 +73,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' + compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' } idea.module { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 270c6ba..2df2a68 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -28,6 +28,8 @@ import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; +import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; + /** * Compose a new message. */ @@ -37,6 +39,8 @@ public class ComposeMessageActivity extends AppCompatActivity { public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"; public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"; + public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"; + public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -69,14 +73,22 @@ public class ComposeMessageActivity extends AppCompatActivity { private static Intent getReplyIntent(Context ctx, Plaintext item) { Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class); - replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); BitmessageAddress receivingIdentity = item.getTo(); if (receivingIdentity.isChan()) { + // reply to chan, not to the sender of the message + replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity); // I hate when people send as chan, so it won't be the default behaviour. replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx)); } else { + replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity); } + // if the original message was sent using extended encoding, use it as well + // so features like threading can be supported + if (item.getEncoding() == EXTENDED) { + replyIntent.putExtra(EXTRA_ENCODING, EXTENDED); + replyIntent.putExtra(EXTRA_PARENT, item); + } String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) .equalsIgnoreCase("RE:")) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index a73a566..b3fa8e1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -27,18 +28,28 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.EditText; +import android.widget.Toast; import java.util.List; import ch.dissem.apps.abit.adapter.ContactAdapter; +import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment; import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import static android.app.Activity.RESULT_OK; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_PARENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; /** * Compose a new message. @@ -52,6 +63,8 @@ public class ComposeMessageFragment extends Fragment { private EditText subjectInput; private EditText bodyInput; private boolean broadcast; + private Plaintext.Encoding encoding; + private Plaintext parent; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -79,6 +92,14 @@ public class ComposeMessageFragment extends Fragment { if (getArguments().containsKey(EXTRA_CONTENT)) { content = getArguments().getString(EXTRA_CONTENT); } + if (getArguments().containsKey(EXTRA_ENCODING)) { + encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); + } else { + encoding = Plaintext.Encoding.SIMPLE; + } + if (getArguments().containsKey(EXTRA_PARENT)) { + parent = (Plaintext) getArguments().getSerializable(EXTRA_PARENT); + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -145,36 +166,83 @@ public class ComposeMessageFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.send: - if (broadcast) { - Singleton.getBitmessageContext(getContext()).broadcast(identity, - subjectInput.getText().toString(), - bodyInput.getText().toString()); - } else { - String inputString = recipientInput.getText().toString(); - if (recipient == null || !recipient.toString().equals(inputString)) { - try { - recipient = new BitmessageAddress(inputString); - } catch (Exception e) { - List<BitmessageAddress> contacts = Singleton.getAddressRepository - (getContext()).getContacts(); - for (BitmessageAddress contact : contacts) { - if (inputString.equalsIgnoreCase(contact.getAlias())) { - recipient = contact; - if (inputString.equals(contact.getAlias())) - break; - } - } - } - } - Singleton.getBitmessageContext(getContext()).send(identity, recipient, - subjectInput.getText().toString(), - bodyInput.getText().toString()); - } - getActivity().finish(); + send(); + return true; + case R.id.select_encoding: + SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment(); + encodingDialog.setTargetFragment(this, 0); + encodingDialog.show(getFragmentManager(), "select encoding dialog"); return true; default: return super.onOptionsItemSelected(item); } } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == 0 && resultCode == RESULT_OK) { + encoding = (Plaintext.Encoding) data.getSerializableExtra(EXTRA_ENCODING); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void send() { + Plaintext.Builder builder; + BitmessageContext bmc = Singleton.getBitmessageContext(getContext()); + if (broadcast) { + builder = new Plaintext.Builder(BROADCAST) + .from(identity); + } else { + String inputString = recipientInput.getText().toString(); + if (recipient == null || !recipient.toString().equals(inputString)) { + try { + recipient = new BitmessageAddress(inputString); + } catch (Exception e) { + List<BitmessageAddress> contacts = Singleton.getAddressRepository + (getContext()).getContacts(); + for (BitmessageAddress contact : contacts) { + if (inputString.equalsIgnoreCase(contact.getAlias())) { + recipient = contact; + if (inputString.equals(contact.getAlias())) + break; + } + } + } + } + builder = new Plaintext.Builder(MSG) + .from(identity) + .to(recipient); + } + switch (encoding) { + case SIMPLE: + builder.message( + subjectInput.getText().toString(), + bodyInput.getText().toString() + ); + break; + case EXTENDED: + builder.message( + new ExtendedEncoding.Builder() + .message() + .subject(subjectInput.getText().toString()) + .body(bodyInput.getText().toString()) + .build() + ); + break; + default: + Toast.makeText( + getContext(), + getContext().getString(R.string.error_unsupported_encoding, encoding), + Toast.LENGTH_LONG + ).show(); + builder.message( + subjectInput.getText().toString(), + bodyInput.getText().toString() + ); + } + bmc.send(builder.build()); + getActivity().finish(); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java new file mode 100644 index 0000000..c9f3837 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit.dialog; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioGroup; +import android.widget.Toast; + +import ch.dissem.apps.abit.ImportIdentityActivity; +import ch.dissem.apps.abit.MainActivity; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Pubkey; + +import static android.app.Activity.RESULT_OK; +import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING; +import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; +import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; + +/** + * @author Christian Basler + */ + +public class SelectEncodingDialogFragment extends AppCompatDialogFragment { + private Plaintext.Encoding encoding; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) { + encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); + } + if (encoding == null) { + encoding = SIMPLE; + } + getDialog().setTitle(R.string.select_encoding_title); + View view = inflater.inflate(R.layout.dialog_select_message_encoding, container, false); + final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); + switch (encoding) { + case SIMPLE: + radioGroup.check(R.id.simple); + break; + case EXTENDED: + radioGroup.check(R.id.extended); + break; + } + view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.extended: + encoding = EXTENDED; + break; + case R.id.simple: + break; + default: + encoding = SIMPLE; + return; + } + Intent result = new Intent(); + result.putExtra(EXTRA_ENCODING, encoding); + getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, result); + dismiss(); + } + }); + view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); + } + }); + return view; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java index 24cc1bb..a0d6b4d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.java @@ -199,8 +199,6 @@ public class AndroidAddressRepository implements AddressRepository { ByteArrayOutputStream out = new ByteArrayOutputStream(); address.getPubkey().writeUnencrypted(out); values.put(COLUMN_PUBLIC_KEY, out.toByteArray()); - } else { - values.put(COLUMN_PUBLIC_KEY, (byte[]) null); } if (address.getPrivateKey() != null) { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); diff --git a/app/src/main/res/drawable/ic_action_encoding.xml b/app/src/main/res/drawable/ic_action_encoding.xml new file mode 100644 index 0000000..92b7f03 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_encoding.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" /> +</vector> diff --git a/app/src/main/res/layout/dialog_select_message_encoding.xml b/app/src/main/res/layout/dialog_select_message_encoding.xml new file mode 100644 index 0000000..4a1991f --- /dev/null +++ b/app/src/main/res/layout/dialog_select_message_encoding.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2016 Christian Basler + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="18dp" + android:paddingEnd="24dp" + android:paddingStart="24dp" + android:paddingTop="18dp"> + + <TextView + android:id="@+id/description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/select_encoding_warning" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintTop_creator="1"/> + + + <RadioGroup + android:id="@+id/radioGroup" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingBottom="24dp" + android:paddingTop="24dp" + app:layout_constraintLeft_toLeftOf="@+id/description" + app:layout_constraintRight_toRightOf="@+id/description" + app:layout_constraintTop_toBottomOf="@+id/description"> + + <RadioButton + android:id="@+id/simple" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:text="SIMPLE"/> + + <RadioButton + android:id="@+id/extended" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:text="EXTENDED"/> + + </RadioGroup> + + + <Button + android:id="@+id/ok" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/ok" + android:textColor="@color/colorAccent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintRight_toRightOf="@+id/radioGroup" + app:layout_constraintTop_toBottomOf="@+id/radioGroup" + tools:layout_constraintRight_creator="1" + tools:layout_constraintTop_creator="1"/> + + <Button + android:id="@+id/dismiss" + style="?android:attr/borderlessButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/cancel" + android:textColor="@color/colorAccent" + app:layout_constraintRight_toLeftOf="@+id/ok" + app:layout_constraintTop_toBottomOf="@+id/radioGroup"/> + +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/menu/compose.xml b/app/src/main/res/menu/compose.xml index 922a705..b6aafb2 100644 --- a/app/src/main/res/menu/compose.xml +++ b/app/src/main/res/menu/compose.xml @@ -6,4 +6,10 @@ app:showAsAction="always" android:icon="@drawable/ic_action_send" android:title="@string/send"/> -</menu> \ No newline at end of file + <item + android:id="@+id/select_encoding" + app:showAsAction="never" + android:icon="@drawable/ic_action_encoding" + android:title="Select encoding" + /> +</menu> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ec78307..c28b572 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -110,4 +110,5 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="status_received">empfangen</string> <string name="status_sent">gesendet</string> <string name="status_sent_acknowledged">Empfang bestätigt</string> + <string name="error_unsupported_encoding">Codierung ‘%s’ wird nicht unterstützt, es wird ‘SIMPLE’ verwendet.</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 262b398..9edcf98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,4 +109,7 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="status_sent">sent</string> <string name="status_received">received</string> <string name="outbox">Outbox</string> + <string name="error_unsupported_encoding">Unsupported encoding ‘%s’, using ‘SIMPLE’ instead.</string> + <string name="select_encoding_warning">‘EXTENDED’ is a new encoding that\‘s not widely supported yet but has some interesting features. To stay on the save side, select ‘SIMPLE’.</string> + <string name="select_encoding_title">Select Message Encoding</string> </resources> From 0a8459750acb0b5173399425223c8054488b708d Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 5 Dec 2016 12:59:37 +0100 Subject: [PATCH 084/110] Fixed/added translation --- app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c28b572..ce4217b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -111,4 +111,6 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="status_sent">gesendet</string> <string name="status_sent_acknowledged">Empfang bestätigt</string> <string name="error_unsupported_encoding">Codierung ‘%s’ wird nicht unterstützt, es wird ‘SIMPLE’ verwendet.</string> + <string name="select_encoding_title">Kodierung auswählen</string> + <string name="select_encoding_warning">‘EXTENDED’ ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle ‘SIMPLE’ um sicher zu gehen.</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9edcf98..397ad27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,6 +110,6 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="status_received">received</string> <string name="outbox">Outbox</string> <string name="error_unsupported_encoding">Unsupported encoding ‘%s’, using ‘SIMPLE’ instead.</string> - <string name="select_encoding_warning">‘EXTENDED’ is a new encoding that\‘s not widely supported yet but has some interesting features. To stay on the save side, select ‘SIMPLE’.</string> + <string name="select_encoding_warning">‘EXTENDED’ is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select ‘SIMPLE’.</string> <string name="select_encoding_title">Select Message Encoding</string> </resources> From 6f26e84f71e834db98e1e5287bc785f6f1c02805 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 6 Dec 2016 06:40:58 +0100 Subject: [PATCH 085/110] Some code quality improvements --- .../apps/abit/ComposeMessageFragment.java | 1 + .../dialog/SelectEncodingDialogFragment.java | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index b3fa8e1..3a357b6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -240,6 +240,7 @@ public class ComposeMessageFragment extends Fragment { subjectInput.getText().toString(), bodyInput.getText().toString() ); + break; } bmc.send(builder.build()); getActivity().finish(); diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java index c9f3837..3b8b917 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java @@ -16,9 +16,7 @@ package ch.dissem.apps.abit.dialog; -import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatDialogFragment; @@ -26,14 +24,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RadioGroup; -import android.widget.Toast; -import ch.dissem.apps.abit.ImportIdentityActivity; -import ch.dissem.apps.abit.MainActivity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ch.dissem.apps.abit.R; -import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.payload.Pubkey; import static android.app.Activity.RESULT_OK; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING; @@ -45,6 +41,7 @@ import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; */ public class SelectEncodingDialogFragment extends AppCompatDialogFragment { + private static final Logger LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment.class); private Plaintext.Encoding encoding; @Nullable @@ -67,6 +64,9 @@ public class SelectEncodingDialogFragment extends AppCompatDialogFragment { case EXTENDED: radioGroup.check(R.id.extended); break; + default: + LOG.warn("Unexpected encoding: " + encoding); + break; } view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { @Override @@ -76,9 +76,10 @@ public class SelectEncodingDialogFragment extends AppCompatDialogFragment { encoding = EXTENDED; break; case R.id.simple: + encoding = SIMPLE; break; default: - encoding = SIMPLE; + dismiss(); return; } Intent result = new Intent(); From 249c97d4bacb844f9238b3c0c09755a1140fedcd Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 16 Dec 2016 07:44:01 +0100 Subject: [PATCH 086/110] Added function to cleanup inventory (mine grew to 1.5GB) but no automatism yet --- app/build.gradle | 8 ++--- .../apps/abit/ComposeMessageFragment.java | 6 ++-- .../ch/dissem/apps/abit/SettingsFragment.java | 34 +++++++++++++++++++ .../apps/abit/service/BitmessageService.java | 4 +-- app/src/main/res/values-de/strings.xml | 4 +++ app/src/main/res/values/strings.xml | 4 +++ app/src/main/res/xml/preferences.xml | 5 +++ build.gradle | 2 +- 8 files changed, 57 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 20fa736..f96adba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ if (project.hasProperty("project.configs") //noinspection GroovyMissingReturnStatement android { compileSdkVersion 25 - buildToolsVersion "25.0.0" + buildToolsVersion "25.0.1" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() @@ -40,9 +40,9 @@ android { ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.0.1' - compile 'com.android.support:support-v4:25.0.1' - compile 'com.android.support:design:25.0.1' + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:support-v4:25.1.0' + compile 'com.android.support:design:25.1.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 3a357b6..4413933 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -38,7 +38,7 @@ import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import static android.app.Activity.RESULT_OK; import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; @@ -223,10 +223,10 @@ public class ComposeMessageFragment extends Fragment { break; case EXTENDED: builder.message( - new ExtendedEncoding.Builder() - .message() + new Message.Builder() .subject(subjectInput.getText().toString()) .body(bodyInput.getText().toString()) + .addParent(parent) .build() ); break; diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 223e8de..15fc6b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -19,14 +19,17 @@ package ch.dissem.apps.abit; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.os.AsyncTask; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.widget.Toast; import com.mikepenz.aboutlibraries.Libs; import com.mikepenz.aboutlibraries.LibsBuilder; +import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; @@ -59,6 +62,37 @@ public class SettingsFragment return true; } }); + final Preference cleanup = findPreference("cleanup"); + cleanup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AsyncTask<Void, Void, Void>() { + @Override + protected void onPreExecute() { + cleanup.setEnabled(false); + Toast.makeText(getActivity(), R.string.cleanup_notification_start, Toast + .LENGTH_SHORT).show(); + } + + @Override + protected Void doInBackground(Void... voids) { + Singleton.getBitmessageContext(getActivity()).cleanup(); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + Toast.makeText( + getActivity(), + R.string.cleanup_notification_end, + Toast.LENGTH_LONG + ).show(); + cleanup.setEnabled(true); + } + }.execute(); + return true; + } + }); Preference status = findPreference("status"); status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 1b8c645..5c8a528 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -33,11 +33,11 @@ import static ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIF * onPerformSync(). */ public class BitmessageService extends Service { - private NetworkNotification notification = null; private static BitmessageContext bmc = null; - private static volatile boolean running = false; + private NetworkNotification notification = null; + public static boolean isRunning() { return running && bmc.isRunning(); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ce4217b..4d03c23 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -113,4 +113,8 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu <string name="error_unsupported_encoding">Codierung ‘%s’ wird nicht unterstützt, es wird ‘SIMPLE’ verwendet.</string> <string name="select_encoding_title">Kodierung auswählen</string> <string name="select_encoding_warning">‘EXTENDED’ ist ein neues Datenformat welches noch nicht überall funktioniert, es hat jedoch einige interessante Vorteile. Wähle ‘SIMPLE’ um sicher zu gehen.</string> + <string name="cleanup">Aufräumen</string> + <string name="cleanup_notification_start">Aufräumarbeiten gestartet</string> + <string name="cleanup_notification_end">Aufräumarbeiten beendet</string> + <string name="cleanup_summary">Veraltete Inventareinträge werden entfernt</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 397ad27..96037f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,4 +112,8 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="error_unsupported_encoding">Unsupported encoding ‘%s’, using ‘SIMPLE’ instead.</string> <string name="select_encoding_warning">‘EXTENDED’ is a new message format that\'s not widely supported yet but has some interesting features. To stay on the save side, select ‘SIMPLE’.</string> <string name="select_encoding_title">Select Message Encoding</string> + <string name="cleanup">Cleanup</string> + <string name="cleanup_summary">Remove outdated inventory entries</string> + <string name="cleanup_notification_start">Cleanup started</string> + <string name="cleanup_notification_end">Cleanup finished</string> </resources> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 85568c0..fd31078 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -44,6 +44,11 @@ android:action="android.intent.action.VIEW" android:data="@string/help_out_link"/> </Preference> + <Preference + android:key="cleanup" + android:title="@string/cleanup" + android:summary="@string/cleanup_summary" + /> <Preference android:key="status" android:summary="@string/status_summary" diff --git a/build.gradle b/build.gradle index 554b46e..149d3e7 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From d5407b7b01690ba1cd9ca81118173f58ca14bbd1 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 24 Dec 2016 00:15:56 +0100 Subject: [PATCH 087/110] Minor improvement --- .../repository/AndroidMessageRepository.java | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 228c67e..a1500f4 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -33,7 +33,6 @@ import java.util.LinkedList; import java.util.List; import ch.dissem.apps.abit.R; -import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -242,25 +241,12 @@ public class AndroidMessageRepository extends AbstractMessageRepository { @Override public void save(Plaintext message) { + saveContactIfNecessary(message.getFrom()); + saveContactIfNecessary(message.getTo()); SQLiteDatabase db = sql.getWritableDatabase(); try { db.beginTransaction(); - // save from address if necessary - BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message - .getFrom().getAddress()); - if (message.getId() == null) { - if (savedAddress == null) { - ctx.getAddressRepository().save(message.getFrom()); - } else if (savedAddress.getPubkey() == null) { - savedAddress.setPubkey(message.getFrom().getPubkey()); - ctx.getAddressRepository().save(savedAddress); - } - } - if (savedAddress != null) { - message.getFrom().setAlias(savedAddress.getAlias()); - } - // save message if (message.getId() == null) { insert(db, message); From e74b18ed3f7a6bcfdb9e7c9ca19b9ff8bacae1e7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 1 Jan 2017 15:00:46 +0100 Subject: [PATCH 088/110] Fixed bug where network notification couldn't be dismissed and updated build tools --- app/build.gradle | 2 +- .../ch/dissem/apps/abit/notification/NetworkNotification.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f96adba..c78c327 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ if (project.hasProperty("project.configs") //noinspection GroovyMissingReturnStatement android { compileSdkVersion 25 - buildToolsVersion "25.0.1" + buildToolsVersion "25.0.2" defaultConfig { applicationId "ch.dissem.apps." + appName.toLowerCase() diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index 606c389..4a11cc2 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java @@ -109,6 +109,7 @@ public class NetworkNotification extends AbstractNotification { public void run() { if (!update()) { cancel(); + ctx.stopService(new Intent(ctx, BitmessageService.class)); } NetworkNotification.super.show(); } From 37371a0e94f7dbb7edc7a46068f3e7f5a8d348b3 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 27 Jan 2017 08:25:40 +0100 Subject: [PATCH 089/110] Moved dependency so it isn't groupt with the test dependencies --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c78c327..61e6175 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,10 +70,10 @@ dependencies { transitive = true } compile 'com.github.angads25:filepicker:1.0.6' + compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' } idea.module { From 18d72d727c74a3f970e6c1b7c00b17ba234842fb Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 23 Feb 2017 17:37:15 +0100 Subject: [PATCH 090/110] Fixed notification title for when there are more than 5 messages --- .../dissem/apps/abit/notification/NewMessageNotification.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index 06e4c02..3bddc2e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java +++ b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java @@ -92,14 +92,12 @@ public class NewMessageNotification extends AbstractNotification { numberOfUnacknowledgedMessages) { NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.ic_notification_new_message) - .setContentTitle(ctx.getString(R.string.n_new_messages, unacknowledged.size())) + .setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)) .setContentText(ctx.getString(R.string.app_name)); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (unacknowledged) { - inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, - numberOfUnacknowledgedMessages)); for (Plaintext msg : unacknowledged) { Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable From 65c03bd6380eeb2fb9d8e5c5446d09e0be300803 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 23 Feb 2017 17:38:00 +0100 Subject: [PATCH 091/110] Load lists asynchronously --- .../ch/dissem/apps/abit/MainActivity.java | 156 +++++++++++------- .../dissem/apps/abit/MessageListFragment.java | 63 +++++-- .../ch/dissem/apps/abit/SettingsFragment.java | 8 +- 3 files changed, 148 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 812f6cf..7b98637 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit; import android.content.Intent; import android.graphics.Point; +import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; @@ -53,8 +54,9 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; @@ -125,12 +127,6 @@ public class MainActivity extends AppCompatActivity super.onCreate(savedInstanceState); instance = new WeakReference<>(this); bmc = Singleton.getBitmessageContext(this); - List<Label> labels = bmc.messages().getLabels(); - if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) { - selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL); - } else if (selectedLabel == null) { - selectedLabel = labels.get(0); - } setContentView(R.layout.activity_message_list); @@ -155,7 +151,7 @@ public class MainActivity extends AppCompatActivity listFragment.setActivateOnItemClick(true); } - createDrawer(toolbar, labels); + createDrawer(toolbar); // handle intents Intent intent = getIntent(); @@ -217,22 +213,8 @@ public class MainActivity extends AppCompatActivity } } - private void createDrawer(Toolbar toolbar, Collection<Label> labels) { + private void createDrawer(Toolbar toolbar) { final ArrayList<IProfile> profiles = new ArrayList<>(); - for (BitmessageAddress identity : bmc.addresses().getIdentities()) { - LOG.info("Adding identity " + identity.getAddress()); - profiles.add(new ProfileDrawerItem() - .withIcon(new Identicon(identity)) - .withName(identity.toString()) - .withNameShown(true) - .withEmail(identity.getAddress()) - .withTag(identity) - ); - } - if (profiles.isEmpty()) { - // Create an initial identity - Singleton.getIdentity(this); - } profiles.add(new ProfileSettingDrawerItem() .withName(getString(R.string.add_identity)) .withDescription(getString(R.string.add_identity_summary)) @@ -288,43 +270,7 @@ public class MainActivity extends AppCompatActivity accountHeader.setActiveProfile(profiles.get(0), true); } - ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); - for (Label label : labels) { - PrimaryDrawerItem item = new PrimaryDrawerItem() - .withName(label.toString()) - .withTag(label); - if (label.getType() == null) { - item.withIcon(CommunityMaterial.Icon.cmd_label) - .withIconColor(label.getColor()); - } else { - switch (label.getType()) { - case INBOX: - item.withIcon(GoogleMaterial.Icon.gmd_inbox); - break; - case DRAFT: - item.withIcon(CommunityMaterial.Icon.cmd_file); - break; - case OUTBOX: - item.withIcon(CommunityMaterial.Icon.cmd_outbox); - break; - case SENT: - item.withIcon(CommunityMaterial.Icon.cmd_send); - break; - case BROADCAST: - item.withIcon(CommunityMaterial.Icon.cmd_rss); - break; - case UNREAD: - item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); - break; - case TRASH: - item.withIcon(GoogleMaterial.Icon.gmd_delete); - break; - default: - item.withIcon(CommunityMaterial.Icon.cmd_label); - } - } - drawerItems.add(item); - } + final ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); drawerItems.add(new PrimaryDrawerItem() .withName(R.string.archive) .withTag(null) @@ -397,6 +343,59 @@ public class MainActivity extends AppCompatActivity }) .withShowDrawerOnFirstLaunch(true) .build(); + + new AsyncTask<Void, Void, List<BitmessageAddress>>() { + @Override + protected List<BitmessageAddress> doInBackground(Void... params) { + List<BitmessageAddress> identities = bmc.addresses().getIdentities(); + if (identities.isEmpty()) { + // Create an initial identity + Singleton.getIdentity(MainActivity.this); + } + return identities; + } + + @Override + protected void onPostExecute(List<BitmessageAddress> identities) { + for (BitmessageAddress identity : identities) { + addIdentityEntry(identity); + } + } + }.execute(); + + new AsyncTask<Void, Void, List<Label>>() { + @Override + protected List<Label> doInBackground(Void... params) { + return bmc.messages().getLabels(); + } + + @Override + protected void onPostExecute(List<Label> labels) { + if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) { + selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL); + } else if (selectedLabel == null) { + selectedLabel = labels.get(0); + } + for (Label label : labels) { + addLabelEntry(label); + } + showSelectedLabel(); + } + }.execute(); + } + + @Override + protected void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + savedInstanceState.putSerializable("selectedLabel", selectedLabel); + } + + @Override + @SuppressWarnings("unchecked") + protected void onRestoreInstanceState(Bundle savedInstanceState) { + selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); + showSelectedLabel(); + super.onRestoreInstanceState(savedInstanceState); } private void addIdentityDialog() { @@ -429,6 +428,43 @@ public class MainActivity extends AppCompatActivity } } + public void addLabelEntry(Label label) { + PrimaryDrawerItem item = new PrimaryDrawerItem() + .withName(label.toString()) + .withTag(label); + if (label.getType() == null) { + item.withIcon(CommunityMaterial.Icon.cmd_label) + .withIconColor(label.getColor()); + } else { + switch (label.getType()) { + case INBOX: + item.withIcon(GoogleMaterial.Icon.gmd_inbox); + break; + case DRAFT: + item.withIcon(CommunityMaterial.Icon.cmd_file); + break; + case OUTBOX: + item.withIcon(CommunityMaterial.Icon.cmd_outbox); + break; + case SENT: + item.withIcon(CommunityMaterial.Icon.cmd_send); + break; + case BROADCAST: + item.withIcon(CommunityMaterial.Icon.cmd_rss); + break; + case UNREAD: + item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); + break; + case TRASH: + item.withIcon(GoogleMaterial.Icon.gmd_delete); + break; + default: + item.withIcon(CommunityMaterial.Icon.cmd_label); + } + } + drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3); + } + public void updateIdentityEntry(BitmessageAddress identity) { for (IProfile profile : accountHeader.getProfiles()) { if (profile instanceof ProfileDrawerItem) { diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index fbd2f00..55e91e7 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -17,6 +17,7 @@ package ch.dissem.apps.abit; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; @@ -37,7 +38,9 @@ import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeMana import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; +import java.util.Collections; import java.util.List; +import java.util.Objects; import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter; import ch.dissem.apps.abit.listener.ActionBarListener; @@ -97,20 +100,29 @@ public class MessageListFragment extends Fragment implements ListHolder { MainActivity activity = (MainActivity) getActivity(); messageRepo = Singleton.getMessageRepository(activity); - doUpdateList(activity.getSelectedLabel()); + currentLabel = activity.getSelectedLabel(); + doUpdateList(currentLabel); } @Override public void updateList(Label label) { - currentLabel = label; - - if (!isVisible()) return; + if (!isResumed()) { + currentLabel = label; + return; + } + if (!Objects.equals(currentLabel, label)) { + adapter.setData(label, Collections.<Plaintext>emptyList()); + adapter.notifyDataSetChanged(); + } doUpdateList(label); } - private void doUpdateList(Label label) { - List<Plaintext> messages = Singleton.getMessageRepository(getContext()).findMessages(label); + private void doUpdateList(final Label label) { + currentLabel = label; + if (emptyTrashMenuItem != null) { + emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); + } if (getActivity() instanceof ActionBarListener) { if (label != null) { ((ActionBarListener) getActivity()).updateTitle(label.toString()); @@ -118,11 +130,21 @@ public class MessageListFragment extends Fragment implements ListHolder { ((ActionBarListener) getActivity()).updateTitle(getString(R.string.archive)); } } - if (emptyTrashMenuItem != null) { - emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); - } - adapter.setData(label, messages); - adapter.notifyDataSetChanged(); + new AsyncTask<Void, Void, List<Plaintext>>() { + + @Override + protected List<Plaintext> doInBackground(Void... params) { + return messageRepo.findMessages(label); + } + + @Override + protected void onPostExecute(List<Plaintext> messages) { + if (adapter != null) { + adapter.setData(label, messages); + adapter.notifyDataSetChanged(); + } + } + }.execute(); } @Override @@ -276,11 +298,20 @@ public class MessageListFragment extends Fragment implements ListHolder { case R.id.empty_trash: if (currentLabel.getType() != Label.Type.TRASH) return true; - MessageRepository repo = Singleton.getMessageRepository(getContext()); - for (Plaintext message : repo.findMessages(currentLabel)) { - repo.remove(message); - } - updateList(currentLabel); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + for (Plaintext message : messageRepo.findMessages(currentLabel)) { + messageRepo.remove(message); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + updateList(currentLabel); + } + }.execute(); return true; default: return false; diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 15fc6b0..6879025 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -67,23 +67,25 @@ public class SettingsFragment @Override public boolean onPreferenceClick(Preference preference) { new AsyncTask<Void, Void, Void>() { + private Context ctx = getActivity().getApplicationContext(); + @Override protected void onPreExecute() { cleanup.setEnabled(false); - Toast.makeText(getActivity(), R.string.cleanup_notification_start, Toast + Toast.makeText(ctx, R.string.cleanup_notification_start, Toast .LENGTH_SHORT).show(); } @Override protected Void doInBackground(Void... voids) { - Singleton.getBitmessageContext(getActivity()).cleanup(); + Singleton.getBitmessageContext(ctx).cleanup(); return null; } @Override protected void onPostExecute(Void aVoid) { Toast.makeText( - getActivity(), + ctx, R.string.cleanup_notification_end, Toast.LENGTH_LONG ).show(); From 300da1730d17f144a8d189f3c69eaa77866935be Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 24 Feb 2017 17:33:59 +0100 Subject: [PATCH 092/110] Archive is now handled somewhat differently, so we can distinguish between 'archive' and 'not initialized'. --- .../ch/dissem/apps/abit/MainActivity.java | 9 ++------ .../dissem/apps/abit/MessageListFragment.java | 20 ++++++++++++----- .../abit/adapter/SwipeableMessageAdapter.java | 5 +++-- .../repository/AndroidMessageRepository.java | 22 +++++++++++++++---- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 7b98637..c994207 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -54,14 +54,13 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; +import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; @@ -273,7 +272,7 @@ public class MainActivity extends AppCompatActivity final ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); drawerItems.add(new PrimaryDrawerItem() .withName(R.string.archive) - .withTag(null) + .withTag(AndroidMessageRepository.LABEL_ARCHIVE) .withIcon(CommunityMaterial.Icon.cmd_archive) ); drawerItems.add(new DividerDrawerItem()); @@ -330,10 +329,6 @@ public class MainActivity extends AppCompatActivity startActivity(new Intent(MainActivity.this, SettingsActivity .class)); break; - case R.string.archive: - selectedLabel = null; - showSelectedLabel(); - break; case R.string.full_node: return true; } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 55e91e7..f2bc0fb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -100,8 +100,7 @@ public class MessageListFragment extends Fragment implements ListHolder { MainActivity activity = (MainActivity) getActivity(); messageRepo = Singleton.getMessageRepository(activity); - currentLabel = activity.getSelectedLabel(); - doUpdateList(currentLabel); + doUpdateList(activity.getSelectedLabel()); } @Override @@ -119,15 +118,24 @@ public class MessageListFragment extends Fragment implements ListHolder { } private void doUpdateList(final Label label) { + if (label == null) { + if (getActivity() instanceof ActionBarListener) { + ((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name)); + } + adapter.setData(null, Collections.<Plaintext>emptyList()); + adapter.notifyDataSetChanged(); + return; + } currentLabel = label; if (emptyTrashMenuItem != null) { - emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); + emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH); } if (getActivity() instanceof ActionBarListener) { - if (label != null) { - ((ActionBarListener) getActivity()).updateTitle(label.toString()); + ActionBarListener actionBarListener = (ActionBarListener) getActivity(); + if ("archive".equals(label.toString())) { + actionBarListener.updateTitle(getString(R.string.archive)); } else { - ((ActionBarListener) getActivity()).updateTitle(getString(R.string.archive)); + actionBarListener.updateTitle(label.toString()); } } new AsyncTask<Void, Void, List<Plaintext>>() { diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java index f50a3f6..7990c62 100644 --- a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -45,6 +45,7 @@ import ch.dissem.apps.abit.util.Assets; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; +import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; /** @@ -199,7 +200,7 @@ public class SwipeableMessageAdapter @Override public int onGetSwipeReactionType(ViewHolder holder, int position, int x, int y) { - if (label == null || label.getType() == Label.Type.TRASH) { + if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) { return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; } return REACTION_CAN_SWIPE_BOTH_H; @@ -217,7 +218,7 @@ public class SwipeableMessageAdapter bgRes = R.drawable.bg_swipe_item_left; break; case DRAWABLE_SWIPE_RIGHT_BACKGROUND: - if (label == null || label.getType() == Label.Type.TRASH) { + if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) { bgRes = R.drawable.bg_swipe_item_neutral; } else { bgRes = R.drawable.bg_swipe_item_right; diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index a1500f4..40bc9c6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -48,6 +48,8 @@ import static java.lang.String.valueOf; public class AndroidMessageRepository extends AbstractMessageRepository { private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class); + public static final Label LABEL_ARCHIVE = new Label("archive", null, 0); + private static final String TABLE_NAME = "Message"; private static final String COLUMN_ID = "id"; private static final String COLUMN_IV = "iv"; @@ -82,6 +84,15 @@ public class AndroidMessageRepository extends AbstractMessageRepository { this.context = ctx; } + @Override + public List<Plaintext> findMessages(Label label) { + if (label == LABEL_ARCHIVE) { + return super.findMessages((Label) null); + } else { + return super.findMessages(label); + } + } + public List<Label> findLabels(String where) { List<Label> result = new LinkedList<>(); @@ -153,15 +164,18 @@ public class AndroidMessageRepository extends AbstractMessageRepository { public int countUnread(Label label) { String[] args; String where; - if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND "; + if (label == null){ + return 0; + } + if (label == LABEL_ARCHIVE) { + where = ""; args = new String[]{ - label.getId().toString(), Label.Type.UNREAD.name() }; } else { - where = ""; + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND "; args = new String[]{ + label.getId().toString(), Label.Type.UNREAD.name() }; } From 42cf18445c0674c0c38e954596cd69f019151d1a Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 18 Mar 2017 07:09:03 +0100 Subject: [PATCH 093/110] Bumped Jabit version to prepare for conversations --- app/build.gradle | 20 ++--- .../V4.0__Create_table_message_parent.sql | 11 +++ .../ch/dissem/apps/abit/MainActivity.java | 31 ++++--- .../ch/dissem/apps/abit/SettingsFragment.java | 6 +- .../repository/AndroidMessageRepository.java | 87 ++++++++++++++++++- .../abit/repository/AndroidNodeRegistry.java | 7 ++ .../apps/abit/repository/SqlHelper.java | 8 +- .../ch/dissem/apps/abit/util/UuidUtils.java | 37 ++++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 10 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java diff --git a/app/build.gradle b/app/build.gradle index 61e6175..aec9574 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,9 +40,9 @@ android { ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.1.0' - compile 'com.android.support:support-v4:25.1.0' - compile 'com.android.support:design:25.1.0' + compile 'com.android.support:appcompat-v7:25.3.0' + compile 'com.android.support:support-v4:25.3.0' + compile 'com.android.support:design:25.3.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -52,25 +52,25 @@ dependencies { compile 'org.slf4j:slf4j-android:1.7.12' - compile 'com.mikepenz:materialize:1.0.0@aar' - compile('com.mikepenz:materialdrawer:5.6.0@aar') { + compile 'com.mikepenz:materialize:1.0.1@aar' + compile('com.mikepenz:materialdrawer:5.8.2@aar') { transitive = true } - compile('com.mikepenz:aboutlibraries:5.8.1@aar') { + compile('com.mikepenz:aboutlibraries:5.9.4@aar') { transitive = true } compile 'com.mikepenz:iconics:1.6.2@aar' - compile 'com.mikepenz:community-material-typeface:1.5.54.2@aar' + compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' compile 'com.google.zxing:core:3.3.0' compile 'io.github.yavski:fab-speed-dial:1.0.6' compile 'com.github.amlcurran.showcaseview:library:5.4.3' - compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar') { + compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } - compile 'com.github.angads25:filepicker:1.0.6' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' + compile 'com.github.angads25:filepicker:1.0.9' + compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql new file mode 100644 index 0000000..855140a --- /dev/null +++ b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql @@ -0,0 +1,11 @@ +ALTER TABLE Message ADD COLUMN conversation BINARY[16]; + +CREATE TABLE Message_Parent ( + parent BINARY(64) NOT NULL, + child BINARY(64) NOT NULL, + pos INT NOT NULL, + conversation BINARY[16] NOT NULL, + + PRIMARY KEY (parent, child), + FOREIGN KEY (child) REFERENCES Message (iv) +); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index c994207..fc45f75 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -55,6 +55,7 @@ import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; @@ -311,7 +312,15 @@ public class MainActivity extends AppCompatActivity public boolean onItemClick(View view, int position, IDrawerItem item) { if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); - showSelectedLabel(); + if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof + MessageListFragment) { + ((MessageListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(selectedLabel); + } else { + MessageListFragment listFragment = new MessageListFragment(); + changeList(listFragment); + listFragment.updateList(selectedLabel); + } return false; } else if (item instanceof Nameable<?>) { Nameable<?> ni = (Nameable<?>) item; @@ -374,7 +383,7 @@ public class MainActivity extends AppCompatActivity for (Label label : labels) { addLabelEntry(label); } - showSelectedLabel(); + drawer.setSelection(drawer.getDrawerItem(selectedLabel)); } }.execute(); } @@ -389,7 +398,9 @@ public class MainActivity extends AppCompatActivity @SuppressWarnings("unchecked") protected void onRestoreInstanceState(Bundle savedInstanceState) { selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); - showSelectedLabel(); + + drawer.setSelection(drawer.getDrawerItem(selectedLabel)); + super.onRestoreInstanceState(savedInstanceState); } @@ -439,7 +450,7 @@ public class MainActivity extends AppCompatActivity item.withIcon(CommunityMaterial.Icon.cmd_file); break; case OUTBOX: - item.withIcon(CommunityMaterial.Icon.cmd_outbox); + item.withIcon(CommunityMaterial.Icon.cmd_inbox_arrow_up); break; case SENT: item.withIcon(CommunityMaterial.Icon.cmd_send); @@ -521,18 +532,6 @@ public class MainActivity extends AppCompatActivity } } - private void showSelectedLabel() { - if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof - MessageListFragment) { - ((MessageListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(selectedLabel); - } else { - MessageListFragment listFragment = new MessageListFragment(); - changeList(listFragment); - listFragment.updateList(selectedLabel); - } - } - /** * Callback method from {@link ListSelectionListener} * indicating that the item with the given ID was selected. diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 6879025..111bf08 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -29,8 +29,10 @@ import android.widget.Toast; import com.mikepenz.aboutlibraries.Libs; import com.mikepenz.aboutlibraries.LibsBuilder; +import ch.dissem.apps.abit.repository.AndroidNodeRegistry; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.bitmessage.BitmessageContext; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; @@ -78,7 +80,9 @@ public class SettingsFragment @Override protected Void doInBackground(Void... voids) { - Singleton.getBitmessageContext(ctx).cleanup(); + BitmessageContext bmc = Singleton.getBitmessageContext(ctx); + bmc.cleanup(); + bmc.internals().getNodeRegistry().clear(); return null; } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 40bc9c6..3f7c8c1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -31,8 +31,10 @@ import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.UUID; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.util.UuidUtils; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -40,6 +42,8 @@ import ch.dissem.bitmessage.ports.AbstractMessageRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; +import static ch.dissem.apps.abit.util.UuidUtils.asUuid; +import static ch.dissem.bitmessage.utils.Strings.hex; import static java.lang.String.valueOf; /** @@ -65,6 +69,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private static final String COLUMN_RETRIES = "retries"; private static final String COLUMN_NEXT_TRY = "next_try"; private static final String COLUMN_INITIAL_HASH = "initial_hash"; + private static final String COLUMN_CONVERSATION = "conversation"; + + private static final String PARENTS_TABLE_NAME = "Message_Parent"; private static final String JOIN_TABLE_NAME = "Message_Label"; private static final String JT_COLUMN_MESSAGE = "message_id"; @@ -164,7 +171,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { public int countUnread(Label label) { String[] args; String where; - if (label == null){ + if (label == null) { return 0; } if (label == LABEL_ARCHIVE) { @@ -187,6 +194,72 @@ public class AndroidMessageRepository extends AbstractMessageRepository { ); } + @Override + public List<UUID> findConversations(Label label) { + String[] projection = { + COLUMN_CONVERSATION, + }; + + String where; + if (label == null) { + where = "id NOT IN (SELECT message_id FROM Message_Label)"; + } else { + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"; + } + List<UUID> result = new LinkedList<>(); + SQLiteDatabase db = sql.getReadableDatabase(); + try (Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, null + )) { + while (c.moveToNext()) { + byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)); + result.add(asUuid(uuidBytes)); + } + } + return result; + } + + + private void updateParents(SQLiteDatabase db, Plaintext message) { + if (message.getInventoryVector() == null || message.getParents().isEmpty()) { + // There are no parents to save yet (they are saved in the extended data, that's enough for now) + return; + } + db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(message.getInitialHash()).toString()}); + + byte[] childIV = message.getInventoryVector().getHash(); + // save new parents + int order = 0; + for (InventoryVector parentIV : message.getParents()) { + Plaintext parent = getMessage(parentIV); + mergeConversations(db, parent.getConversationId(), message.getConversationId()); + order++; + ContentValues values = new ContentValues(); + values.put("parent", parentIV.getHash()); + values.put("child", childIV); + values.put("pos", order); + values.put("conversation", UuidUtils.asBytes(message.getConversationId())); + db.insertOrThrow(PARENTS_TABLE_NAME, null, values); + } + } + + /** + * Replaces every occurrence of the source conversation ID with the target ID + * + * @param db is used to keep everything within one transaction + * @param source ID of the conversation to be merged + * @param target ID of the merge target + */ + private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) { + ContentValues values = new ContentValues(); + values.put("conversation", UuidUtils.asBytes(target)); + String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()}; + db.update(TABLE_NAME, values, "conversation=?", whereArgs); + db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs); + } + protected List<Plaintext> find(String where) { List<Plaintext> result = new LinkedList<>(); @@ -205,7 +278,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, - COLUMN_NEXT_TRY + COLUMN_NEXT_TRY, + COLUMN_CONVERSATION }; SQLiteDatabase db = sql.getReadableDatabase(); @@ -220,8 +294,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA)); Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex (COLUMN_TYPE))); - Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new - ByteArrayInputStream(data)); + Plaintext.Builder builder = Plaintext.readWithoutSignature(type, + new ByteArrayInputStream(data)); long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); @@ -240,6 +314,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { if (!c.isNull(nextTryColumn)) { builder.nextTry(c.getLong(nextTryColumn)); } + builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))); builder.labels(findLabels(id)); result.add(builder.build()); } @@ -268,6 +343,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { update(db, message); } + updateParents(db, message); + // remove existing labels db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())}); @@ -302,6 +379,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { values.put(COLUMN_TTL, message.getTTL()); values.put(COLUMN_RETRIES, message.getRetries()); values.put(COLUMN_NEXT_TRY, message.getNextTry()); + values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); long id = db.insertOrThrow(TABLE_NAME, null, values); message.setId(id); } @@ -322,6 +400,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { values.put(COLUMN_TTL, message.getTTL()); values.put(COLUMN_RETRIES, message.getRetries()); values.put(COLUMN_NEXT_TRY, message.getNextTry()); + values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java index b20c173..2183fcb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java @@ -10,6 +10,7 @@ import android.database.sqlite.SQLiteStatement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -56,6 +57,12 @@ public class AndroidNodeRegistry implements NodeRegistry { db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))}); } + @Override + public void clear() { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, null, null); + } + private Long loadExistingTime(NetworkAddress node) { SQLiteStatement statement = loadExistingStatement.get(); if (statement == null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 0e95170..db7d411 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -27,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - private static final int DATABASE_VERSION = 6; + private static final int DATABASE_VERSION = 7; private static final String DATABASE_NAME = "jabit.db"; private final Context ctx; @@ -61,6 +61,8 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V3.3__Create_table_node"); case 5: executeMigration(db, "V3.4__Add_label_outbox"); + case 6: + executeMigration(db, "V4.0__Create_table_message_parent"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than // DATABASE_VERSION. @@ -73,7 +75,7 @@ public class SqlHelper extends SQLiteOpenHelper { } } - public static StringBuilder join(long... numbers) { + static StringBuilder join(long... numbers) { StringBuilder streamList = new StringBuilder(); for (int i = 0; i < numbers.length; i++) { if (i > 0) streamList.append(", "); @@ -82,7 +84,7 @@ public class SqlHelper extends SQLiteOpenHelper { return streamList; } - public static StringBuilder join(Enum<?>... types) { + static StringBuilder join(Enum<?>... types) { StringBuilder streamList = new StringBuilder(); for (int i = 0; i < types.length; i++) { if (i > 0) streamList.append(", "); diff --git a/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java b/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java new file mode 100644 index 0000000..543dbac --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java @@ -0,0 +1,37 @@ +package ch.dissem.apps.abit.util; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * SQLite has no UUID data type, and UUIDs are therefore best saved as BINARY[16]. This class + * takes care of conversion between byte[16] and UUID. + * <p> + * Thanks to Brice Roncace on + * <a href="http://stackoverflow.com/questions/17893609/convert-uuid-to-byte-that-works-when-using-uuid-nameuuidfrombytesb"> + * Stack Overflow + * </a> + * for providing the UUID <-> byte[] conversions. + * </p> + */ +public class UuidUtils { + public static UUID asUuid(byte[] bytes) { + if (bytes == null) { + return null; + } + ByteBuffer bb = ByteBuffer.wrap(bytes); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + + public static byte[] asBytes(UUID uuid) { + if (uuid == null) { + return null; + } + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/build.gradle b/build.gradle index 149d3e7..b67f629 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e50d996..353d4a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 12 22:10:25 CEST 2016 +#Thu Mar 09 06:38:41 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip From 6be31c9f4e4f741b4cd656581717fa57b9c27fa1 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 20 Mar 2017 07:28:41 +0100 Subject: [PATCH 094/110] Fixed "add identity" dialog, updated dependencies --- app/build.gradle | 6 +++--- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index aec9574..5cee257 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,7 +50,7 @@ dependencies { compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" compile "ch.dissem.jabit:jabit-wif:$jabitVersion" - compile 'org.slf4j:slf4j-android:1.7.12' + compile 'org.slf4j:slf4j-android:1.7.25' compile 'com.mikepenz:materialize:1.0.1@aar' compile('com.mikepenz:materialdrawer:5.8.2@aar') { @@ -69,11 +69,11 @@ dependencies { compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } - compile 'com.github.angads25:filepicker:1.0.9' + compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.19' } idea.module { diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ba7dc9e..7d6f486 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,7 +19,7 @@ <item name="android:textColor">@color/colorAccent</item> </style> - <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog"> + <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth"> <item name="windowNoTitle">false</item> </style> </resources> From a203af654be1f863442fb5d557899dd713f8f3ab Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 23 Mar 2017 16:59:36 +0100 Subject: [PATCH 095/110] Add labels to message detail view --- app/build.gradle | 11 ++- .../ch/dissem/apps/abit/MainActivity.java | 40 +--------- .../apps/abit/MessageDetailFragment.java | 74 ++++++++++++++++++ .../repository/AndroidMessageRepository.java | 32 +------- .../java/ch/dissem/apps/abit/util/Labels.java | 77 +++++++++++++++++++ .../res/layout/fragment_message_detail.xml | 35 +++++---- app/src/main/res/layout/item_label.xml | 28 +++++++ 7 files changed, 215 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/Labels.java create mode 100644 app/src/main/res/layout/item_label.xml diff --git a/app/build.gradle b/app/build.gradle index 5cee257..c87655e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,11 +38,12 @@ android { //ext.jabitVersion = '2.0.4' ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' +ext.supportVersion = '25.2.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.3.0' - compile 'com.android.support:support-v4:25.3.0' - compile 'com.android.support:design:25.3.0' + compile "com.android.support:appcompat-v7:$supportVersion" + compile "com.android.support:support-v4:$supportVersion" + compile "com.android.support:design:$supportVersion" compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -59,7 +60,8 @@ dependencies { compile('com.mikepenz:aboutlibraries:5.9.4@aar') { transitive = true } - compile 'com.mikepenz:iconics:1.6.2@aar' + compile "com.mikepenz:iconics-core:2.8.2@aar" + compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' @@ -71,6 +73,7 @@ dependencies { } compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile 'org.solovyev.android.views:linear-layout-manager:0.5@aar' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.7.19' diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index fc45f75..9a2d9b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -48,14 +48,10 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IProfile; import com.mikepenz.materialdrawer.model.interfaces.Nameable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; @@ -65,6 +61,7 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -100,7 +97,6 @@ public class MainActivity extends AppCompatActivity public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; - private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final int ADD_IDENTITY = 1; private static final int MANAGE_IDENTITY = 2; @@ -437,37 +433,9 @@ public class MainActivity extends AppCompatActivity public void addLabelEntry(Label label) { PrimaryDrawerItem item = new PrimaryDrawerItem() .withName(label.toString()) - .withTag(label); - if (label.getType() == null) { - item.withIcon(CommunityMaterial.Icon.cmd_label) - .withIconColor(label.getColor()); - } else { - switch (label.getType()) { - case INBOX: - item.withIcon(GoogleMaterial.Icon.gmd_inbox); - break; - case DRAFT: - item.withIcon(CommunityMaterial.Icon.cmd_file); - break; - case OUTBOX: - item.withIcon(CommunityMaterial.Icon.cmd_inbox_arrow_up); - break; - case SENT: - item.withIcon(CommunityMaterial.Icon.cmd_send); - break; - case BROADCAST: - item.withIcon(CommunityMaterial.Icon.cmd_rss); - break; - case UNREAD: - item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); - break; - case TRASH: - item.withIcon(GoogleMaterial.Icon.gmd_delete); - break; - default: - item.withIcon(CommunityMaterial.Icon.cmd_label); - } - } + .withTag(label) + .withIcon(Labels.getIcon(label)) + .withIconColor(Labels.getColor(label)); drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 5dced97..857a195 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -16,8 +16,11 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.Menu; @@ -29,14 +32,19 @@ import android.widget.ImageView; import android.widget.TextView; import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.view.IconicsImageView; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Assets; import ch.dissem.apps.abit.util.Drawables; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -106,6 +114,11 @@ public class MessageDetailFragment extends Fragment { } else if (item.getType() == Plaintext.Type.BROADCAST) { ((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast); } + RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels); + LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels()); + labelView.setAdapter(labelAdapter); + labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2)); + TextView messageBody = (TextView) rootView.findViewById(R.id.text); messageBody.setText(item.getText()); @@ -197,4 +210,65 @@ public class MessageDetailFragment extends Fragment { } return false; } + + private static class LabelAdapter extends + RecyclerView.Adapter<LabelAdapter.ViewHolder> { + + private final List<Label> labels; + private final Context ctx; + + private LabelAdapter(Context ctx, Set<Label> labels) { + this.labels = new ArrayList<>(labels); + this.ctx = ctx; + } + + @Override + public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View contactView = inflater.inflate(R.layout.item_label, parent, false); + + // Return a new holder instance + return new ViewHolder(contactView); + } + + // Involves populating data into the item through holder + @Override + public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) { + // Get the data model based on position + Label label = labels.get(position); + + viewHolder.icon.setColor(Labels.getColor(label)); + viewHolder.icon.setIcon(Labels.getIcon(label)); + viewHolder.label.setText(Labels.getText(label, ctx)); + } + + // Returns the total count of items in the list + @Override + public int getItemCount() { + return labels.size(); + } + + // Provide a direct reference to each of the views within a data item + // Used to cache the views within the item layout for fast access + static class ViewHolder extends RecyclerView.ViewHolder { + // Your holder should contain a member variable + // for any view that will be set as you render a row + public IconicsImageView icon; + public TextView label; + + // We also create a constructor that accepts the entire item row + // and does the view lookups to find each subview + ViewHolder(View itemView) { + // Stores the itemView in a public final member variable that can be used + // to access the context from any ViewHolder instance. + super(itemView); + + icon = (IconicsImageView) itemView.findViewById(R.id.icon); + label = (TextView) itemView.findViewById(R.id.label); + } + } + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 3f7c8c1..b38cb1d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -33,7 +33,7 @@ import java.util.LinkedList; import java.util.List; import java.util.UUID; -import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.UuidUtils; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; @@ -129,35 +129,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private Label getLabel(Cursor c) { String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE)); Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName); - String text; - if (type == null) { + String text = Labels.getText(type, null, context); + if (text == null) { text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); - } else { - switch (type) { - case INBOX: - text = context.getString(R.string.inbox); - break; - case DRAFT: - text = context.getString(R.string.draft); - break; - case OUTBOX: - text = context.getString(R.string.outbox); - break; - case SENT: - text = context.getString(R.string.sent); - break; - case UNREAD: - text = context.getString(R.string.unread); - break; - case TRASH: - text = context.getString(R.string.trash); - break; - case BROADCAST: - text = context.getString(R.string.broadcasts); - break; - default: - text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); - } } Label label = new Label( text, diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Labels.java b/app/src/main/java/ch/dissem/apps/abit/util/Labels.java new file mode 100644 index 0000000..75c6def --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Labels.java @@ -0,0 +1,77 @@ +package ch.dissem.apps.abit.util; + +import android.content.Context; +import android.support.annotation.ColorInt; + +import com.mikepenz.community_material_typeface_library.CommunityMaterial; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.typeface.IIcon; + +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * Helper class to help with translating the default labels, getting label colors and so on. + */ +public class Labels { + public static String getText(Label label, Context ctx) { + return getText(label.getType(), label.toString(), ctx); + } + + public static String getText(Label.Type type, String alternative, Context ctx) { + if (type == null) { + return alternative; + } else { + switch (type) { + case INBOX: + return ctx.getString(R.string.inbox); + case DRAFT: + return ctx.getString(R.string.draft); + case OUTBOX: + return ctx.getString(R.string.outbox); + case SENT: + return ctx.getString(R.string.sent); + case UNREAD: + return ctx.getString(R.string.unread); + case TRASH: + return ctx.getString(R.string.trash); + case BROADCAST: + return ctx.getString(R.string.broadcasts); + default: + return alternative; + } + } + } + + public static IIcon getIcon(Label label) { + if (label.getType() == null) { + return CommunityMaterial.Icon.cmd_label; + } + switch (label.getType()) { + case INBOX: + return GoogleMaterial.Icon.gmd_inbox; + case DRAFT: + return CommunityMaterial.Icon.cmd_file; + case OUTBOX: + return CommunityMaterial.Icon.cmd_inbox_arrow_up; + case SENT: + return CommunityMaterial.Icon.cmd_send; + case BROADCAST: + return CommunityMaterial.Icon.cmd_rss; + case UNREAD: + return GoogleMaterial.Icon.gmd_markunread_mailbox; + case TRASH: + return GoogleMaterial.Icon.gmd_delete; + default: + return CommunityMaterial.Icon.cmd_label; + } + } + + @ColorInt + public static int getColor(Label label) { + if (label.getType() == null) { + return label.getColor(); + } + return 0xFF000000; + } +} diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index b0a7ef5..95ca52d 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -1,14 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" android:focusableInTouchMode="true" + android:paddingBottom="64dp" android:orientation="vertical"> <TextView @@ -24,7 +25,7 @@ android:padding="16dp" android:textAppearance="?android:attr/textAppearanceLarge" tools:ignore="UnusedAttribute" - tools:text="Subject"/> + tools:text="Subject" /> <ImageView android:id="@+id/status" @@ -32,17 +33,17 @@ android:layout_height="60dp" android:layout_alignParentEnd="true" android:layout_alignParentTop="true" - android:tint="@color/colorAccent" - tools:src="@drawable/ic_notification_proof_of_work" android:padding="16dp" - tools:ignore="ContentDescription"/> + android:tint="@color/colorAccent" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_notification_proof_of_work" /> <View android:id="@+id/divider" android:layout_width="fill_parent" android:layout_height="2dip" android:layout_below="@id/subject" - android:background="@color/divider"/> + android:background="@color/divider" /> <ImageView android:id="@+id/avatar" @@ -52,7 +53,7 @@ android:layout_below="@+id/divider" android:layout_margin="16dp" android:src="@color/colorAccent" - tools:ignore="ContentDescription"/> + tools:ignore="ContentDescription" /> <TextView android:id="@+id/sender" @@ -64,7 +65,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textStyle="bold" - tools:text="Sender"/> + tools:text="Sender" /> <TextView android:id="@+id/recipient" @@ -75,7 +76,7 @@ android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - tools:text="Recipient"/> + tools:text="Recipient" /> <TextView android:id="@+id/text" @@ -86,8 +87,16 @@ android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="32dp" - android:paddingBottom="64dp" + android:paddingBottom="16dp" android:text="New Text" - android:textIsSelectable="true"/> + android:textIsSelectable="true" /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/labels" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/text" + android:paddingEnd="16dp" + android:paddingStart="16dp" /> </RelativeLayout> </ScrollView> diff --git a/app/src/main/res/layout/item_label.xml b/app/src/main/res/layout/item_label.xml new file mode 100644 index 0000000..26869a5 --- /dev/null +++ b/app/src/main/res/layout/item_label.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <com.mikepenz.iconics.view.IconicsImageView + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + app:iiv_color="@android:color/black" + app:iiv_icon="cmd-label" /> + + <TextView + android:id="@+id/label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="24dp" + tools:text="Label" + android:layout_alignParentTop="true" + android:layout_toEndOf="@+id/icon" /> +</RelativeLayout> From e93dc43f89660242fefef13914186fc0074c4082 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 24 Mar 2017 07:35:57 +0100 Subject: [PATCH 096/110] Tiny code improvement --- app/src/main/res/layout/fragment_message_detail.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 95ca52d..1c63f9b 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -88,7 +88,7 @@ android:layout_marginRight="16dp" android:layout_marginTop="32dp" android:paddingBottom="16dp" - android:text="New Text" + tools:text="Message Body" android:textIsSelectable="true" /> <android.support.v7.widget.RecyclerView @@ -96,7 +96,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@+id/text" - android:paddingEnd="16dp" - android:paddingStart="16dp" /> + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> </RelativeLayout> </ScrollView> From 61c9cde2f5690a21ec5a7e1469bb15ad52dfd145 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 4 Apr 2017 06:29:44 +0200 Subject: [PATCH 097/110] Gradle wrapper and dependency versions bumped --- app/build.gradle | 9 +++++---- build.gradle | 3 +++ gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 22 +++++++++++++++------- gradlew.bat | 6 ------ 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c87655e..e5ec049 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' ext.supportVersion = '25.2.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile "com.android.support:appcompat-v7:$supportVersion" compile "com.android.support:support-v4:$supportVersion" compile "com.android.support:design:$supportVersion" @@ -64,19 +65,19 @@ dependencies { compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' - compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' + compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' - compile 'io.github.yavski:fab-speed-dial:1.0.6' + + compile 'io.github.yavski:fab-speed-dial:1.0.7' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' - compile 'org.solovyev.android.views:linear-layout-manager:0.5@aar' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.19' + testCompile 'org.mockito:mockito-core:2.7.21' } idea.module { diff --git a/build.gradle b/build.gradle index b67f629..c37a44a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,6 +18,8 @@ buildscript { } allprojects { + apply plugin: 'com.github.ben-manes.versions' + repositories { jcenter() mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..9b92f4a573d78bc17ece97d1c612ce7db44f2c44 100644 GIT binary patch delta 21799 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 delta 21015 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 S8gVG|1v@Jj9C(#*>;C{k4ZhI; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 353d4a8..8164845 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 09 06:38:41 CET 2017 +#Tue Apr 04 00:02:32 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/gradlew b/gradlew index 27309d9..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f6d5974..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line From 8770575c959126a099f1059c4cafca77c51933c6 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 7 Apr 2017 14:36:16 +0200 Subject: [PATCH 098/110] Fixed NullPointerException --- .../main/java/ch/dissem/apps/abit/MainActivity.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 9a2d9b0..e7aaa9a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -379,7 +379,10 @@ public class MainActivity extends AppCompatActivity for (Label label : labels) { addLabelEntry(label); } - drawer.setSelection(drawer.getDrawerItem(selectedLabel)); + IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel); + if (selectedDrawerItem != null) { + drawer.setSelection(selectedDrawerItem); + } } }.execute(); } @@ -395,8 +398,10 @@ public class MainActivity extends AppCompatActivity protected void onRestoreInstanceState(Bundle savedInstanceState) { selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); - drawer.setSelection(drawer.getDrawerItem(selectedLabel)); - + IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel); + if (selectedItem != null) { + drawer.setSelection(selectedItem); + } super.onRestoreInstanceState(savedInstanceState); } From f77bbe1a4363c6ef86a2009ba3a8dcc51c76bb74 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:35:26 +0200 Subject: [PATCH 099/110] Show related messages (parents, replies) --- .../apps/abit/MessageDetailFragment.java | 75 ++++++++++++++++++- .../dissem/apps/abit/service/Singleton.java | 14 ++++ app/src/main/res/drawable/border_bottom.xml | 10 +++ .../res/layout/fragment_message_detail.xml | 18 ++++- .../res/layout/item_message_minimized.xml | 64 ++++++++++++++++ app/src/main/res/layout/message_row.xml | 26 +++---- 6 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/border_bottom.xml create mode 100644 app/src/main/res/layout/item_message_minimized.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 857a195..0552cb6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -18,8 +18,10 @@ package ch.dissem.apps.abit; import android.content.Context; import android.os.Bundle; +import android.support.annotation.IdRes; import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.util.Linkify; import android.view.LayoutInflater; @@ -47,12 +49,14 @@ import ch.dissem.apps.abit.util.Drawables; import ch.dissem.apps.abit.util.Labels; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; +import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; /** @@ -143,16 +147,30 @@ public class MessageDetailFragment extends Fragment { removed = true; } } + MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext()); if (removed) { if (getActivity() instanceof ActionBarListener) { ((ActionBarListener) getActivity()).updateUnread(); } - Singleton.getMessageRepository(inflater.getContext()).save(item); + messageRepo.save(item); } + List<Plaintext> parents = new ArrayList<>(item.getParents().size()); + for (InventoryVector parentIV : item.getParents()) { + parents.add(messageRepo.getMessage(parentIV)); + } + showRelatedMessages(rootView, R.id.parents, parents); + showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item)); } return rootView; } + private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) { + RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.parents); + RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages); + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.message, menu); @@ -211,6 +229,61 @@ public class MessageDetailFragment extends Fragment { return false; } + private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> { + private final List<Plaintext> messages; + private final Context ctx; + + private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) { + this.messages = messages; + this.ctx = ctx; + } + + @Override + public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false); + + // Return a new holder instance + return new RelatedMessageAdapter.ViewHolder(contactView); + } + + // Involves populating data into the item through holder + @Override + public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) { + // Get the data model based on position + Plaintext message = messages.get(position); + + viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom())); + viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus())); + viewHolder.sender.setText(message.getFrom().toString()); + viewHolder.extract.setText(normalizeWhitespaces(message.getText())); + } + + // Returns the total count of items in the list + @Override + public int getItemCount() { + return messages.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final ImageView avatar; + private final ImageView status; + private final TextView sender; + private final TextView extract; + + ViewHolder(View itemView) { + super(itemView); + avatar = (ImageView) itemView.findViewById(R.id.avatar); + status = (ImageView) itemView.findViewById(R.id.status); + sender = (TextView) itemView.findViewById(R.id.sender); + extract = (TextView) itemView.findViewById(R.id.text); + } + } + } + private static class LabelAdapter extends RecyclerView.Adapter<LabelAdapter.ViewHolder> { diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 1bceb0b..e01f099 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -42,6 +42,7 @@ import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.ConversationService; import ch.dissem.bitmessage.utils.TTL; import static ch.dissem.bitmessage.utils.UnixTime.DAY; @@ -51,6 +52,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; */ public class Singleton { private static BitmessageContext bitmessageContext; + private static ConversationService conversationService; private static MessageListener messageListener; private static BitmessageAddress identity; private static AndroidProofOfWorkRepository powRepo; @@ -160,4 +162,16 @@ public class Singleton { throw new IllegalArgumentException("Identity expected, but no private key available"); Singleton.identity = identity; } + + public static ConversationService getConversationService(Context ctx) { + if (conversationService == null) { + final BitmessageContext bmc = getBitmessageContext(ctx); + synchronized (Singleton.class) { + if (conversationService == null) { + conversationService = new ConversationService(bmc.messages()); + } + } + } + return conversationService; + } } diff --git a/app/src/main/res/drawable/border_bottom.xml b/app/src/main/res/drawable/border_bottom.xml new file mode 100644 index 0000000..ab52873 --- /dev/null +++ b/app/src/main/res/drawable/border_bottom.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > + <item android:top="1dp" android:bottom="1dp"> + <shape + android:shape="rectangle"> + <stroke android:width="1dp" android:color="#FFDDDDDD" /> + <solid android:color="#00000000" /> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 1c63f9b..e698bf5 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -78,12 +78,20 @@ android:paddingRight="8dp" tools:text="Recipient" /> + <android.support.v7.widget.RecyclerView + android:id="@+id/parents" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/avatar" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> + <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentStart="true" - android:layout_below="@+id/avatar" + android:layout_below="@+id/parents" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="32dp" @@ -98,5 +106,13 @@ android:layout_below="@+id/text" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/responses" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/text" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> </RelativeLayout> </ScrollView> diff --git a/app/src/main/res/layout/item_message_minimized.xml b/app/src/main/res/layout/item_message_minimized.xml new file mode 100644 index 0000000..f556959 --- /dev/null +++ b/app/src/main/res/layout/item_message_minimized.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/border_bottom"> + + <ImageView + android:id="@+id/avatar" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorPrimaryDark" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/sender" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignTop="@+id/avatar" + android:layout_marginTop="-5dp" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Sender" /> + + <TextView + android:id="@+id/text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@+id/sender" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Text" /> + + <ImageView + android:id="@+id/status" + android:layout_width="24dp" + android:layout_height="wrap_content" + android:layout_alignBottom="@id/avatar" + android:layout_alignEnd="@+id/avatar" + android:layout_marginBottom="-8dp" + android:layout_marginEnd="-8dp" + android:tint="@color/colorAccent" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_notification_proof_of_work" /> + +</RelativeLayout> diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index f23d5c1..9c9f88a 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright 2015 Christian Basler ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,7 @@ ~ limitations under the License. --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -31,11 +29,10 @@ android:foreground="?attr/selectableItemBackground" tools:ignore="UselessParent"> - <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground"> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground"> <ImageView android:id="@+id/avatar" @@ -45,7 +42,7 @@ android:layout_alignParentTop="true" android:layout_margin="16dp" android:src="@color/colorPrimaryDark" - tools:ignore="ContentDescription"/> + tools:ignore="ContentDescription" /> <TextView android:id="@+id/sender" @@ -63,8 +60,7 @@ android:paddingTop="0dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textStyle="bold" - tools:text="Sender" - /> + tools:text="Sender" /> <TextView android:id="@+id/subject" @@ -78,7 +74,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceSmall" - tools:text="Subject"/> + tools:text="Subject" /> <TextView android:id="@+id/text" @@ -94,7 +90,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceSmall" - tools:text="Text"/> + tools:text="Text" /> <ImageView android:id="@+id/status" @@ -106,7 +102,7 @@ android:layout_marginEnd="-8dp" android:tint="@color/colorAccent" tools:ignore="ContentDescription" - tools:src="@drawable/ic_notification_proof_of_work"/> + tools:src="@drawable/ic_notification_proof_of_work" /> </RelativeLayout> From 55746743c43b40be83b45ae093554a48c6488b9c Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:36:23 +0200 Subject: [PATCH 100/110] Updated dependencies, enabled multidex --- app/build.gradle | 10 ++++++---- app/src/main/AndroidManifest.xml | 1 + build.gradle | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e5ec049..cdd925d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ android { versionCode 11 versionName "1.0-beta11" jackOptions.enabled = false + multiDexEnabled true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 @@ -45,6 +46,7 @@ dependencies { compile "com.android.support:appcompat-v7:$supportVersion" compile "com.android.support:support-v4:$supportVersion" compile "com.android.support:design:$supportVersion" + compile "com.android.support:multidex:1.0.1" compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -55,10 +57,10 @@ dependencies { compile 'org.slf4j:slf4j-android:1.7.25' compile 'com.mikepenz:materialize:1.0.1@aar' - compile('com.mikepenz:materialdrawer:5.8.2@aar') { + compile('com.mikepenz:materialdrawer:5.9.0@aar') { transitive = true } - compile('com.mikepenz:aboutlibraries:5.9.4@aar') { + compile('com.mikepenz:aboutlibraries:5.9.5@aar') { transitive = true } compile "com.mikepenz:iconics-core:2.8.2@aar" @@ -68,7 +70,7 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' - compile 'io.github.yavski:fab-speed-dial:1.0.7' + compile 'io.github.yavski:fab-speed-dial:1.0.6' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true @@ -77,7 +79,7 @@ dependencies { compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + testCompile 'org.mockito:mockito-core:2.7.22' } idea.module { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a7a2f74..d46c295 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" + android:name="android.support.multidex.MultiDexApplication" tools:replace="android:allowBackup"> <activity android:name=".MainActivity" diff --git a/build.gradle b/build.gradle index c37a44a..ceae43c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' // NOTE: Do not place your application dependencies here; they belong From 26fca259bcf5afce657fee07d07a0d58191d42b7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:38:11 +0200 Subject: [PATCH 101/110] Alternative key exchange by providing the public key in the URI --- .../apps/abit/AddressDetailFragment.java | 16 +++++ .../apps/abit/CreateAddressActivity.java | 58 +++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 40b2410..95ce443 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -26,6 +26,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; +import android.util.Base64; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -48,13 +49,18 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.wif.WifExporter; import static android.graphics.Color.BLACK; import static android.graphics.Color.WHITE; +import static android.util.Base64.URL_SAFE; /** @@ -269,6 +275,16 @@ public class AddressDetailFragment extends Fragment { if (address.getAlias() != null) { link.append("?label=").append(address.getAlias()); } + if (address.getPubkey() != null) { + link.append(address.getAlias() == null ? '?' : '&'); + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); + try { + address.getPubkey().writeUnencrypted(pubkey); + } catch (IOException e) { + throw new ApplicationException(e); + } + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + } BitMatrix result; try { result = new MultiFormatWriter().encode(link.toString(), diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index cf5d51f..27d344d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -20,17 +20,32 @@ import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.util.Base64; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Switch; import android.widget.TextView; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.payload.V2Pubkey; +import ch.dissem.bitmessage.entity.payload.V3Pubkey; +import ch.dissem.bitmessage.entity.payload.V4Pubkey; + +import static android.util.Base64.URL_SAFE; public class CreateAddressActivity extends AppCompatActivity { + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$"); + private byte[] pubkeyBytes; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -48,12 +63,19 @@ public class CreateAddressActivity extends AppCompatActivity { String addressText = getAddress(uri); String[] parameters = getParameters(uri); for (String parameter : parameters) { - String name = parameter.substring(0, 6).toLowerCase(); - if (name.startsWith("label")) { - label.setText(parameter.substring(parameter.indexOf('=') + 1).trim()); - } else if (name.startsWith("action")) { - parameter = parameter.toLowerCase(); - subscribe.setChecked(parameter.contains("subscribe")); + Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); + String key = matcher.group(1).toLowerCase(); + String value = matcher.group(2); + switch (key) { + case "label": + label.setText(value.trim()); + break; + case "action": + subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); + break; + case "pubkey": + pubkeyBytes = Base64.decode(value, URL_SAFE); + break; } } @@ -83,6 +105,30 @@ public class CreateAddressActivity extends AppCompatActivity { if (subscribe.isChecked()) { bmc.addSubscribtion(bmAddress); } + if (pubkeyBytes != null) { + try { + final Pubkey pubkey; + InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes); + long stream = bmAddress.getStream(); + switch ((int) bmAddress.getVersion()) { + case 2: + pubkey = V2Pubkey.read(pubkeyStream, stream); + break; + case 3: + pubkey = V3Pubkey.read(pubkeyStream, stream); + break; + case 4: + pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream)); + break; + default: + pubkey = null; + } + if (pubkey != null) { + bmAddress.setPubkey(pubkey); + } + } catch (Exception ignore) { + } + } setResult(Activity.RESULT_OK); finish(); From 911dfa7a27b83a9f8eb5f927f66a817a7f5faa41 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 18 Apr 2017 16:38:45 +0200 Subject: [PATCH 102/110] Show QR code when clicking on profile image --- .../apps/abit/AddressDetailFragment.java | 60 +------------------ .../ch/dissem/apps/abit/MainActivity.java | 52 ++++++++++++++++ .../ch/dissem/apps/abit/util/Drawables.java | 59 ++++++++++++++++++ 3 files changed, 112 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 95ce443..704726d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -20,13 +20,11 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.Bitmap; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; -import android.util.Base64; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -39,29 +37,14 @@ import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.WriterException; -import com.google.zxing.common.BitMatrix; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.wif.WifExporter; -import static android.graphics.Color.BLACK; -import static android.graphics.Color.WHITE; -import static android.util.Base64.URL_SAFE; - /** * A fragment representing a single Message detail screen. @@ -70,7 +53,6 @@ import static android.util.Base64.URL_SAFE; * on handsets. */ public class AddressDetailFragment extends Fragment { - private static final Logger LOG = LoggerFactory.getLogger(AddressDetailFragment.class); /** * The fragment argument representing the item ID that this fragment * represents. @@ -78,8 +60,6 @@ public class AddressDetailFragment extends Fragment { public static final String ARG_ITEM = "item"; public static final String EXPORT_POSTFIX = ".keys.dat"; - private static final int QR_CODE_SIZE = 350; - /** * The content this fragment is presenting. */ @@ -263,50 +243,12 @@ public class AddressDetailFragment extends Fragment { // QR code ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); - qrCode.setImageBitmap(qrCode(item)); + qrCode.setImageBitmap(Drawables.qrCode(item)); } return rootView; } - Bitmap qrCode(BitmessageAddress address) { - StringBuilder link = new StringBuilder("bitmessage:"); - link.append(address.getAddress()); - if (address.getAlias() != null) { - link.append("?label=").append(address.getAlias()); - } - if (address.getPubkey() != null) { - link.append(address.getAlias() == null ? '?' : '&'); - ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); - try { - address.getPubkey().writeUnencrypted(pubkey); - } catch (IOException e) { - throw new ApplicationException(e); - } - link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); - } - BitMatrix result; - try { - result = new MultiFormatWriter().encode(link.toString(), - BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); - } catch (WriterException e) { - LOG.error(e.getMessage(), e); - return null; - } - int w = result.getWidth(); - int h = result.getHeight(); - int[] pixels = new int[w * h]; - for (int y = 0; y < h; y++) { - int offset = y * w; - for (int x = 0; x < w; x++) { - pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; - } - } - Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h); - return bitmap; - } - @Override public void onPause() { if (item != null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index e7aaa9a..d744b99 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -16,16 +16,23 @@ package ch.dissem.apps.abit; +import android.app.Dialog; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.view.Display; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.widget.CompoundButton; +import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.Toast; @@ -61,6 +68,7 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Drawables; import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; @@ -230,6 +238,50 @@ public class MainActivity extends AppCompatActivity .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) + .withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() { + @Override + public boolean onProfileImageClick(View view, IProfile profile, boolean current) { + if (current) { + // Show QR code in modal dialog + final Dialog dialog = new Dialog(MainActivity.this); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + ImageView imageView = new ImageView(MainActivity.this); + imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this))); + imageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + dialog.addContentView(imageView, new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + Window window = dialog.getWindow(); + if (window != null) { + Display display = window.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + int dim = size.x < size.y ? size.x : size.y; + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(window.getAttributes()); + lp.width = dim; + lp.height = dim; + + window.setAttributes(lp); + } + dialog.show(); + return true; + } + return false; + } + + @Override + public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) { + return false; + } + }) .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { @Override public boolean onProfileChanged(View view, IProfile profile, boolean current) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index a35281a..d577515 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -19,19 +19,40 @@ package ch.dissem.apps.abit.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.util.Base64; import android.view.Menu; import android.view.MenuItem; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; import com.mikepenz.iconics.IconicsDrawable; import com.mikepenz.iconics.typeface.IIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; +import static android.util.Base64.URL_SAFE; /** * Some helper methods to work with drawables. */ public class Drawables { + private static final Logger LOG = LoggerFactory.getLogger(Drawables.class); + + private static final int QR_CODE_SIZE = 350; + public static MenuItem addIcon(Context ctx, Menu menu, int menuItem, IIcon icon) { MenuItem item = menu.findItem(menuItem); item.setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()); @@ -49,4 +70,42 @@ public class Drawables { identicon.draw(canvas); return bitmap; } + + public static Bitmap qrCode(BitmessageAddress address) { + StringBuilder link = new StringBuilder("bitmessage:"); + link.append(address.getAddress()); + if (address.getAlias() != null) { + link.append("?label=").append(address.getAlias()); + } + if (address.getPubkey() != null) { + link.append(address.getAlias() == null ? '?' : '&'); + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); + try { + address.getPubkey().writeUnencrypted(pubkey); + } catch (IOException e) { + throw new ApplicationException(e); + } + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + } + BitMatrix result; + try { + result = new MultiFormatWriter().encode(link.toString(), + BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); + } catch (WriterException e) { + LOG.error(e.getMessage(), e); + return null; + } + int w = result.getWidth(); + int h = result.getHeight(); + int[] pixels = new int[w * h]; + for (int y = 0; y < h; y++) { + int offset = y * w; + for (int x = 0; x < w; x++) { + pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; + } + } + Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h); + return bitmap; + } } From a8dada6c89c0b1a049986d82310f2d17cf24d6d8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 19 Apr 2017 00:20:51 +0200 Subject: [PATCH 103/110] Alternative key exchange by providing the public key in the URI (bugfixes) --- .../apps/abit/CreateAddressActivity.java | 26 ++++++++++--------- .../ch/dissem/apps/abit/util/Drawables.java | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index 27d344d..6ed8060 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -64,18 +64,20 @@ public class CreateAddressActivity extends AppCompatActivity { String[] parameters = getParameters(uri); for (String parameter : parameters) { Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); - String key = matcher.group(1).toLowerCase(); - String value = matcher.group(2); - switch (key) { - case "label": - label.setText(value.trim()); - break; - case "action": - subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); - break; - case "pubkey": - pubkeyBytes = Base64.decode(value, URL_SAFE); - break; + if (matcher.find()) { + String key = matcher.group(1).toLowerCase(); + String value = matcher.group(2); + switch (key) { + case "label": + label.setText(value.trim()); + break; + case "action": + subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); + break; + case "pubkey": + pubkeyBytes = Base64.decode(value, URL_SAFE); + break; + } } } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index d577515..dfda9c8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -43,6 +43,7 @@ import ch.dissem.bitmessage.exception.ApplicationException; import static android.graphics.Color.BLACK; import static android.graphics.Color.WHITE; +import static android.util.Base64.NO_WRAP; import static android.util.Base64.URL_SAFE; /** @@ -85,7 +86,7 @@ public class Drawables { } catch (IOException e) { throw new ApplicationException(e); } - link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE | NO_WRAP)); } BitMatrix result; try { From 572ecf1577d2f303349c642e206473d6af2d7510 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Apr 2017 07:23:21 +0200 Subject: [PATCH 104/110] Some minor fixes for working with extended encoding --- .../apps/abit/ComposeMessageActivity.java | 2 +- .../apps/abit/ComposeMessageFragment.java | 17 +++++++++++++++++ .../dissem/apps/abit/MessageDetailFragment.java | 7 +++++-- .../repository/AndroidMessageRepository.java | 4 ++-- .../main/res/layout/fragment_message_detail.xml | 5 +++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 2df2a68..1cde1b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -87,8 +87,8 @@ public class ComposeMessageActivity extends AppCompatActivity { // so features like threading can be supported if (item.getEncoding() == EXTENDED) { replyIntent.putExtra(EXTRA_ENCODING, EXTENDED); - replyIntent.putExtra(EXTRA_PARENT, item); } + replyIntent.putExtra(EXTRA_PARENT, item); String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) .equalsIgnoreCase("RE:")) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 4413933..142e8f8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -79,6 +80,11 @@ public class ComposeMessageFragment extends Fragment { if (getArguments() != null) { if (getArguments().containsKey(EXTRA_IDENTITY)) { identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY); + if (getActivity() != null) { + if (identity == null || identity.getPrivateKey() == null) { + identity = Singleton.getIdentity(getActivity()); + } + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -156,6 +162,14 @@ public class ComposeMessageFragment extends Fragment { return rootView; } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (identity == null || identity.getPrivateKey() == null) { + identity = Singleton.getIdentity(context); + } + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.compose, menu); @@ -170,6 +184,9 @@ public class ComposeMessageFragment extends Fragment { return true; case R.id.select_encoding: SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment(); + Bundle args = new Bundle(); + args.putSerializable(EXTRA_ENCODING, encoding); + encodingDialog.setArguments(args); encodingDialog.setTargetFragment(this, 0); encodingDialog.show(getFragmentManager(), "select encoding dialog"); return true; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 0552cb6..90137ed 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -156,7 +156,10 @@ public class MessageDetailFragment extends Fragment { } List<Plaintext> parents = new ArrayList<>(item.getParents().size()); for (InventoryVector parentIV : item.getParents()) { - parents.add(messageRepo.getMessage(parentIV)); + Plaintext parent = messageRepo.getMessage(parentIV); + if (parent != null) { + parents.add(parent); + } } showRelatedMessages(rootView, R.id.parents, parents); showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item)); @@ -165,7 +168,7 @@ public class MessageDetailFragment extends Fragment { } private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) { - RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.parents); + RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id); RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index b38cb1d..f305ffb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -201,9 +201,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // There are no parents to save yet (they are saved in the extended data, that's enough for now) return; } - db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(message.getInitialHash()).toString()}); - byte[] childIV = message.getInventoryVector().getHash(); + db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()}); + // save new parents int order = 0; for (InventoryVector parentIV : message.getParents()) { diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index e698bf5..73bb3d9 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -105,13 +105,14 @@ android:layout_height="wrap_content" android:layout_below="@+id/text" android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" /> + android:layout_marginRight="16dp" + android:layout_marginBottom="16dp"/> <android.support.v7.widget.RecyclerView android:id="@+id/responses" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_below="@+id/text" + android:layout_below="@+id/labels" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" /> </RelativeLayout> From 30c5bf6b904f0f4210a31cc1cd61a888eb7fcedd Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Apr 2017 23:24:28 +0200 Subject: [PATCH 105/110] Open related messages on click --- .../apps/abit/MessageDetailFragment.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 90137ed..ab851a9 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -17,6 +17,7 @@ package ch.dissem.apps.abit; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.v4.app.Fragment; @@ -263,6 +264,7 @@ public class MessageDetailFragment extends Fragment { viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus())); viewHolder.sender.setText(message.getFrom().toString()); viewHolder.extract.setText(normalizeWhitespaces(message.getText())); + viewHolder.item = message; } // Returns the total count of items in the list @@ -271,18 +273,33 @@ public class MessageDetailFragment extends Fragment { return messages.size(); } - static class ViewHolder extends RecyclerView.ViewHolder { + class ViewHolder extends RecyclerView.ViewHolder { private final ImageView avatar; private final ImageView status; private final TextView sender; private final TextView extract; + private Plaintext item; - ViewHolder(View itemView) { + ViewHolder(final View itemView) { super(itemView); avatar = (ImageView) itemView.findViewById(R.id.avatar); status = (ImageView) itemView.findViewById(R.id.status); sender = (TextView) itemView.findViewById(R.id.sender); extract = (TextView) itemView.findViewById(R.id.text); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (ctx instanceof MainActivity) { + ((MainActivity) ctx).onItemSelected(item); + } else { + Intent detailIntent; + detailIntent = new Intent(ctx, MessageDetailActivity.class); + detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); + ctx.startActivity(detailIntent); + } + } + }); + } } } From 91cc90ec04ef0d756beb4fbe40ffe81345238ea3 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 21 Apr 2017 07:23:39 +0200 Subject: [PATCH 106/110] Fix unread badge for archive --- .../java/ch/dissem/apps/abit/MainActivity.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index d744b99..f7d2c07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -78,6 +78,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import static android.widget.Toast.LENGTH_LONG; import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; +import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -321,7 +322,7 @@ public class MainActivity extends AppCompatActivity final ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); drawerItems.add(new PrimaryDrawerItem() .withName(R.string.archive) - .withTag(AndroidMessageRepository.LABEL_ARCHIVE) + .withTag(LABEL_ARCHIVE) .withIcon(CommunityMaterial.Icon.cmd_archive) ); drawerItems.add(new DividerDrawerItem()); @@ -533,13 +534,15 @@ public class MainActivity extends AppCompatActivity for (IDrawerItem item : drawer.getDrawerItems()) { if (item.getTag() instanceof Label) { Label label = (Label) item.getTag(); - int unread = bmc.messages().countUnread(label); - if (unread > 0) { - ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); - } else { - ((PrimaryDrawerItem) item).withBadge((String) null); + if (label != LABEL_ARCHIVE) { + int unread = bmc.messages().countUnread(label); + if (unread > 0) { + ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); + } else { + ((PrimaryDrawerItem) item).withBadge((String) null); + } + drawer.updateItem(item); } - drawer.updateItem(item); } } } From d3f1e6abd27ad63c3f7166fa2918b5d7303bf8f8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 21 Apr 2017 15:20:50 +0200 Subject: [PATCH 107/110] Minor layout improvements --- .../main/res/layout/item_message_minimized.xml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/item_message_minimized.xml b/app/src/main/res/layout/item_message_minimized.xml index f556959..0b2e209 100644 --- a/app/src/main/res/layout/item_message_minimized.xml +++ b/app/src/main/res/layout/item_message_minimized.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:padding="8dp" android:background="@drawable/border_bottom"> <ImageView @@ -11,7 +12,7 @@ android:layout_height="24dp" android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:layout_margin="16dp" + android:layout_marginTop="5dp" android:src="@color/colorPrimaryDark" tools:ignore="ContentDescription" /> @@ -20,16 +21,13 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_alignParentEnd="true" - android:layout_alignTop="@+id/avatar" - android:layout_marginTop="-5dp" + android:layout_alignParentTop="true" android:layout_toEndOf="@+id/avatar" android:ellipsize="end" android:lines="1" android:paddingBottom="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="0dp" - android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:textStyle="bold" tools:text="Sender" /> @@ -44,8 +42,8 @@ android:gravity="center_vertical" android:lines="1" android:paddingBottom="8dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:textAppearance="?android:attr/textAppearanceSmall" tools:text="Text" /> From d704a40b66c58d0d63b584ba0c5f9115982eb383 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 22 Apr 2017 07:42:20 +0200 Subject: [PATCH 108/110] Regularly call cleanup() --- app/build.gradle | 6 +++--- .../abit/dialog/SelectEncodingDialogFragment.java | 3 +-- .../apps/abit/service/BitmessageService.java | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cdd925d..6284311 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,7 @@ android { //ext.jabitVersion = '2.0.4' ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' -ext.supportVersion = '25.2.0' +ext.supportVersion = '25.3.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) @@ -63,9 +63,9 @@ dependencies { compile('com.mikepenz:aboutlibraries:5.9.5@aar') { transitive = true } - compile "com.mikepenz:iconics-core:2.8.2@aar" + compile "com.mikepenz:iconics-core:2.8.3@aar" compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' - compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' + compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java index 3b8b917..899ca0f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java @@ -46,8 +46,7 @@ public class SelectEncodingDialogFragment extends AppCompatDialogFragment { @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle - savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) { encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 5c8a528..ef64b61 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit.service; import android.app.Service; import android.content.Intent; +import android.os.Handler; import android.os.IBinder; import android.support.annotation.Nullable; @@ -38,6 +39,17 @@ public class BitmessageService extends Service { private NetworkNotification notification = null; + private final Handler cleanupHandler = new Handler(); + private final Runnable cleanupTask = new Runnable() { + @Override + public void run() { + bmc.cleanup(); + if (isRunning()) { + cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L); + } + } + }; + public static boolean isRunning() { return running && bmc.isRunning(); } @@ -61,6 +73,7 @@ public class BitmessageService extends Service { bmc.startup(); } notification.show(); + cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L); } return Service.START_STICKY; } @@ -72,6 +85,8 @@ public class BitmessageService extends Service { } running = false; notification.showShutdown(); + cleanupHandler.removeCallbacks(cleanupTask); + bmc.cleanup(); stopSelf(); } From d36ffe193973d4f10a28365d1a799f9a7b7d40f9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 25 Apr 2017 08:07:33 +0200 Subject: [PATCH 109/110] Fixed layout - apparently ConstraintLayout doesn't work properly for dialogs yet, at least in this case --- .../layout/dialog_select_message_encoding.xml | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/layout/dialog_select_message_encoding.xml b/app/src/main/res/layout/dialog_select_message_encoding.xml index 4a1991f..02dcab0 100644 --- a/app/src/main/res/layout/dialog_select_message_encoding.xml +++ b/app/src/main/res/layout/dialog_select_message_encoding.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> -<android.support.constraint.ConstraintLayout +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -31,10 +31,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:text="@string/select_encoding_warning" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:layout_constraintLeft_creator="1" - tools:layout_constraintTop_creator="1"/> + android:layout_alignParentTop="true" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true"/> <RadioGroup @@ -43,9 +42,9 @@ android:layout_height="wrap_content" android:paddingBottom="24dp" android:paddingTop="24dp" - app:layout_constraintLeft_toLeftOf="@+id/description" - app:layout_constraintRight_toRightOf="@+id/description" - app:layout_constraintTop_toBottomOf="@+id/description"> + android:layout_alignStart="@+id/description" + android:layout_alignEnd="@+id/description" + android:layout_below="@+id/description"> <RadioButton android:id="@+id/simple" @@ -72,10 +71,8 @@ android:text="@string/ok" android:textColor="@color/colorAccent" app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/radioGroup" - app:layout_constraintTop_toBottomOf="@+id/radioGroup" - tools:layout_constraintRight_creator="1" - tools:layout_constraintTop_creator="1"/> + android:layout_alignRight="@+id/radioGroup" + android:layout_below="@+id/radioGroup"/> <Button android:id="@+id/dismiss" @@ -84,7 +81,7 @@ android:layout_height="wrap_content" android:text="@string/cancel" android:textColor="@color/colorAccent" - app:layout_constraintRight_toLeftOf="@+id/ok" - app:layout_constraintTop_toBottomOf="@+id/radioGroup"/> + android:layout_toLeftOf="@+id/ok" + android:layout_below="@+id/radioGroup"/> -</android.support.constraint.ConstraintLayout> +</RelativeLayout> From 66108c861800ab11359a645ee1132d52a771a6fe Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 25 Apr 2017 21:29:11 +0200 Subject: [PATCH 110/110] Version 1.0-beta12 bump --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6284311..76984f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "ch.dissem.apps." + appName.toLowerCase() minSdkVersion 19 targetSdkVersion 25 - versionCode 11 - versionName "1.0-beta11" + versionCode 12 + versionName "1.0-beta12" jackOptions.enabled = false multiDexEnabled true }