Merge branch 'release/1.0-beta12'

This commit is contained in:
Christian Basler 2017-04-25 21:29:27 +02:00
commit 4f36f36ab3
246 changed files with 11700 additions and 2196 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4

View File

@ -1,42 +1,85 @@
apply plugin: 'idea' apply plugin: 'idea'
apply plugin: 'com.android.application' 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";
}
//noinspection GroovyMissingReturnStatement
android { android {
compileSdkVersion 23 compileSdkVersion 25
buildToolsVersion "23.0.1" buildToolsVersion "25.0.2"
defaultConfig { defaultConfig {
applicationId "ch.dissem.apps.abit" applicationId "ch.dissem.apps." + appName.toLowerCase()
minSdkVersion 15 minSdkVersion 19
targetSdkVersion 23 targetSdkVersion 25
versionCode 1 versionCode 12
versionName "1.0" versionName "1.0-beta12"
jackOptions.enabled = false
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
} }
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
} }
} }
} }
//ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT'
ext.supportVersion = '25.3.1'
dependencies { dependencies {
compile fileTree(dir: 'libs', include: ['*.jar']) 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 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' compile "com.android.support:appcompat-v7:$supportVersion"
compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' compile "com.android.support:support-v4:$supportVersion"
compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT' compile "com.android.support:design:$supportVersion"
compile "com.android.support:multidex:1.0.1"
compile 'org.slf4j:slf4j-android:1.7.12' 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 "ch.dissem.jabit:jabit-wif:$jabitVersion"
compile('com.mikepenz:materialdrawer:3.1.0@aar') { compile 'org.slf4j:slf4j-android:1.7.25'
compile 'com.mikepenz:materialize:1.0.1@aar'
compile('com.mikepenz:materialdrawer:5.9.0@aar') {
transitive = true transitive = true
} }
compile 'com.mikepenz:iconics:1.6.2@aar' compile('com.mikepenz:aboutlibraries:5.9.5@aar') {
compile 'com.mikepenz:community-material-typeface:1.1.71@aar' transitive = true
}
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.9.32.1@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 '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'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.22'
} }
idea.module { idea.module {
@ -48,4 +91,4 @@ android {
lintOptions { lintOptions {
abortOnError false abortOnError false
} }
} }

View File

@ -1,22 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest
xmlns:tools="http://schemas.android.com/tools" package="ch.dissem.apps.abit"
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_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/> <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.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.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<application <application
android:allowBackup="true" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:name="android.support.multidex.MultiDexApplication"
tools:replace="android:allowBackup">
<activity <activity
android:name=".MessageListActivity" android:name=".MainActivity"
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
@ -24,30 +31,34 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".MessageDetailActivity" android:name=".MessageDetailActivity"
android:label="@string/title_message_detail" android:label="@string/title_message_detail"
android:parentActivityName=".MessageListActivity" android:parentActivityName=".MainActivity"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".MessageListActivity"/> android:value=".MainActivity"/>
</activity> </activity>
<activity <activity
android:name=".SubscriptionDetailActivity" android:name=".AddressDetailActivity"
android:label="@string/title_subscription_detail" android:label="@string/title_subscription_detail"
android:parentActivityName=".MessageListActivity" android:parentActivityName=".MainActivity"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".MessageListActivity"/> android:value=".MainActivity"/>
</activity> </activity>
<activity <activity
android:name=".ComposeMessageActivity" android:name=".dialog.FullNodeDialogActivity"
android:label="Compose" android:label="@string/full_node"
android:parentActivityName=".MessageListActivity"> android:theme="@style/Theme.AppCompat.Light.Dialog"/>
<activity
android:name=".ComposeMessageActivity"
android:label="@string/compose_message"
android:parentActivityName=".MainActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".MessageListActivity"/> android:value=".MainActivity"/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SENDTO"/> <action android:name="android.intent.action.SENDTO"/>
@ -74,20 +85,19 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="@string/settings" android:label="@string/settings"
android:parentActivityName=".MessageListActivity"> android:parentActivityName=".MainActivity">
<intent-filter> <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> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".OpenBitmessageLinkActivity" android:name=".CreateAddressActivity"
android:label="@string/title_activity_open_bitmessage_link" android:label="@string/title_activity_open_bitmessage_link"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
@ -99,6 +109,84 @@
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
</intent-filter> </intent-filter>
</activity> </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"
android:exported="false"/>
<service
android:name=".service.ProofOfWorkService"
android:exported="false"/>
<!-- Synchronization -->
<provider
android:name=".synchronization.StubProvider"
android:authorities="ch.dissem.apps.abit.provider"
android:exported="false"
android:syncable="true"/>
<service
android:name=".synchronization.AuthenticatorService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator"/>
</service>
<service
android:name=".synchronization.SyncService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
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">
<intent-filter>
<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> </application>
</manifest> </manifest>

View File

@ -8,6 +8,7 @@ CREATE TABLE Message (
sent INTEGER, sent INTEGER,
received INTEGER, received INTEGER,
status VARCHAR(20) NOT NULL, status VARCHAR(20) NOT NULL,
initial_hash BINARY(64) UNIQUE,
FOREIGN KEY (sender) REFERENCES Address (address), FOREIGN KEY (sender) REFERENCES Address (address),
FOREIGN KEY (recipient) REFERENCES Address (address) FOREIGN KEY (recipient) REFERENCES Address (address)

View File

@ -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);

View File

@ -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
);

View File

@ -0,0 +1 @@
ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0';

View File

@ -0,0 +1,2 @@
ALTER TABLE POW ADD COLUMN expiration_time BIGINT;
ALTER TABLE POW ADD COLUMN message_id BIGINT;

View File

@ -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;

View File

@ -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);

View File

@ -0,0 +1 @@
INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15);

View File

@ -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)
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -16,20 +16,18 @@
package ch.dissem.apps.abit; package ch.dissem.apps.abit;
import android.app.Activity; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
import android.view.View; import android.view.View;
import android.widget.ListView; import android.widget.ListView;
import ch.dissem.apps.abit.listeners.ListSelectionListener;
import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.listener.ListSelectionListener;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.valueobject.Label;
/** /**
* Created by chris on 07.09.15. * @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 * The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets. * activated item position. Only used on tablets.
@ -39,12 +37,13 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
* A dummy implementation of the {@link ListSelectionListener} interface that does * A dummy implementation of the {@link ListSelectionListener} interface that does
* nothing. Used only when this fragment is not attached to an activity. * nothing. Used only when this fragment is not attached to an activity.
*/ */
private static ListSelectionListener<Object> dummyCallbacks = new ListSelectionListener<Object>() { private static final ListSelectionListener<Object> dummyCallbacks =
@Override new ListSelectionListener<Object>() {
public void onItemSelected(Object plaintext) { @Override
} public void onItemSelected(Object item) {
}; // NO OP
protected BitmessageContext bmc; }
};
/** /**
* The fragment's current callback object, which is notified of list item * The fragment's current callback object, which is notified of list item
* clicks. * clicks.
@ -54,15 +53,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
* The current activated item position. Only used on tablets. * The current activated item position. Only used on tablets.
*/ */
private int activatedPosition = ListView.INVALID_POSITION; 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 @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState) {
@ -70,21 +61,34 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
// Restore the previously serialized activated item position. // Restore the previously serialized activated item position.
if (savedInstanceState != null if (savedInstanceState != null
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
} }
} }
@Override @Override
public void onAttach(Activity activity) { public void onResume() {
super.onAttach(activity); 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. // Activities containing this fragment must implement its callbacks.
if (!(activity instanceof ListSelectionListener)) { if (context instanceof ListSelectionListener) {
//noinspection unchecked
callbacks = (ListSelectionListener) context;
} else {
throw new IllegalStateException("Activity must implement fragment's callbacks."); throw new IllegalStateException("Activity must implement fragment's callbacks.");
} }
callbacks = (ListSelectionListener) activity;
} }
@Override @Override
@ -101,6 +105,7 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
// Notify the active callbacks interface (the activity, if the // Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected. // fragment is attached to one) that an item has been selected.
//noinspection unchecked
callbacks.onItemSelected((T) listView.getItemAtPosition(position)); callbacks.onItemSelected((T) listView.getItemAtPosition(position));
} }
@ -118,11 +123,15 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
* given the 'activated' state when touched. * given the 'activated' state when touched.
*/ */
public void setActivateOnItemClick(boolean activateOnItemClick) { public void setActivateOnItemClick(boolean activateOnItemClick) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically this.activateOnItemClick = activateOnItemClick;
// give items the 'activated' state when touched.
getListView().setChoiceMode(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_SINGLE
: ListView.CHOICE_MODE_NONE); : ListView.CHOICE_MODE_NONE);
}
} }
private void setActivatedPosition(int position) { private void setActivatedPosition(int position) {

View File

@ -16,34 +16,23 @@
package ch.dissem.apps.abit; package ch.dissem.apps.abit;
import android.content.Intent;
import android.os.Bundle; 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;
/** /**
* An activity representing a single Subscription detail screen. This * An activity representing a single Subscription detail screen. This
* activity is only used on handset devices. On tablet-size devices, * activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items * item details are presented side-by-side with a list of items
* in a {@link MessageListActivity}. * in a {@link MainActivity}.
* <p/> * <p/>
* This activity is mostly just a 'shell' activity containing nothing * 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 DetailActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.toolbar_layout);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Show the Up button in the action bar.
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// savedInstanceState is non-null when there is fragment state // savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity // saved from previous configurations of this activity
@ -58,30 +47,13 @@ public class SubscriptionDetailActivity extends AppCompatActivity {
// Create the detail fragment and add it to the activity // Create the detail fragment and add it to the activity
// using a fragment transaction. // using a fragment transaction.
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putSerializable(SubscriptionDetailFragment.ARG_ITEM, arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
getIntent().getSerializableExtra(SubscriptionDetailFragment.ARG_ITEM)); getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM));
SubscriptionDetailFragment fragment = new SubscriptionDetailFragment(); AddressDetailFragment fragment = new AddressDetailFragment();
fragment.setArguments(arguments); fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.add(R.id.content, fragment) .add(R.id.content, fragment)
.commit(); .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, MessageListActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
} }

View File

@ -0,0 +1,263 @@
/*
* 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.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;
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 android.widget.Toast;
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
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;
/**
* 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 {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
public static final String ARG_ITEM = "item";
public static final String EXPORT_POSTFIX = ".keys.dat";
/**
* 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);
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);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
final Activity ctx = getActivity();
switch (menuItem.getItemId()) {
case R.id.write_message: {
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: {
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);
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();
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;
}
}
@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) {
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());
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(
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 button, boolean checked) {
item.setSubscribed(checked);
}
});
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(Drawables.qrCode(item));
}
return rootView;
}
@Override
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();
}
}

View File

@ -16,25 +16,37 @@
package ch.dissem.apps.abit; package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
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.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; 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;
/** /**
* 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> { public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@ -43,18 +55,15 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag
} }
public void updateList() { public void updateList() {
List<BitmessageAddress> addresses = bmc.addresses().getContacts(); List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext())
.getContacts();
Collections.sort(addresses, new Comparator<BitmessageAddress>() { 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 @Override
public int compare(BitmessageAddress lhs, BitmessageAddress rhs) { 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.isSubscribed() == rhs.isSubscribed()) {
if (lhs.getAlias() != null) { if (lhs.getAlias() != null) {
if (rhs.getAlias() != null) { if (rhs.getAlias() != null) {
@ -76,38 +85,82 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag
} }
}); });
setListAdapter(new ArrayAdapter<BitmessageAddress>( setListAdapter(new ArrayAdapter<BitmessageAddress>(
getActivity(), getActivity(),
android.R.layout.simple_list_item_activated_1, android.R.layout.simple_list_item_activated_1,
android.R.id.text1, android.R.id.text1,
addresses) { addresses) {
@NonNull
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, @NonNull ViewGroup parent) {
if (convertView == null) { if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(getContext()); 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); BitmessageAddress item = getItem(position);
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); assert item != null;
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new
Identicon(item));
TextView name = (TextView) convertView.findViewById(R.id.name); TextView name = (TextView) convertView.findViewById(R.id.name);
name.setText(item.toString()); name.setText(item.toString());
TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number); 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,
convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? View.VISIBLE : View.INVISIBLE); item.getStream()));
convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ?
View.VISIBLE : View.INVISIBLE);
return convertView; return convertView;
} }
}); });
} }
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
if (ctx instanceof ActionBarListener) {
((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions));
}
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
View rootView = inflater.inflate(R.layout.fragment_subscribtions, container, false); savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_address_list, container, false);
return rootView; FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact);
fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() {
@Override
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:
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
startActivity(intent);
return true;
default:
return false;
}
}
});
return view;
} }
@Override @Override
void updateList(Label label) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && data.hasExtra("SCAN_RESULT")) {
Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT"));
Intent intent = new Intent(getActivity(), CreateAddressActivity.class);
intent.setData(uri);
startActivity(intent);
}
}
@Override
public void updateList(Label label) {
updateList(); updateList();
} }
} }

View File

@ -1,9 +1,34 @@
/*
* 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; package ch.dissem.apps.abit;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.entity.BitmessageAddress; 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. * Compose a new message.
@ -11,6 +36,11 @@ import ch.dissem.bitmessage.entity.BitmessageAddress;
public class ComposeMessageActivity extends AppCompatActivity { public class ComposeMessageActivity extends AppCompatActivity {
public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"; 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_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";
public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING";
public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -20,6 +50,7 @@ public class ComposeMessageActivity extends AppCompatActivity {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close); getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(false); getSupportActionBar().setHomeButtonEnabled(false);
@ -28,7 +59,47 @@ public class ComposeMessageActivity extends AppCompatActivity {
ComposeMessageFragment fragment = new ComposeMessageFragment(); ComposeMessageFragment fragment = new ComposeMessageFragment();
fragment.setArguments(getIntent().getExtras()); fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.replace(R.id.content, fragment) .replace(R.id.content, fragment)
.commit(); .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);
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:")) {
prefix = "";
} else {
prefix = "RE: ";
}
replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject());
replyIntent.putExtra(EXTRA_CONTENT,
"\n\n------------------------------------------------------\n"
+ item.getText());
return replyIntent;
} }
} }

View File

@ -1,24 +1,71 @@
/*
* 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; package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.*; import android.view.LayoutInflater;
import android.view.inputmethod.EditorInfo; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Toast; 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.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.extended.Message;
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_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_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. * Compose a new message.
*/ */
public class ComposeMessageFragment extends Fragment { public class ComposeMessageFragment extends Fragment {
private BitmessageContext bmCtx;
private BitmessageAddress identity; private BitmessageAddress identity;
private BitmessageAddress recipient; private BitmessageAddress recipient;
private String subject;
private String content;
private AutoCompleteTextView recipientInput;
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 * Mandatory empty constructor for the fragment manager to instantiate the
@ -33,10 +80,34 @@ public class ComposeMessageFragment extends Fragment {
if (getArguments() != null) { if (getArguments() != null) {
if (getArguments().containsKey(EXTRA_IDENTITY)) { if (getArguments().containsKey(EXTRA_IDENTITY)) {
identity = (BitmessageAddress) getArguments().getSerializable(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");
} }
broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false);
if (getArguments().containsKey(EXTRA_RECIPIENT)) { if (getArguments().containsKey(EXTRA_RECIPIENT)) {
recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT); recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT);
} }
if (getArguments().containsKey(EXTRA_SUBJECT)) {
subject = getArguments().getString(EXTRA_SUBJECT);
}
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");
} }
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -45,16 +116,60 @@ public class ComposeMessageFragment extends Fragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false); View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false);
if (recipient != null) { recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient);
EditText recipientInput = (EditText) rootView.findViewById(R.id.recipient); if (broadcast) {
recipientInput.setText(recipient.toString()); recipientInput.setVisibility(View.GONE);
} else {
final ContactAdapter adapter = new ContactAdapter(getContext());
recipientInput.setAdapter(adapter);
recipientInput.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
adapter.getItem(pos);
}
}
);
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) {
recipientInput.setText(recipient.toString());
}
} }
EditText body = (EditText) rootView.findViewById(R.id.body); subjectInput = (EditText) rootView.findViewById(R.id.subject);
body.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); subjectInput.setText(subject);
body.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); 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; return rootView;
} }
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (identity == null || identity.getPrivateKey() == null) {
identity = Singleton.getIdentity(context);
}
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.compose, menu); inflater.inflate(R.menu.compose, menu);
@ -65,11 +180,87 @@ public class ComposeMessageFragment extends Fragment {
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.send: case R.id.send:
Toast.makeText(getActivity(), "TODO: Send", Toast.LENGTH_SHORT).show(); send();
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; return true;
default: default:
return super.onOptionsItemSelected(item); 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 Message.Builder()
.subject(subjectInput.getText().toString())
.body(bodyInput.getText().toString())
.addParent(parent)
.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()
);
break;
}
bmc.send(builder.build());
getActivity().finish();
}
} }

View File

@ -0,0 +1,169 @@
/*
* 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.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);
Uri uri = getIntent().getData();
if (uri != null)
setContentView(R.layout.activity_open_bitmessage_link);
else
setContentView(R.layout.activity_create_bitmessage_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);
if (uri != null) {
String addressText = getAddress(uri);
String[] parameters = getParameters(uri);
for (String parameter : parameters) {
Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter);
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;
}
}
}
address.setText(addressText);
}
final Button cancel = (Button) findViewById(R.id.cancel);
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(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);
}
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();
} catch (RuntimeException e) {
address.setError(getString(R.string.error_illegal_address));
}
}
});
}
private String getAddress(Uri uri) {
StringBuilder result = new StringBuilder();
String schemeSpecificPart = uri.getSchemeSpecificPart();
if (!schemeSpecificPart.startsWith("BM-")) {
result.append("BM-");
}
if (schemeSpecificPart.contains("?")) {
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')));
} else if (schemeSpecificPart.contains("#")) {
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')));
} else {
result.append(schemeSpecificPart);
}
return result.toString();
}
private String[] getParameters(Uri uri) {
int index = uri.getSchemeSpecificPart().indexOf('?');
if (index >= 0) {
String parameterPart = uri.getSchemeSpecificPart().substring(index + 1);
return parameterPart.split("&");
} else {
return new String[0];
}
}
}

View File

@ -0,0 +1,53 @@
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 abstract 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) {
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);
}
}
}

View File

@ -16,63 +16,95 @@
package ch.dissem.apps.abit; package ch.dissem.apps.abit;
import android.graphics.*; import android.graphics.*;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
/** /**
* * @author Christian Basler
*/ */
public class Identicon extends Drawable { public class Identicon extends Drawable {
private static final int SIZE = 9; private static final int SIZE = 9;
private static final int CENTER_COLUMN = 5; private static final int CENTER_COLUMN = 5;
private final Paint paint; private final Paint paint;
private float width; private final int color;
private float height; private final int background;
private final boolean[][] fields;
private final boolean chan;
private final TextPaint textPaint;
private float cellWidth; public Identicon(@NonNull BitmessageAddress input) {
private float cellHeight;
private byte[] hash;
private int color;
private int background;
private boolean[][] fields;
public Identicon(BitmessageAddress input) {
paint = new Paint(); paint = new Paint();
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true); paint.setAntiAlias(true);
textPaint = new TextPaint();
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setColor(0xFF607D8B);
textPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
hash = input.getRipe(); chan = input.isChan();
byte[] hash = input.getRipe();
fields = new boolean[SIZE][SIZE]; fields = new boolean[SIZE][SIZE];
color = Color.HSVToColor(new float[]{Math.abs(hash[0] * hash[1] + hash[2]) % 360, 0.8f, 1.0f}); color = Color.HSVToColor(new float[]{
background = Color.HSVToColor(new float[]{Math.abs(hash[1] * hash[2] + hash[0]) % 360, 0.8f, 1.0f}); 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 row = 0; row < SIZE; row++) {
for (int column = 0; column < SIZE; column++) { if (!chan || row < 5 || row > 6) {
fields[row][column] = hash[(row * (column < CENTER_COLUMN ? column : SIZE - column - 1)) % hash.length] >= 0; 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];
}
}
} }
} }
} }
@Override @Override
public void draw(Canvas canvas) { public void draw(@NonNull Canvas canvas) {
float x, y; float x, y;
float width = canvas.getWidth();
float height = canvas.getHeight();
float cellWidth = width / (float) SIZE;
float cellHeight = height / (float) SIZE;
paint.setColor(background); paint.setColor(background);
canvas.drawCircle(width/2, height/2, width/2, paint); canvas.drawCircle(width / 2, height / 2, width / 2, paint);
paint.setColor(color); paint.setColor(color);
for (int row = 0; row < SIZE; row++) { for (int row = 0; row < SIZE; row++) {
for (int column = 0; column < SIZE; column++) { for (int column = 0; column < SIZE; column++) {
if (fields[row][column]) { if (fields[row][column]) {
x = cellWidth * column; x = cellWidth * column;
y = cellHeight * row; y = cellHeight * row;
canvas.drawCircle(
canvas.drawCircle(x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, paint); 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 @Override
@ -89,16 +121,4 @@ public class Identicon extends Drawable {
public int getOpacity() { public int getOpacity() {
return PixelFormat.TRANSPARENT; 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;
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 AddressSelectorAdapter adapter;
private WifImporter importer;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
String wifData = getArguments().getString(WIF_DATA);
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());
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(),
LinearLayoutManager.VERTICAL,
false);
RecyclerView 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 view) {
importer.importAll(adapter.getSelected());
MainActivity mainActivity = MainActivity.getInstance();
if (mainActivity != null) {
for (BitmessageAddress selected : adapter.getSelected()) {
mainActivity.addIdentityEntry(selected);
}
}
getActivity().finish();
}
});
return view;
}
}

View File

@ -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();
}
}
}

View File

@ -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 view) {
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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,622 @@
/*
* 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.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;
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;
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;
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
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 java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
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.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;
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.repository.AndroidMessageRepository.LABEL_ARCHIVE;
import static ch.dissem.apps.abit.service.BitmessageService.isRunning;
/**
* An activity representing a list of Messages. This activity
* has different presentations for handset and tablet-size devices. On
* handsets, the activity presents a list of items, which when touched,
* lead to a {@link MessageDetailActivity} representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
* <p>
* The activity makes heavy use of fragments. The list of items is a
* {@link MessageListFragment} and the item details
* (if present) is a {@link MessageDetailFragment}.
* </p><p>
* This activity also implements the required
* {@link ListSelectionListener} interface
* to listen for item selections.
* </p>
*/
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";
private static final int ADD_IDENTITY = 1;
private static final int MANAGE_IDENTITY = 2;
private static final long ID_NODE_SWITCH = 1;
private static WeakReference<MainActivity> instance;
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean twoPane;
private Label selectedLabel;
private BitmessageContext bmc;
private AccountHeader accountHeader;
private Drawer drawer;
private SwitchDrawerItem nodeSwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instance = new WeakReference<>(this);
bmc = Singleton.getBitmessageContext(this);
setContentView(R.layout.activity_message_list);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
MessageListFragment listFragment = new MessageListFragment();
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
// large-screen layouts (res/values-large and
// 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);
// handle intents
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)) {
SyncAdapter.startSync(this);
} 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);
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()
.setButtonPosition(lps);
}
}
private <F extends Fragment & ListHolder> void changeList(F listFragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.item_list, listFragment)
.addToBackStack(null)
.commit();
if (twoPane) {
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true);
}
}
private void createDrawer(Toolbar toolbar) {
final ArrayList<IProfile> profiles = new ArrayList<>();
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)
);
profiles.add(new ProfileSettingDrawerItem()
.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)
.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) {
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;
}
})
.build();
if (profiles.size() > 2) { // There's always the add and manage identity items
accountHeader.setActiveProfile(profiles.get(0), true);
}
final ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
drawerItems.add(new PrimaryDrawerItem()
.withName(R.string.archive)
.withTag(LABEL_ARCHIVE)
.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));
nodeSwitch = new SwitchDrawerItem()
.withIdentifier(ID_NODE_SWITCH)
.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();
} else {
stopService(new Intent(MainActivity.this, BitmessageService.class));
}
}
});
drawer = new DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withAccountHeader(accountHeader)
.withDrawerItems(drawerItems)
.addStickyDrawerItems(nodeSwitch)
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
@Override
public boolean onItemClick(View view, int position, IDrawerItem item) {
if (item.getTag() instanceof Label) {
selectedLabel = (Label) item.getTag();
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;
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.full_node:
return true;
}
}
return false;
}
})
.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);
}
IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel);
if (selectedDrawerItem != null) {
drawer.setSelection(selectedDrawerItem);
}
}
}.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");
IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel);
if (selectedItem != null) {
drawer.setSelection(selectedItem);
}
super.onRestoreInstanceState(savedInstanceState);
}
private void addIdentityDialog() {
AddIdentityDialogFragment dialog = new AddIdentityDialogFragment();
dialog.show(getSupportFragmentManager(), "dialog");
}
@Override
protected void onResume() {
updateUnread();
updateNodeSwitch();
Singleton.getMessageListener(this).resetNotification();
super.onResume();
}
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 2 setting elements.
// Set the new profile above them ;)
accountHeader.addProfile(
newProfile, accountHeader.getProfiles().size() - 2);
} else {
accountHeader.addProfiles(newProfile);
}
}
public void addLabelEntry(Label label) {
PrimaryDrawerItem item = new PrimaryDrawerItem()
.withName(label.toString())
.withTag(label)
.withIcon(Labels.getIcon(label))
.withIconColor(Labels.getColor(label));
drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3);
}
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) {
if (identity.equals(((ProfileDrawerItem) profile).getTag())) {
accountHeader.removeProfile(profile);
return;
}
}
}
}
private void checkAndStartNode() {
if (Preferences.isConnectionAllowed(MainActivity.this)) {
startService(new Intent(this, BitmessageService.class));
} else {
startActivity(new Intent(this, FullNodeDialogActivity.class));
}
}
@Override
public void updateUnread() {
for (IDrawerItem item : drawer.getDrawerItems()) {
if (item.getTag() instanceof Label) {
Label label = (Label) item.getTag();
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);
}
}
}
}
public static void updateNodeSwitch() {
final MainActivity i = getInstance();
if (i != null) {
i.runOnUiThread(new Runnable() {
@Override
public void run() {
i.nodeSwitch.withChecked(i.bmc.isRunning());
i.drawer.updateStickyFooterItem(i.nodeSwitch);
}
});
}
}
/**
* Callback method from {@link ListSelectionListener}
* indicating that the item with the given ID was selected.
*/
@Override
public void onItemSelected(Serializable item) {
if (twoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item);
Fragment fragment;
if (item instanceof Plaintext)
fragment = new MessageDetailFragment();
else if (item instanceof BitmessageAddress)
fragment = new AddressDetailFragment();
else
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
"was "
+ item.getClass().getSimpleName());
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent;
if (item instanceof Plaintext) {
detailIntent = new Intent(this, MessageDetailActivity.class);
detailIntent.putExtra(EXTRA_SHOW_LABEL, selectedLabel);
} else if (item instanceof BitmessageAddress) {
detailIntent = new Intent(this, AddressDetailActivity.class);
} else {
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " +
"was "
+ item.getClass().getSimpleName());
}
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
startActivity(detailIntent);
}
}
@Override
public void updateTitle(CharSequence title) {
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(title);
}
}
public Label getSelectedLabel() {
return selectedLabel;
}
public static MainActivity getInstance() {
if (instance == null) return null;
return instance.get();
}
}

View File

@ -3,32 +3,26 @@ package ch.dissem.apps.abit;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.NavUtils; 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; import android.view.MenuItem;
import ch.dissem.bitmessage.entity.valueobject.Label;
/** /**
* An activity representing a single Message detail screen. This * An activity representing a single Message detail screen. This
* activity is only used on handset devices. On tablet-size devices, * activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items * item details are presented side-by-side with a list of items
* in a {@link MessageListActivity}. * in a {@link MainActivity}.
* <p/> * <p/>
* This activity is mostly just a 'shell' activity containing nothing * This activity is mostly just a 'shell' activity containing nothing
* more than a {@link MessageDetailFragment}. * more than a {@link MessageDetailFragment}.
*/ */
public class MessageDetailActivity extends AppCompatActivity { public class MessageDetailActivity extends DetailActivity {
private Label label;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.toolbar_layout);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Show the Up button in the action bar.
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// savedInstanceState is non-null when there is fragment state // savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity // saved from previous configurations of this activity
@ -40,33 +34,30 @@ public class MessageDetailActivity extends AppCompatActivity {
// http://developer.android.com/guide/components/fragments.html // http://developer.android.com/guide/components/fragments.html
// //
if (savedInstanceState == null) { if (savedInstanceState == null) {
label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL);
// Create the detail fragment and add it to the activity // Create the detail fragment and add it to the activity
// using a fragment transaction. // using a fragment transaction.
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM)); getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM));
MessageDetailFragment fragment = new MessageDetailFragment(); MessageDetailFragment fragment = new MessageDetailFragment();
fragment.setArguments(arguments); fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.add(R.id.content, fragment) .add(R.id.content, fragment)
.commit(); .commit();
} }
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId(); switch (item.getItemId()) {
if (id == android.R.id.home) { case android.R.id.home:
// This ID represents the Home or Up button. In the case of this Intent parentIntent = new Intent(this, MainActivity.class);
// activity, the Up button is shown. Use NavUtils to allow users parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label);
// to navigate up one level in the application structure. For NavUtils.navigateUpTo(this, parentIntent);
// more details, see the Navigation pattern on Android Design: return true;
// default:
// http://developer.android.com/design/patterns/navigation.html#up-vs-back return super.onOptionsItemSelected(item);
//
NavUtils.navigateUpTo(this, new Intent(this, MessageListActivity.class));
return true;
} }
return super.onOptionsItemSelected(item);
} }
} }

View File

@ -1,25 +1,68 @@
/*
* 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; package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.*; 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;
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.ImageView;
import android.widget.TextView; 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.service.Singleton;
import ch.dissem.apps.abit.utils.Drawables; import ch.dissem.apps.abit.util.Assets;
import ch.dissem.bitmessage.BitmessageContext; 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.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import com.mikepenz.google_material_typeface_library.GoogleMaterial; import ch.dissem.bitmessage.ports.MessageRepository;
import java.util.Iterator; 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;
/** /**
* A fragment representing a single Message detail screen. * 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} * in two-pane mode (on tablets) or a {@link MessageDetailActivity}
* on handsets. * on handsets.
*/ */
@ -64,38 +107,82 @@ public class MessageDetailFragment extends Fragment {
// Show the dummy content as text in a TextView. // Show the dummy content as text in a TextView.
if (item != null) { if (item != null) {
((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); ((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(); 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()); ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString());
if (item.getTo() != null) { if (item.getTo() != null) {
((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString());
} else if (item.getType() == Plaintext.Type.BROADCAST) { } else if (item.getType() == Plaintext.Type.BROADCAST) {
((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast); ((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast);
} }
((TextView) rootView.findViewById(R.id.text)).setText(item.getText()); RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels);
} LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels());
labelView.setAdapter(labelAdapter);
labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
boolean removed = false; TextView messageBody = (TextView) rootView.findViewById(R.id.text);
Iterator<Label> labels = item.getLabels().iterator(); messageBody.setText(item.getText());
while (labels.hasNext()) {
if (labels.next().getType() == Label.Type.UNREAD) { Linkify.addLinks(messageBody, WEB_URLS);
labels.remove(); Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
removed = true; new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher match, String url) {
return match.group();
}
}
);
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;
}
} }
} MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext());
if (removed) { if (removed) {
Singleton.getBitmessageContext(inflater.getContext()).messages().save(item); if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
messageRepo.save(item);
}
List<Plaintext> parents = new ArrayList<>(item.getParents().size());
for (InventoryVector parentIV : item.getParents()) {
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));
} }
return rootView; return rootView;
} }
private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) {
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id);
RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message, menu); inflater.inflate(R.menu.message, menu);
Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply); 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.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); Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive);
super.onCreateOptionsMenu(menu, inflater); super.onCreateOptionsMenu(menu, inflater);
@ -103,38 +190,41 @@ public class MessageDetailFragment extends Fragment {
@Override @Override
public boolean onOptionsItemSelected(MenuItem menuItem) { public boolean onOptionsItemSelected(MenuItem menuItem) {
BitmessageContext bmc = Singleton.getBitmessageContext(getActivity()); MessageRepository messageRepo = Singleton.getMessageRepository(getContext());
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.reply: case R.id.reply:
Intent replyIntent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); ComposeMessageActivity.launchReplyTo(this, item);
replyIntent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item.getFrom());
replyIntent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, item.getTo());
startActivity(replyIntent);
return true; return true;
case R.id.delete: case R.id.delete:
if (isInTrash(item)) { if (isInTrash(item)) {
bmc.messages().remove(item); messageRepo.remove(item);
} else { } else {
item.getLabels().clear(); item.getLabels().clear();
item.addLabels(bmc.messages().getLabels(Label.Type.TRASH)); item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
bmc.messages().save(item); messageRepo.save(item);
} }
getActivity().onBackPressed(); getActivity().onBackPressed();
return true; return true;
case R.id.mark_unread: case R.id.mark_unread:
item.addLabels(bmc.messages().getLabels(Label.Type.UNREAD)); item.addLabels(messageRepo.getLabels(Label.Type.UNREAD));
bmc.messages().save(item); messageRepo.save(item);
if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
return true; return true;
case R.id.archive: case R.id.archive:
if (item.isUnread() && getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
item.getLabels().clear(); item.getLabels().clear();
bmc.messages().save(item); messageRepo.save(item);
return true; return true;
default: default:
return false; return false;
} }
} }
private boolean isInTrash(Plaintext item) { public static boolean isInTrash(Plaintext item) {
for (Label label : item.getLabels()) { for (Label label : item.getLabels()) {
if (label.getType() == Label.Type.TRASH) { if (label.getType() == Label.Type.TRASH) {
return true; return true;
@ -142,4 +232,136 @@ public class MessageDetailFragment extends Fragment {
} }
return false; 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()));
viewHolder.item = message;
}
// Returns the total count of items in the list
@Override
public int getItemCount() {
return messages.size();
}
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(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);
}
}
});
}
}
}
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);
}
}
}
} }

View File

@ -1,346 +0,0 @@
package ch.dissem.apps.abit;
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.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;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
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.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
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.util.ArrayList;
/**
* An activity representing a list of Messages. This activity
* has different presentations for handset and tablet-size devices. On
* handsets, the activity presents a list of items, which when touched,
* lead to a {@link MessageDetailActivity} representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
* <p>
* The activity makes heavy use of fragments. The list of items is a
* {@link MessageListFragment} and the item details
* (if present) is a {@link MessageDetailFragment}.
* </p><p>
* This activity also implements the required
* {@link ListSelectionListener} interface
* to listen for item selections.
* </p>
*/
public class MessageListActivity 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 int ADD_IDENTITY = 1;
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean twoPane;
private AccountHeader accountHeader;
private BitmessageContext bmc;
private Label selectedLabel;
private Menu menu;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bmc = Singleton.getBitmessageContext(this);
selectedLabel = bmc.messages().getLabels().get(0);
setContentView(R.layout.activity_message_list);
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
MessageListFragment listFragment = new MessageListFragment();
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
// large-screen layouts (res/values-large and
// res/values-sw600dp). If this view is present, then the
// activity should be in two-pane mode.
twoPane = true;
}
createDrawer(toolbar);
Singleton.getMessageListener(this).resetNotification();
// handle intents
if (getIntent().hasExtra(EXTRA_SHOW_MESSAGE)) {
onItemSelected(getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE));
}
}
@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) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.item_list, listFragment)
.addToBackStack(null)
.commit();
if (twoPane) {
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true);
}
}
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())
.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)
);
profiles.add(new ProfileSettingDrawerItem()
.withName(getString(R.string.manage_identity))
.withIcon(GoogleMaterial.Icon.gmd_settings)
);
// 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) {
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);
}
}
// false if it should close the drawer
return false;
}
})
.build();
ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
for (Label label : bmc.messages().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);
}
drawerItems.add(item);
}
new DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withAccountHeader(accountHeader)
.withDrawerItems(drawerItems)
.addStickyDrawerItems(
new SecondaryDrawerItem()
.withName(R.string.subscriptions)
.withIcon(CommunityMaterial.Icon.cmd_rss_box),
new SecondaryDrawerItem()
.withName(R.string.settings)
.withIcon(GoogleMaterial.Icon.gmd_settings)
)
.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);
}
return false;
} else if (item instanceof Nameable<?>) {
Nameable<?> ni = (Nameable<?>) item;
switch (ni.getNameRes()) {
case R.string.subscriptions:
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(MessageListActivity.this, SettingsActivity.class));
break;
}
}
return false;
}
})
.withCloseOnClick(true)
.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.
*/
@Override
public void onItemSelected(Serializable item) {
if (twoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item);
Fragment fragment;
if (item instanceof Plaintext)
fragment = new MessageDetailFragment();
else if (item instanceof BitmessageAddress)
fragment = new SubscriptionDetailFragment();
else
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was "
+ item.getClass().getSimpleName());
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent;
if (item instanceof Plaintext)
detailIntent = new Intent(this, MessageDetailActivity.class);
else if (item instanceof BitmessageAddress)
detailIntent = new Intent(this, SubscriptionDetailActivity.class);
else
throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but was "
+ item.getClass().getSimpleName());
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
startActivity(detailIntent);
}
}
@Override
public void updateTitle(CharSequence title) {
getSupportActionBar().setTitle(title);
}
public Label getSelectedLabel() {
return selectedLabel;
}
}

View File

@ -1,24 +1,61 @@
/*
* 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; package ch.dissem.apps.abit;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.graphics.Typeface; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment;
import android.support.v4.app.ListFragment; import android.support.v4.content.ContextCompat;
import android.view.*; import android.support.v7.widget.LinearLayoutManager;
import android.widget.ArrayAdapter; import android.support.v7.widget.RecyclerView;
import android.widget.ImageView; import android.view.LayoutInflater;
import android.widget.ListView; import android.view.Menu;
import android.widget.TextView; import android.view.MenuInflater;
import ch.dissem.apps.abit.listeners.ActionBarListener; import android.view.MenuItem;
import ch.dissem.apps.abit.listeners.ListSelectionListener; 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;
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.Collections;
import java.util.List;
import java.util.Objects;
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.apps.abit.service.Singleton;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository; 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;
/** /**
* A list fragment representing a list of Messages. This fragment * A list fragment representing a list of Messages. This fragment
@ -29,10 +66,19 @@ import ch.dissem.bitmessage.ports.MessageRepository;
* Activities containing this fragment MUST implement the {@link ListSelectionListener} * Activities containing this fragment MUST implement the {@link ListSelectionListener}
* interface. * 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 Label currentLabel;
private MenuItem emptyTrashMenuItem; private MenuItem emptyTrashMenuItem;
private MessageRepository messageRepo;
private boolean activateOnItemClick;
/** /**
* Mandatory empty constructor for the fragment manager to instantiate the * Mandatory empty constructor for the fragment manager to instantiate the
@ -51,65 +97,202 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
MainActivity activity = (MainActivity) getActivity();
messageRepo = Singleton.getMessageRepository(activity);
updateList(((MessageListActivity) getActivity()).getSelectedLabel()); doUpdateList(activity.getSelectedLabel());
} }
@Override @Override
public void updateList(Label label) { public void updateList(Label label) {
currentLabel = label; if (!isResumed()) {
setListAdapter(new ArrayAdapter<Plaintext>( currentLabel = label;
getActivity(), return;
android.R.layout.simple_list_item_activated_1, }
android.R.id.text1,
bmc.messages().findMessages(label)) { if (!Objects.equals(currentLabel, label)) {
@Override adapter.setData(label, Collections.<Plaintext>emptyList());
public View getView(int position, View convertView, ViewGroup parent) { adapter.notifyDataSetChanged();
if (convertView == null) { }
LayoutInflater inflater = LayoutInflater.from(getContext()); doUpdateList(label);
convertView = inflater.inflate(R.layout.message_row, null, false); }
}
Plaintext item = getItem(position); private void doUpdateList(final Label label) {
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item.getFrom())); if (label == null) {
TextView sender = (TextView) convertView.findViewById(R.id.sender); if (getActivity() instanceof ActionBarListener) {
sender.setText(item.getFrom().toString()); ((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name));
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;
} }
}); adapter.setData(null, Collections.<Plaintext>emptyList());
if (getActivity() instanceof ActionBarListener) { adapter.notifyDataSetChanged();
((ActionBarListener) getActivity()).updateTitle(label.toString()); return;
} }
currentLabel = label;
if (emptyTrashMenuItem != null) { if (emptyTrashMenuItem != null) {
emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH);
} }
if (getActivity() instanceof ActionBarListener) {
ActionBarListener actionBarListener = (ActionBarListener) getActivity();
if ("archive".equals(label.toString())) {
actionBarListener.updateTitle(getString(R.string.archive));
} else {
actionBarListener.updateTitle(label.toString());
}
}
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 @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); 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. // Show the dummy content as text in a TextView.
FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fab_compose_message); FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id
fab.setOnClickListener(new View.OnClickListener() { .fab_compose_message);
fab.setMenuListener(new SimpleMenuListenerAdapter() {
@Override @Override
public void onClick(View view) { public boolean onMenuItemSelected(MenuItem menuItem) {
startActivity(new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class)); 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
// animation is running)
recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager();
recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning
(true);
recyclerViewTouchActionGuardManager.setEnabled(true);
// swipe manager
recyclerViewSwipeManager = new RecyclerViewSwipeManager();
//adapter
adapter = new SwipeableMessageAdapter();
adapter.setActivateOnItemClick(activateOnItemClick);
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) {
int position = recyclerView.getChildAdapterPosition(v);
adapter.setSelectedPosition(position);
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; 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 @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message_list, menu); inflater.inflate(R.menu.message_list, menu);
@ -123,15 +306,31 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
case R.id.empty_trash: case R.id.empty_trash:
if (currentLabel.getType() != Label.Type.TRASH) return true; if (currentLabel.getType() != Label.Type.TRASH) return true;
MessageRepository repo = bmc.messages(); new AsyncTask<Void, Void, Void>() {
for (Plaintext message : repo.findMessages(currentLabel)) { @Override
repo.remove(message); protected Void doInBackground(Void... params) {
} for (Plaintext message : messageRepo.findMessages(currentLabel)) {
updateList(currentLabel); messageRepo.remove(message);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
updateList(currentLabel);
}
}.execute();
return true; return true;
default: default:
return false; return false;
} }
} }
@Override
public void setActivateOnItemClick(boolean activateOnItemClick) {
if (adapter != null) {
adapter.setActivateOnItemClick(activateOnItemClick);
}
this.activateOnItemClick = activateOnItemClick;
}
} }

View File

@ -1,113 +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.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;
import android.widget.TextView;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
public class OpenBitmessageLinkActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_open_bitmessage_link);
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();
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();
importContact.setChecked(parameter.contains("add"));
subscribe.setChecked(parameter.contains("subscribe"));
}
}
addressView.setText(address);
final Button cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View 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) {
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);
}
setResult(Activity.RESULT_OK);
finish();
}
});
}
private String getAddress(Uri uri) {
StringBuilder result = new StringBuilder();
String schemeSpecificPart = uri.getSchemeSpecificPart();
if (!schemeSpecificPart.startsWith("BM-")) {
result.append("BM-");
}
if (schemeSpecificPart.contains("?")) {
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')));
} else if (schemeSpecificPart.contains("#")) {
result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')));
} else {
result.append(schemeSpecificPart);
}
return result.toString();
}
private String[] getParameters(Uri uri) {
int index = uri.getSchemeSpecificPart().indexOf('?');
if (index >= 0) {
String parameterPart = uri.getSchemeSpecificPart().substring(index + 1);
return parameterPart.split("&");
} else {
return new String[0];
}
}
}

View File

@ -1,27 +1,18 @@
package ch.dissem.apps.abit; package ch.dissem.apps.abit;
import android.os.Bundle; import android.os.Bundle;
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 { public class SettingsActivity extends DetailActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.toolbar_layout);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(false);
// Display the fragment as the main content. // Display the fragment as the main content.
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.replace(R.id.content, new SettingsFragment()) .replace(R.id.content, new SettingsFragment())
.commit(); .commit();
} }
} }

View File

@ -1,18 +1,140 @@
/*
* 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; 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.os.Bundle;
import android.preference.PreferenceActivity; import android.preference.Preference;
import android.preference.PreferenceFragment; 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.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;
/** /**
* Created by chris on 14.07.15. * @author Christian Basler
*/ */
public class SettingsFragment extends PreferenceFragment { public class SettingsFragment
extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Load the preferences from an XML resource // Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences); 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;
}
});
final Preference cleanup = findPreference("cleanup");
cleanup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@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(ctx, R.string.cleanup_notification_start, Toast
.LENGTH_SHORT).show();
}
@Override
protected Void doInBackground(Void... voids) {
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
bmc.cleanup();
bmc.internals().getNodeRegistry().clear();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
Toast.makeText(
ctx,
R.string.cleanup_notification_end,
Toast.LENGTH_LONG
).show();
cleanup.setEnabled(true);
}
}.execute();
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
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;
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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 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;
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);
//noinspection ConstantConditions
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()) {
status.append(address.getAddress()).append('\n');
}
status.append('\n');
status.append(bmc.status());
((TextView) findViewById(R.id.content)).setText(status);
}
}

View File

@ -1,121 +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 MessageListActivity}
* 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_subscription_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);
}
});
}
return rootView;
}
@Override
public void onPause() {
Singleton.getBitmessageContext(getActivity()).addresses().save(item);
super.onPause();
}
}

View File

@ -0,0 +1,99 @@
/*
* 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 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 compoundButton, boolean isChecked) {
if (data != null) {
data.selected = isChecked;
}
}
});
}
}
public List<BitmessageAddress> getSelected() {
List<BitmessageAddress> result = new LinkedList<>();
for (Selectable<BitmessageAddress> selectable : data) {
if (selectable.selected) {
result.add(selectable.data);
}
}
return result;
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015 Christian Basler * Copyright 2016 Christian Basler
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,19 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.utils; package ch.dissem.apps.abit.adapter;
import android.content.Context; import ch.dissem.apps.abit.util.PRNGFixes;
import android.view.Menu; import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography;
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. * @author Christian Basler
*/ */
public class Drawables { public class AndroidCryptography extends SpongyCryptography {
public static void addIcon(Context ctx, Menu menu, int menuItem, GoogleMaterial.Icon icon) { public AndroidCryptography() {
menu.findItem(menuItem).setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.primary_text_default_material_dark).actionBar()); PRNGFixes.apply();
} }
} }

View File

@ -0,0 +1,140 @@
/*
* 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.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();
}
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,329 @@
/*
* 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.annotation.SuppressLint;
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.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;
/**
* 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.ViewHolder>
implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
private List<Plaintext> data = Collections.emptyList();
private EventListener eventListener;
private final View.OnClickListener itemViewOnClickListener;
private final 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);
void onItemArchived(Plaintext item);
void onItemViewClicked(View v);
}
@SuppressWarnings("WeakerAccess")
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;
ViewHolder(View v) {
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);
}
@Override
public View getSwipeableContainerView() {
return container;
}
}
public SwipeableMessageAdapter() {
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.
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);
}
}
private void onSwipeableViewContainerClick(View v) {
if (eventListener != null) {
eventListener.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v));
}
}
public Plaintext getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return (long) data.get(position).getId();
}
@Override
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 ViewHolder(v);
}
@Override
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);
// (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.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()));
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(ViewHolder holder, int position, int x, int y) {
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;
}
@Override
@SuppressLint("SwitchIntDef")
public void onSetSwipeBackground(ViewHolder holder, int position, int type) {
int bgRes = 0;
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 == LABEL_ARCHIVE || 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
@SuppressLint("SwitchIntDef")
public SwipeResultAction onSwipeItem(ViewHolder 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;
}
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;
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.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;
}
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.util.Arrays;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
/**
* 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);
}
}
}
}

View File

@ -0,0 +1,162 @@
/*
* 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.v4.app.FragmentActivity;
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;
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;
/**
* @author Christian Basler
*/
public class AddIdentityDialogFragment 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_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 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);
}
@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();
}
});
return view;
}
private void addChanDialog() {
FragmentActivity activity = getActivity();
final Context ctx = activity.getBaseContext();
@SuppressLint("InflateParams")
final View dialogView = activity.getLayoutInflater()
.inflate(R.layout.dialog_input_passphrase, null);
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;
}
@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();
}
@Override
public int getTheme() {
return R.style.FixedDialog;
}
}

View File

@ -0,0 +1,136 @@
/*
* 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.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 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) {
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 view) {
dismiss();
}
});
return view;
}
@Override
public int getTheme() {
return R.style.FixedDialog;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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 android.view.View;
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(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();
}
});
}
}

View File

@ -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.Intent;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.entity.Plaintext;
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 static final Logger LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment.class);
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;
default:
LOG.warn("Unexpected encoding: " + encoding);
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:
encoding = SIMPLE;
break;
default:
dismiss();
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;
}
}

View File

@ -14,11 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.listeners; package ch.dissem.apps.abit.listener;
/** /**
* Created by chris on 06.09.15. * @author Christian Basler
*/ */
public interface ActionBarListener { public interface ActionBarListener {
void updateTitle(CharSequence title); void updateTitle(CharSequence title);
void updateUnread();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015 Christian Basler * Copyright 2016 Christian Basler
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * 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 * A callback interface that all activities containing this fragment must

View File

@ -0,0 +1,85 @@
/*
* 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.listener;
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;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.Plaintext;
/**
* 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,
* notifications should be combined.
* </p>
*/
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);
}
@Override
public void receive(final Plaintext plaintext) {
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();
}
}
});
}
public void resetNotification() {
pool.submit(new Runnable() {
@Override
public void run() {
notification.hide();
unacknowledged.clear();
numberOfUnacknowledgedMessages = 0;
}
});
}
}

View File

@ -0,0 +1,62 @@
/*
* 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;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
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 ("android.net.conn.CONNECTIVITY_CHANGE".equals(intent.getAction())) {
if (Preferences.isWifiOnly(ctx)) {
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) {
bmc.shutdown();
}
}
}
}
public static boolean isConnectedToMeteredNetwork(Context ctx) {
NetworkInfo netInfo = getNetworkInfo(ctx);
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);
return conMan.getActiveNetworkInfo();
}
}

View File

@ -1,153 +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.listeners;
import android.annotation.TargetApi;
import android.app.Notification;
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.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,
* notifications should be combined.
* </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;
public MessageListener(Context ctx) {
this.ctx = ctx.getApplicationContext();
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
this.pictureSize = getMaxContactPhotoSize(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) {
unacknowledged.addFirst(plaintext);
numberOfUnacknowledgedMessages++;
if (unacknowledged.size() > 5) {
unacknowledged.removeLast();
}
}
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);
} 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);
}
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;
}
public void resetNotification() {
manager.cancel(0);
synchronized (unacknowledged) {
unacknowledged.clear();
numberOfUnacknowledgedMessages = 0;
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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;
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;
protected Notification notification;
public AbstractNotification(Context ctx) {
this.ctx = ctx.getApplicationContext();
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* @return an id unique to this notification class
*/
protected abstract int getNotificationId();
public Notification getNotification() {
return notification;
}
public void show() {
manager.notify(getNotificationId(), notification);
}
public void hide() {
manager.cancel(getNotificationId());
}
}

View File

@ -0,0 +1,61 @@
/*
* 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;
import android.support.annotation.StringRes;
import android.support.v7.app.NotificationCompat;
import ch.dissem.apps.abit.R;
/**
* 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;
private final 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;
}
}

View File

@ -0,0 +1,143 @@
/*
* 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;
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.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 NETWORK_NOTIFICATION_ID = 2;
private final NotificationCompat.Builder builder;
private Timer timer;
public NetworkNotification(Context ctx) {
super(ctx);
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))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setShowWhen(false)
.setContentIntent(pendingIntent);
}
@SuppressLint("StringFormatMatches")
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean update() {
boolean running = BitmessageService.isRunning();
builder.setOngoing(running);
Property connections = BitmessageService.getStatus().getProperty("network", "connections");
if (!running) {
builder.setContentText(ctx.getString(R.string.connection_info_disconnected));
updateNodeSwitch();
} 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);
}
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;
}
@Override
public void show() {
super.show();
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
if (!update()) {
cancel();
ctx.stopService(new Intent(ctx, BitmessageService.class));
}
NetworkNotification.super.show();
}
}, 10_000, 10_000);
}
public void showShutdown() {
if (timer != null) {
timer.cancel();
}
update();
super.show();
}
@Override
protected int getNotificationId() {
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();
}
}

View File

@ -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.notification;
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.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 {
private static final int NEW_MESSAGE_NOTIFICATION_ID = 1;
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");
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),
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) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
builder.setSmallIcon(R.drawable.ic_notification_new_message)
.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) {
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, MainActivity.class);
intent.setAction(MainActivity.ACTION_SHOW_INBOX);
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0);
builder.setContentIntent(pendingIntent);
notification = builder.build();
return this;
}
@Override
protected int getNotificationId() {
return NEW_MESSAGE_NOTIFICATION_ID;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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;
import android.content.Context;
import android.content.Intent;
import android.support.v7.app.NotificationCompat;
import ch.dissem.apps.abit.MainActivity;
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);
update(0);
}
@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(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();
return this;
}
}

View File

@ -0,0 +1,97 @@
/*
* 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;
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.cryptography;
/**
* @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,
cryptography().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;
}
}

View File

@ -1,328 +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.repositories;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static ch.dissem.apps.abit.repositories.SqlHelper.join;
/**
* {@link MessageRepository} implementation using the Android SQL API.
*/
public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder {
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
private static final String TABLE_NAME = "Message";
private static final String COLUMN_ID = "id";
private static final String COLUMN_IV = "iv";
private static final String COLUMN_TYPE = "type";
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_SENT = "sent";
private static final String COLUMN_RECEIVED = "received";
private static final String COLUMN_STATUS = "status";
private static final String JOIN_TABLE_NAME = "Message_Label";
private static final String JT_COLUMN_MESSAGE = "message_id";
private static final String JT_COLUMN_LABEL = "label_id";
private static final String LBL_TABLE_NAME = "Label";
private static final String LBL_COLUMN_ID = "id";
private static final String LBL_COLUMN_LABEL = "label";
private static final String LBL_COLUMN_TYPE = "type";
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;
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) + ")");
}
public List<Label> findLabels(String where) {
List<Label> result = new LinkedList<>();
// 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
};
SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
LBL_TABLE_NAME, projection,
where,
null, null, null,
LBL_COLUMN_ORDER
);
c.moveToFirst();
while (!c.isAfterLast()) {
result.add(getLabel(c));
c.moveToNext();
}
return result;
}
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;
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,
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 where;
if (label != null) {
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND ";
} else {
where = "";
}
SQLiteDatabase db = sql.getReadableDatabase();
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
);
return c.getColumnCount();
}
@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) {
List<Plaintext> result = new LinkedList<>();
// 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_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS
};
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));
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.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.labels(findLabels(id));
result.add(builder.build());
c.moveToNext();
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
private Collection<Label> findLabels(long id) {
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
}
@Override
public void save(Plaintext message) {
SQLiteDatabase db = sql.getWritableDatabase();
try {
db.beginTransaction();
// save from address if necessary
if (message.getId() == null) {
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());
}
bmc.getAddressRepo().save(message.getFrom());
}
}
// save message
if (message.getId() == null) {
insert(db, message);
} else {
update(db, message);
}
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=" + message.getId(), null);
// save labels
ContentValues values = new ContentValues();
for (Label label : message.getLabels()) {
values.put(JT_COLUMN_LABEL, (Long) label.getId());
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
}
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 {
ContentValues values = new ContentValues();
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());
values.put(COLUMN_DATA, Encode.bytes(message));
values.put(COLUMN_SENT, message.getSent());
values.put(COLUMN_RECEIVED, message.getReceived());
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
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_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_SENT, message.getSent());
values.put(COLUMN_RECEIVED, message.getReceived());
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
}
@Override
public void remove(Plaintext message) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "id = " + message.getId(), null);
}
}

View File

@ -14,11 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.repositories; package ch.dissem.apps.abit.repository;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey; 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.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -50,6 +52,7 @@ public class AndroidAddressRepository implements AddressRepository {
private static final String COLUMN_PUBLIC_KEY = "public_key"; private static final String COLUMN_PUBLIC_KEY = "public_key";
private static final String COLUMN_PRIVATE_KEY = "private_key"; private static final String COLUMN_PRIVATE_KEY = "private_key";
private static final String COLUMN_SUBSCRIBED = "subscribed"; private static final String COLUMN_SUBSCRIBED = "subscribed";
private static final String COLUMN_CHAN = "chan";
private final SqlHelper sql; private final SqlHelper sql;
@ -86,6 +89,11 @@ public class AndroidAddressRepository implements AddressRepository {
return find("private_key IS NOT NULL"); return find("private_key IS NOT NULL");
} }
@Override
public List<BitmessageAddress> getChans() {
return find("chan = '1'");
}
@Override @Override
public List<BitmessageAddress> getSubscriptions() { public List<BitmessageAddress> getSubscriptions() {
return find("subscribed = '1'"); return find("subscribed = '1'");
@ -102,7 +110,7 @@ public class AndroidAddressRepository implements AddressRepository {
@Override @Override
public List<BitmessageAddress> getContacts() { 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) { private List<BitmessageAddress> find(String where) {
@ -111,34 +119,36 @@ public class AndroidAddressRepository implements AddressRepository {
// Define a projection that specifies which columns from the database // Define a projection that specifies which columns from the database
// you will actually use after this query. // you will actually use after this query.
String[] projection = { String[] projection = {
COLUMN_ADDRESS, COLUMN_ADDRESS,
COLUMN_ALIAS, COLUMN_ALIAS,
COLUMN_PUBLIC_KEY, COLUMN_PUBLIC_KEY,
COLUMN_PRIVATE_KEY, COLUMN_PRIVATE_KEY,
COLUMN_SUBSCRIBED COLUMN_SUBSCRIBED,
COLUMN_CHAN
}; };
try { SQLiteDatabase db = sql.getReadableDatabase();
SQLiteDatabase db = sql.getReadableDatabase(); try (Cursor c = db.query(
Cursor c = db.query( TABLE_NAME, projection,
TABLE_NAME, projection, where,
where, null, null, null, null
null, null, null, null )) {
); while (c.moveToNext()) {
c.moveToFirst();
while (!c.isAfterLast()) {
BitmessageAddress address; BitmessageAddress address;
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)); byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
if (privateKeyBytes != null) { if (privateKeyBytes != null) {
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream(privateKeyBytes)); PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream
(privateKeyBytes));
address = new BitmessageAddress(privateKey); address = new BitmessageAddress(privateKey);
} else { } else {
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))); address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY)); byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
if (publicKeyBytes != null) { if (publicKeyBytes != null) {
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(), Pubkey pubkey = Factory.readPubkey(address.getVersion(), address
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, false); .getStream(),
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length,
false);
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
pubkey = new V4Pubkey((V3Pubkey) pubkey); pubkey = new V4Pubkey((V3Pubkey) pubkey);
} }
@ -146,55 +156,60 @@ public class AndroidAddressRepository implements AddressRepository {
} }
} }
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS))); 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); address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
result.add(address); result.add(address);
c.moveToNext();
} }
} catch (IOException e) { } catch (IOException e) {
LOG.error(e.getMessage(), e); LOG.error(e.getMessage(), e);
} }
return result; return result;
} }
@Override @Override
public void save(BitmessageAddress address) { public void save(BitmessageAddress address) {
try { if (exists(address)) {
if (exists(address)) { update(address);
update(address); } else {
} else { insert(address);
insert(address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
} }
} }
private boolean exists(BitmessageAddress address) { private boolean exists(BitmessageAddress address) {
SQLiteDatabase db = sql.getReadableDatabase(); SQLiteDatabase db = sql.getReadableDatabase();
Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'", null); try (Cursor cursor = db.rawQuery(
cursor.moveToFirst(); "SELECT COUNT(*) FROM Address WHERE address=?",
return cursor.getInt(0) > 0; new String[]{address.getAddress()}
)) {
cursor.moveToFirst();
return cursor.getInt(0) > 0;
}
} }
private void update(BitmessageAddress address) throws IOException { private void update(BitmessageAddress address) {
try { try {
SQLiteDatabase db = sql.getWritableDatabase(); SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys // Create a new map of values, where column names are the keys
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COLUMN_ALIAS, address.getAlias()); if (address.getAlias() != null) {
values.put(COLUMN_ALIAS, address.getAlias());
}
if (address.getPubkey() != null) { if (address.getPubkey() != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
address.getPubkey().writeUnencrypted(out); address.getPubkey().writeUnencrypted(out);
values.put(COLUMN_PUBLIC_KEY, out.toByteArray()); values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
} 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()));
}
if (address.isChan()) {
values.put(COLUMN_CHAN, true);
}
values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); 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) { if (update < 0) {
LOG.error("Could not update address " + address); LOG.error("Could not update address " + address);
} }
@ -203,7 +218,7 @@ public class AndroidAddressRepository implements AddressRepository {
} }
} }
private void insert(BitmessageAddress address) throws IOException { private void insert(BitmessageAddress address) {
try { try {
SQLiteDatabase db = sql.getWritableDatabase(); SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys // Create a new map of values, where column names are the keys
@ -219,6 +234,7 @@ public class AndroidAddressRepository implements AddressRepository {
values.put(COLUMN_PUBLIC_KEY, (byte[]) null); values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
} }
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
values.put(COLUMN_CHAN, address.isChan());
values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
long insert = db.insert(TABLE_NAME, null, values); long insert = db.insert(TABLE_NAME, null, values);
@ -233,7 +249,7 @@ public class AndroidAddressRepository implements AddressRepository {
@Override @Override
public void remove(BitmessageAddress address) { public void remove(BitmessageAddress address) {
SQLiteDatabase db = sql.getWritableDatabase(); SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "address = " + address.getAddress(), null); db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()});
} }
@Override @Override

View File

@ -14,13 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.repositories; package ch.dissem.apps.abit.repository;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
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.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
@ -28,16 +38,10 @@ import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.Inventory; import ch.dissem.bitmessage.ports.Inventory;
import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger; import static ch.dissem.apps.abit.repository.SqlHelper.join;
import org.slf4j.LoggerFactory; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import java.io.ByteArrayInputStream;
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.bitmessage.utils.UnixTime.now; import static ch.dissem.bitmessage.utils.UnixTime.now;
import static java.lang.String.valueOf;
/** /**
* {@link Inventory} implementation using the Android SQL API. * {@link Inventory} implementation using the Android SQL API.
@ -55,41 +59,63 @@ public class AndroidInventory implements Inventory {
private final SqlHelper sql; private final SqlHelper sql;
private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>();
public AndroidInventory(SqlHelper sql) { public AndroidInventory(SqlHelper sql) {
this.sql = sql; this.sql = sql;
} }
@Override @Override
public List<InventoryVector> getInventory(long... streams) { 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) { private Map<InventoryVector, Long> getCache(long stream) {
// Define a projection that specifies which columns from the database Map<InventoryVector, Long> result = cache.get(stream);
// you will actually use after this query. if (result == null) {
String[] projection = { synchronized (cache) {
COLUMN_HASH if (cache.get(stream) == null) {
}; result = new ConcurrentHashMap<>();
cache.put(stream, result);
SQLiteDatabase db = sql.getReadableDatabase(); String[] projection = {
Cursor c = db.query( COLUMN_HASH, COLUMN_EXPIRES
TABLE_NAME, projection, };
(includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")",
null, null, null, null SQLiteDatabase db = sql.getReadableDatabase();
); try (Cursor c = db.query(
c.moveToFirst(); TABLE_NAME, projection,
List<InventoryVector> result = new LinkedList<>(); "stream = " + stream,
while (!c.isAfterLast()) { null, null, null, null
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); )) {
result.add(new InventoryVector(blob)); while (c.moveToNext()) {
c.moveToNext(); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES));
result.put(new InventoryVector(blob), expires);
}
}
LOG.info("Stream #" + stream + " inventory size: " + result.size());
}
}
} }
return result; return result;
} }
@Override @Override
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { 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; return offer;
} }
@ -98,25 +124,25 @@ public class AndroidInventory implements Inventory {
// Define a projection that specifies which columns from the database // Define a projection that specifies which columns from the database
// you will actually use after this query. // you will actually use after this query.
String[] projection = { String[] projection = {
COLUMN_VERSION, COLUMN_VERSION,
COLUMN_DATA COLUMN_DATA
}; };
SQLiteDatabase db = sql.getReadableDatabase(); SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query( try (Cursor c = db.query(
TABLE_NAME, projection, TABLE_NAME, projection,
"hash = X'" + vector + "'", "hash = X'" + vector + "'",
null, null, null, null null, null, null, null
); )) {
c.moveToFirst(); if (!c.moveToFirst()) {
if (c.isAfterLast()) { LOG.info("Object requested that we don't have. IV: " + vector);
LOG.info("Object requested that we don't have. IV: " + vector); return null;
return null; }
}
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length); return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
}
} }
@Override @Override
@ -124,8 +150,8 @@ public class AndroidInventory implements Inventory {
// Define a projection that specifies which columns from the database // Define a projection that specifies which columns from the database
// you will actually use after this query. // you will actually use after this query.
String[] projection = { String[] projection = {
COLUMN_VERSION, COLUMN_VERSION,
COLUMN_DATA COLUMN_DATA
}; };
StringBuilder where = new StringBuilder("1=1"); StringBuilder where = new StringBuilder("1=1");
if (stream > 0) { if (stream > 0) {
@ -139,18 +165,18 @@ public class AndroidInventory implements Inventory {
} }
SQLiteDatabase db = sql.getReadableDatabase(); SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query(
TABLE_NAME, projection,
where.toString(),
null, null, null, null
);
c.moveToFirst();
List<ObjectMessage> result = new LinkedList<>(); List<ObjectMessage> result = new LinkedList<>();
while (!c.isAfterLast()) { try (Cursor c = db.query(
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); TABLE_NAME, projection,
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); where.toString(),
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), blob.length)); null, null, null, null
c.moveToNext(); )) {
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));
}
} }
return result; return result;
} }
@ -158,6 +184,10 @@ public class AndroidInventory implements Inventory {
@Override @Override
public void storeObject(ObjectMessage object) { public void storeObject(ObjectMessage object) {
InventoryVector iv = object.getInventoryVector(); InventoryVector iv = object.getInventoryVector();
if (getCache(object.getStream()).containsKey(iv))
return;
LOG.trace("Storing object " + iv); LOG.trace("Storing object " + iv);
try { try {
@ -172,27 +202,31 @@ public class AndroidInventory implements Inventory {
values.put(COLUMN_VERSION, object.getVersion()); values.put(COLUMN_VERSION, object.getVersion());
db.insertOrThrow(TABLE_NAME, null, values); db.insertOrThrow(TABLE_NAME, null, values);
getCache(object.getStream()).put(iv, object.getExpiresTime());
} catch (SQLiteConstraintException e) { } catch (SQLiteConstraintException e) {
LOG.trace(e.getMessage(), e); LOG.trace(e.getMessage(), e);
} catch (IOException e) {
LOG.error(e.getMessage(), e);
} }
} }
@Override @Override
public boolean contains(ObjectMessage object) { public boolean contains(ObjectMessage object) {
SQLiteDatabase db = sql.getReadableDatabase(); return getCache(object.getStream()).keySet().contains(object.getInventoryVector());
Cursor c = db.query(
TABLE_NAME, new String[]{COLUMN_STREAM},
"hash = X'" + object.getInventoryVector() + "'",
null, null, null, null
);
return c.getColumnCount() > 0;
} }
@Override @Override
public void cleanup() { public void cleanup() {
long fiveMinutesAgo = now() - 5 * MINUTE;
SQLiteDatabase db = sql.getWritableDatabase(); SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "expires < " + (now() - 300), 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();
while (iterator.hasNext()) {
if (iterator.next().getValue() < fiveMinutesAgo) {
iterator.remove();
}
}
}
} }
} }

View File

@ -0,0 +1,386 @@
/*
* 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.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
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.util.Labels;
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;
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;
/**
* {@link MessageRepository} implementation using the Android SQL API.
*/
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";
private static final String COLUMN_TYPE = "type";
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 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";
private static final String JT_COLUMN_LABEL = "label_id";
private static final String LBL_TABLE_NAME = "Label";
private static final String LBL_COLUMN_ID = "id";
private static final String LBL_COLUMN_LABEL = "label";
private static final String LBL_COLUMN_TYPE = "type";
private static final String LBL_COLUMN_COLOR = "color";
private static final String LBL_COLUMN_ORDER = "ord";
private final SqlHelper sql;
private final Context context;
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
this.sql = sql;
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<>();
// 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
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
LBL_TABLE_NAME, projection,
where,
null, null, null,
LBL_COLUMN_ORDER
)) {
while (c.moveToNext()) {
result.add(getLabel(c));
}
}
return result;
}
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 = Labels.getText(type, null, context);
if (text == null) {
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
}
Label label = new Label(
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) {
return 0;
}
if (label == LABEL_ARCHIVE) {
where = "";
args = new String[]{
Label.Type.UNREAD.name()
};
} else {
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND ";
args = new String[]{
label.getId().toString(),
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=?))",
args
);
}
@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;
}
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()) {
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<>();
// 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_CONVERSATION
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null,
COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC"
)) {
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)));
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(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex
(COLUMN_SENDER))));
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.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))));
builder.labels(findLabels(id));
result.add(builder.build());
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
private Collection<Label> findLabels(long id) {
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
}
@Override
public void save(Plaintext message) {
saveContactIfNecessary(message.getFrom());
saveContactIfNecessary(message.getTo());
SQLiteDatabase db = sql.getWritableDatabase();
try {
db.beginTransaction();
// save message
if (message.getId() == null) {
insert(db, message);
} else {
update(db, message);
}
updateParents(db, message);
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())});
// save labels
ContentValues values = new ContentValues();
for (Label label : message.getLabels()) {
values.put(JT_COLUMN_LABEL, (Long) label.getId());
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
} catch (SQLiteConstraintException e) {
LOG.trace(e.getMessage(), e);
} finally {
db.endTransaction();
}
}
private void insert(SQLiteDatabase db, Plaintext message) {
ContentValues values = new ContentValues();
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());
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());
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
long id = db.insertOrThrow(TABLE_NAME, null, values);
message.setId(id);
}
private void update(SQLiteDatabase db, Plaintext message) {
ContentValues values = new ContentValues();
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());
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());
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
}
@Override
public void remove(Plaintext message) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "id = " + message.getId(), null);
}
}

View File

@ -0,0 +1,194 @@
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.ArrayList;
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))});
}
@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) {
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);
}
}
}

View File

@ -0,0 +1,172 @@
/*
* 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;
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.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;
import ch.dissem.bitmessage.utils.Encode;
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 {
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 static final String COLUMN_EXPIRATION_TIME = "expiration_time";
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
// 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
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
"initial_hash=X'" + hex(initialHash) + "'",
null, null, null, null
)) {
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))
);
} 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: " +
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();
List<byte[]> result = new LinkedList<>();
try (Cursor c = db.query(
TABLE_NAME, projection,
null, null, null, null, null
)) {
while (c.moveToNext()) {
byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH));
result.add(initialHash);
}
}
return result;
}
@Override
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, 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) {
LOG.trace(e.getMessage(), e);
}
}
@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();
db.delete(
TABLE_NAME,
"initial_hash=X'" + hex(initialHash) + "'",
null
);
}
}

View File

@ -14,22 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.repositories; package ch.dissem.apps.abit.repository;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; 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. * Handles database migration and provides access.
*/ */
public class SqlHelper extends SQLiteOpenHelper { public class SqlHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version. // If you change the database schema, you must increment the database version.
public static final int DATABASE_VERSION = 1; private static final int DATABASE_VERSION = 7;
public static final String DATABASE_NAME = "jabit.db"; private static final String DATABASE_NAME = "jabit.db";
protected final Context ctx; private final Context ctx;
public SqlHelper(Context ctx) { public SqlHelper(Context ctx) {
super(ctx, DATABASE_NAME, null, DATABASE_VERSION); super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
@ -38,7 +39,7 @@ public class SqlHelper extends SQLiteOpenHelper {
@Override @Override
public void onCreate(SQLiteDatabase db) { public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, 1); onUpgrade(db, 0, DATABASE_VERSION);
} }
@Override @Override
@ -48,18 +49,33 @@ public class SqlHelper extends SQLiteOpenHelper {
executeMigration(db, "V1.0__Create_table_inventory"); executeMigration(db, "V1.0__Create_table_inventory");
executeMigration(db, "V1.1__Create_table_address"); executeMigration(db, "V1.1__Create_table_address");
executeMigration(db, "V1.2__Create_table_message"); executeMigration(db, "V1.2__Create_table_message");
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");
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");
case 5:
executeMigration(db, "V3.4__Add_label_outbox");
case 6:
executeMigration(db, "V4.0__Create_table_message_parent");
default: 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.
} }
} }
protected void executeMigration(SQLiteDatabase db, String name) { private void executeMigration(SQLiteDatabase db, String name) {
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) { for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
db.execSQL(statement); db.execSQL(statement);
} }
} }
public static StringBuilder join(long... numbers) { static StringBuilder join(long... numbers) {
StringBuilder streamList = new StringBuilder(); StringBuilder streamList = new StringBuilder();
for (int i = 0; i < numbers.length; i++) { for (int i = 0; i < numbers.length; i++) {
if (i > 0) streamList.append(", "); if (i > 0) streamList.append(", ");
@ -68,7 +84,7 @@ public class SqlHelper extends SQLiteOpenHelper {
return streamList; return streamList;
} }
public static StringBuilder join(Enum<?>... types) { static StringBuilder join(Enum<?>... types) {
StringBuilder streamList = new StringBuilder(); StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) { for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", "); if (i > 0) streamList.append(", ");

View File

@ -0,0 +1,73 @@
/*
* 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);
Singleton.getMessageListener(this).resetNotification();
}
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));
}
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.Intent;
import android.os.Handler;
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.NETWORK_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 {
private static BitmessageContext bmc = null;
private static volatile boolean running = false;
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();
}
@Override
public void onCreate() {
if (bmc == null) {
bmc = Singleton.getBitmessageContext(this);
}
notification = new NetworkNotification(this);
running = false;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!isRunning()) {
running = true;
notification.connecting();
startForeground(NETWORK_NOTIFICATION_ID, notification.getNotification());
if (!bmc.isRunning()) {
bmc.startup();
}
notification.show();
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L);
}
return Service.START_STICKY;
}
@Override
public void onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown();
}
running = false;
notification.showShutdown();
cleanupHandler.removeCallbacks(cleanupTask);
bmc.cleanup();
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
public static Property getStatus() {
if (bmc != null) {
return bmc.status();
} else {
return new Property("bitmessage context", null);
}
}
}

View File

@ -0,0 +1,119 @@
/*
* 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.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import java.util.LinkedList;
import java.util.Queue;
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 {
// Object to use as a thread-safe lock
private static final ProofOfWorkEngine engine = new MultiThreadedPOWEngine();
private static final Queue<PowItem> queue = new LinkedList<>();
private static boolean calculating;
private ProofOfWorkNotification notification;
@Override
public void onCreate() {
notification = new ProofOfWorkNotification(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new PowBinder(this);
}
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;
}
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();
}
}
}
}
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;
}
}
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 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);
}
}
}
});
}
}

View File

@ -0,0 +1,78 @@
/*
* 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;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
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;
/**
* Proof of Work engine that uses the Proof of Work service.
*/
public class ServicePowEngine implements ProofOfWorkEngine {
private final Context ctx;
private static final Object lock = new Object();
private final Queue<PowItem> queue = new LinkedList<>();
private PowBinder service;
public ServicePowEngine(Context ctx) {
this.ctx = ctx;
}
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (lock) {
ServicePowEngine.this.service = (PowBinder) service;
while (!queue.isEmpty()) {
ServicePowEngine.this.service.process(queue.poll());
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
}
};
@Override
public void calculateNonce(byte[] initialHash, byte[] targetValue, Callback callback) {
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);
}
}
}
}

View File

@ -1,24 +1,62 @@
/*
* 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; package ch.dissem.apps.abit.service;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
import ch.dissem.apps.abit.listeners.MessageListener; import java.util.List;
import ch.dissem.apps.abit.repositories.AndroidAddressRepository;
import ch.dissem.apps.abit.repositories.AndroidInventory; import ch.dissem.apps.abit.MainActivity;
import ch.dissem.apps.abit.repositories.AndroidMessageRepository; import ch.dissem.apps.abit.R;
import ch.dissem.apps.abit.repositories.SqlHelper; 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;
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.BitmessageContext;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler; import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.security.sc.SpongySecurity; 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;
/** /**
* Provides singleton objects across the application. * Provides singleton objects across the application.
*/ */
public class Singleton { public class Singleton {
private static BitmessageContext bitmessageContext; private static BitmessageContext bitmessageContext;
private static ConversationService conversationService;
private static MessageListener messageListener; private static MessageListener messageListener;
private static BitmessageAddress identity;
private static AndroidProofOfWorkRepository powRepo;
private static boolean creatingIdentity;
public static BitmessageContext getBitmessageContext(Context context) { public static BitmessageContext getBitmessageContext(Context context) {
if (bitmessageContext == null) { if (bitmessageContext == null) {
@ -26,15 +64,24 @@ public class Singleton {
if (bitmessageContext == null) { if (bitmessageContext == null) {
final Context ctx = context.getApplicationContext(); final Context ctx = context.getApplicationContext();
SqlHelper sqlHelper = new SqlHelper(ctx); SqlHelper sqlHelper = new SqlHelper(ctx);
powRepo = new AndroidProofOfWorkRepository(sqlHelper);
TTL.pubkey(2 * DAY);
bitmessageContext = new BitmessageContext.Builder() bitmessageContext = new BitmessageContext.Builder()
.security(new SpongySecurity()) .proofOfWorkEngine(new SwitchingProofOfWorkEngine(
.nodeRegistry(new MemoryNodeRegistry()) ctx, Constants.PREFERENCE_SERVER_POW,
.inventory(new AndroidInventory(sqlHelper)) new ServerPowEngine(ctx),
.addressRepo(new AndroidAddressRepository(sqlHelper)) new ServicePowEngine(ctx)
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) ))
.networkHandler(new DefaultNetworkHandler()) .cryptography(new AndroidCryptography())
.listener(getMessageListener(ctx)) .nodeRegistry(new AndroidNodeRegistry(sqlHelper))
.build(); .inventory(new AndroidInventory(sqlHelper))
.addressRepo(new AndroidAddressRepository(sqlHelper))
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
.powRepo(powRepo)
.networkHandler(new NioNetworkHandler())
.listener(getMessageListener(ctx))
.doNotSendPubkeyOnIdentityCreation()
.build();
} }
} }
} }
@ -51,4 +98,80 @@ public class Singleton {
} }
return messageListener; return messageListener;
} }
public static MessageRepository getMessageRepository(Context ctx) {
return getBitmessageContext(ctx).messages();
}
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(final Context ctx) {
if (identity == null) {
final BitmessageContext bmc = getBitmessageContext(ctx);
synchronized (Singleton.class) {
if (identity == null) {
List<BitmessageAddress> identities = bmc.addresses()
.getIdentities();
if (identities.size() > 0) {
identity = identities.get(0);
} else {
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;
}
}
}
}
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;
}
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;
}
} }

View File

@ -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.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 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) {
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();
}
}

View File

@ -0,0 +1,47 @@
/*
* 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;
/**
* 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();
}
}

View File

@ -0,0 +1,89 @@
/*
* 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
* 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(@NonNull Uri uri) {
return null;
}
/*
* query() always returns no results
*
*/
@Override
public Cursor query(
@NonNull Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
return null;
}
/*
* insert() always returns null (no URI)
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
/*
* delete() always returns "no rows affected" (0)
*/
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
/*
* update() always returns "no rows affected" (0)
*/
public int update(
@NonNull Uri uri,
ContentValues values,
String selection,
String[] selectionArgs) {
return 0;
}
}

View File

@ -0,0 +1,189 @@
/*
* 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;
import android.accounts.AccountManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
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;
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;
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.cryptography;
/**
* 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 static final long SYNC_FREQUENCY = 15 * 60; // seconds
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) {
try {
if (account.equals(Authenticator.ACCOUNT_SYNC)) {
if (Preferences.isConnectionAllowed(getContext())) {
syncData();
}
} else if (account.equals(Authenticator.ACCOUNT_POW)) {
syncPOW();
} else {
syncResult.stats.numAuthExceptions++;
}
} catch (IOException e) {
syncResult.stats.numIoExceptions++;
} catch (DecryptionFailedException e) {
syncResult.stats.numAuthExceptions++;
}
}
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");
return;
}
LOG.info("Synchronizing Bitmessage");
LOG.info("Synchronization started");
bmc.synchronize(
Preferences.getTrustedNode(getContext()),
Preferences.getTrustedNodePort(getContext()),
Preferences.getTimeoutInSeconds(getContext()),
true);
LOG.info("Synchronization finished");
}
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");
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");
}
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;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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;
/**
* 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(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();
}
}

View File

@ -14,9 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.apps.abit.utils; package ch.dissem.apps.abit.util;
import android.content.Context; import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -24,19 +26,13 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.entity.Plaintext;
/** /**
* Helper class to work with Assets. * Helper class to work with Assets.
*/ */
public class 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) { public static List<String> readSqlStatements(Context ctx, String name) {
try { try {
InputStream in = ctx.getAssets().open(name); InputStream in = ctx.getAssets().open(name);
@ -53,4 +49,44 @@ public class Assets {
throw new RuntimeException(e); 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;
}
}
} }

View File

@ -0,0 +1,32 @@
/*
* 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 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");
}

View File

@ -0,0 +1,112 @@
/*
* 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.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.NO_WRAP;
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());
return item;
}
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;
}
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 | NO_WRAP));
}
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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,344 @@
/*
* 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.
*/
package ch.dissem.apps.abit.util;
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, (Object) 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}).
*/
@SuppressWarnings("JavaDoc")
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();
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
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");
}
}
}

View File

@ -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.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.io.IOException;
import java.net.InetAddress;
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_SYNC_TIMEOUT;
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE;
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY;
/**
* @author Christian Basler
*/
public class Preferences {
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) throws IOException {
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);
}
return InetAddress.getByName(trustedNode);
}
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);
}
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.isConnectedToMeteredNetwork(ctx);
}
public static boolean isWifiOnly(Context ctx) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true);
}
}

View File

@ -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(" ");
}
}

View File

@ -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();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

Some files were not shown because too many files have changed in this diff Show More