diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19861c3..f0fbceb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,104 +1,133 @@ + 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 65acd36..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,13 +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; /** @@ -44,7 +44,6 @@ public abstract class AbstractItemListFragment extends ListFragment { public void onItemSelected(Object plaintext) { } }; - protected BitmessageContext bmc; /** * The fragment's current callback object, which is notified of list item * clicks. @@ -54,16 +53,10 @@ 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); - @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); @@ -76,15 +69,26 @@ public abstract class AbstractItemListFragment extends ListFragment { } @Override - public void onAttach(Activity activity) { - super.onAttach(activity); + 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(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 @@ -118,11 +122,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/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 51de702..b8a359f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListActivity.java @@ -1,23 +1,24 @@ 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; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; - -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 android.widget.CompoundButton; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; @@ -29,18 +30,34 @@ 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; import java.io.Serializable; +import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Timer; -import java.util.TimerTask; + +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.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.synchronization.StubProvider.AUTHORITY; /** @@ -66,6 +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 * 60; // seconds private static final int ADD_IDENTITY = 1; /** @@ -74,16 +92,36 @@ public class MessageListActivity extends AppCompatActivity */ private boolean twoPane; - private AccountHeader accountHeader; - private BitmessageContext bmc; + 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.service = new Messenger(service); + MessageListActivity.bound = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + service = null; + bound = false; + } + }; + private Label selectedLabel; - private Menu menu; + + 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); @@ -99,6 +137,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); @@ -109,16 +151,22 @@ public class MessageListActivity extends AppCompatActivity if (getIntent().hasExtra(EXTRA_SHOW_MESSAGE)) { onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE)); } + + createSyncAccount(); } - @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 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); } } @@ -138,7 +186,7 @@ public class MessageListActivity extends AppCompatActivity private void createDrawer(Toolbar toolbar) { final ArrayList 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)) @@ -161,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) @@ -169,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 @@ -186,9 +230,10 @@ public class MessageListActivity extends AppCompatActivity } }) .build(); + incomingHandler.updateAccountHeader(accountHeader); ArrayList 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: @@ -227,26 +272,43 @@ 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) + .withChecked(BitmessageService.isRunning()) + .withOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, boolean isChecked) { + 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); + } + } + } + } + }) ) .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(); - if (!(getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof MessageListFragment)) { - MessageListFragment listFragment = new MessageListFragment(); - 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; @@ -262,6 +324,12 @@ 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; } } return false; @@ -271,34 +339,14 @@ 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(); - new NetworkNotification(this).show(); - updateMenu(); - return true; - case R.id.sync_enabled: - bmc.shutdown(); - updateMenu(); - return true; - default: - return super.onOptionsItemSelected(item); + 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); } } @@ -345,6 +393,7 @@ public class MessageListActivity extends AppCompatActivity @Override public void updateTitle(CharSequence title) { + //noinspection ConstantConditions getSupportActionBar().setTitle(title); } @@ -352,4 +401,58 @@ 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 static class IncomingHandler extends Handler { + private WeakReference 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/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index f251d8a..802b901 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -1,15 +1,24 @@ 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; import android.support.design.widget.FloatingActionButton; -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.TextView; + 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.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; @@ -52,11 +61,14 @@ public class MessageListFragment extends AbstractItemListFragment { @Override public void updateList(Label label) { currentLabel = label; + + if (!isVisible()) return; + setListAdapter(new ArrayAdapter<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) { @@ -81,7 +93,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); @@ -117,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..17ffe41 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.service.BitmessageService; 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); + + 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/AbstractNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/AbstractNotification.java index 60bb86d..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 @@ -10,11 +10,11 @@ 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) { - 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 new file mode 100644 index 0000000..c64185e --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/ErrorNotification.java @@ -0,0 +1,42 @@ +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; + +/** + * 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(@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(@StringRes 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/java/ch/dissem/apps/abit/notification/NetworkNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NetworkNotification.java index b51cc47..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 @@ -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; @@ -11,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; @@ -19,18 +19,26 @@ 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; - public NetworkNotification(Context ctx) { + public NetworkNotification(Context ctx, BitmessageContext bmc) { super(ctx); - bmc = Singleton.getBitmessageContext(ctx); + this.bmc = bmc; 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); } + @Override + 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/notification/NewMessageNotification.java b/app/src/main/java/ch/dissem/apps/abit/notification/NewMessageNotification.java index ee10bf1..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; @@ -21,6 +20,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 +76,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/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/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 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 new file mode 100644 index 0000000..e52eb95 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -0,0 +1,164 @@ +package ch.dissem.apps.abit.service; + +import android.app.Service; +import android.content.Intent; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.lang.ref.WeakReference; + +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; + +/** + * 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_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; + 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 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) { + bmc = Singleton.getBitmessageContext(this); + notification = new NetworkNotification(this, bmc); + messenger = new Messenger(new IncomingHandler(this)); + } + } + } + + @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 messenger.getBinder(); + } + + 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) { + 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_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: + // 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()); + bmc.startup(); + notification.show(); + break; + case MSG_STOP_NODE: + bmc.shutdown(); + running = false; + service.get().stopForeground(false); + service.get().stopSelf(); + break; + default: + super.handleMessage(msg); + } + } + } +} \ 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 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 220dc36..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 @@ -9,23 +9,27 @@ 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.security.sc.SpongySecurity; /** * Provides singleton objects across the application. */ public class Singleton { + public static final Object lock = new Object(); private static BitmessageContext bitmessageContext; private static MessageListener messageListener; public static BitmessageContext getBitmessageContext(Context context) { if (bitmessageContext == null) { - synchronized (Singleton.class) { + synchronized (lock) { if (bitmessageContext == null) { 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)) @@ -50,4 +54,12 @@ public class Singleton { } return messageListener; } + + public static MessageRepository getMessageRepository(Context ctx) { + return getBitmessageContext(ctx).messages(); + } + + public static AddressRepository getAddressRepository(Context ctx) { + return getBitmessageContext(ctx).addresses(); + } } 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..6d4eec5 --- /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 = "ch.dissem.bitmessage"; + + // 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..f5b3d2d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/StubProvider.java @@ -0,0 +1,72 @@ +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 { + public static final String AUTHORITY = "ch.dissem.apps.abit.provider"; + + /* + * 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..7654833 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -0,0 +1,85 @@ +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.R; +import ch.dissem.apps.abit.notification.ErrorNotification; +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); + + private final BitmessageContext bmc; + + /** + * Set up the sync adapter + */ + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + 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()) { + LOG.info("Synchronization skipped, Abit is acting as a full node"); + return; + } + LOG.info("Synchronizing Bitmessage"); + + 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); + } 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("sync_timeout", "120")); + try { + LOG.info("Synchronization started"); + bmc.synchronize(InetAddress.getByName(trustedNode), port, timeoutInSeconds, 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); + } + } +} 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..b3abcaf --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncService.java @@ -0,0 +1,65 @@ +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; + +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 + * 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 + 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(this, true); + } + } + } + + /** + * 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/drawable-hdpi/ic_action_notification_sync.png b/app/src/main/res/drawable-hdpi/ic_action_notification_sync.png deleted file mode 100755 index d938029..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_notification_sync.png and /dev/null differ 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 c8bcece..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_action_notification_sync_disabled.png and /dev/null differ 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 f051d5e..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_notification_sync.png and /dev/null differ 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 d8e4526..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_notification_sync_disabled.png and /dev/null differ 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 7e1ffed..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_notification_sync.png and /dev/null differ 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 00e51af..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_action_notification_sync_disabled.png and /dev/null differ 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 4017dab..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync.png and /dev/null differ 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 549cf6d..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_action_notification_sync_disabled.png and /dev/null differ 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 bea0812..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync.png and /dev/null differ 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 d71a99a..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_action_notification_sync_disabled.png and /dev/null differ 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> 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/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 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 d296eca..7020bf7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -4,9 +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="disable_sync">Synchronisieren ausschalten</string> - <string name="enable_sync">Synchronisieren einschalten</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> @@ -33,8 +31,19 @@ <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="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_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 baf9d23..ed1cd40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,9 +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="disable_sync">Disable Sync</string> - <string name="enable_sync">Enable Sync</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> @@ -35,6 +33,17 @@ <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> + <string name="full_node">Full node</string> + <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> diff --git a/app/src/main/res/xml/authenticator.xml b/app/src/main/res/xml/authenticator.xml new file mode 100644 index 0000000..17609a0 --- /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="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/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..fabc86f --- /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.apps.abit.provider" + android:accountType="ch.dissem.bitmessage" + android:userVisible="true" + android:supportsUploading="true" + android:allowParallelSyncs="false" + android:isAlwaysSyncable="true"/> \ No newline at end of file