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 {

View File

@ -1,21 +1,28 @@
<?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"/>
@ -26,28 +33,32 @@
<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
android:name=".dialog.FullNodeDialogActivity"
android:label="@string/full_node"
android:theme="@style/Theme.AppCompat.Light.Dialog"/>
<activity <activity
android:name=".ComposeMessageActivity" android:name=".ComposeMessageActivity"
android:label="Compose" android:label="@string/compose_message"
android:parentActivityName=".MessageListActivity"> 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"/>
@ -76,16 +87,15 @@
<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>
@ -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 =
new ListSelectionListener<Object>() {
@Override @Override
public void onItemSelected(Object plaintext) { 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) {
@ -76,15 +67,28 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
} }
@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,12 +123,16 @@ 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) {
this.activateOnItemClick = activateOnItemClick;
if (isVisible()) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically // When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched. // give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick 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) {
if (position == ListView.INVALID_POSITION) { if (position == ListView.INVALID_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) {
@ -80,34 +89,78 @@ public class SubscriptionListFragment extends AbstractItemListFragment<Bitmessag
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);
@ -31,4 +62,44 @@ public class ComposeMessageActivity extends AppCompatActivity {
.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);
recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient);
if (broadcast) {
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) { if (recipient != null) {
EditText recipientInput = (EditText) rootView.findViewById(R.id.recipient);
recipientInput.setText(recipient.toString()); recipientInput.setText(recipient.toString());
} }
EditText body = (EditText) rootView.findViewById(R.id.body); }
body.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); subjectInput = (EditText) rootView.findViewById(R.id.subject);
body.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); subjectInput.setText(subject);
bodyInput = (EditText) rootView.findViewById(R.id.body);
bodyInput.setText(content);
if (recipient == null) {
recipientInput.requestFocus();
} else if (subject == null || subject.isEmpty()) {
subjectInput.requestFocus();
} else {
bodyInput.requestFocus();
bodyInput.setSelection(0);
}
return rootView; 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,6 +34,7 @@ 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();
@ -55,18 +50,14 @@ public class MessageDetailActivity extends AppCompatActivity {
@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:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpTo(this, new Intent(this, MessageListActivity.class));
return true; return true;
} default:
return super.onOptionsItemSelected(item); 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,16 +107,38 @@ 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));
TextView messageBody = (TextView) rootView.findViewById(R.id.text);
messageBody.setText(item.getText());
Linkify.addLinks(messageBody, WEB_URLS);
Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher match, String url) {
return match.group();
} }
}
);
messageBody.setLinksClickable(true);
messageBody.setTextIsSelectable(true);
boolean removed = false; boolean removed = false;
Iterator<Label> labels = item.getLabels().iterator(); Iterator<Label> labels = item.getLabels().iterator();
@ -83,19 +148,41 @@ public class MessageDetailFragment extends Fragment {
removed = true; 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) {
if (!isResumed()) {
currentLabel = label; currentLabel = label;
setListAdapter(new ArrayAdapter<Plaintext>( return;
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
bmc.messages().findMessages(label)) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.message_row, null, false);
} }
Plaintext item = getItem(position);
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item.getFrom())); if (!Objects.equals(currentLabel, label)) {
TextView sender = (TextView) convertView.findViewById(R.id.sender); adapter.setData(label, Collections.<Plaintext>emptyList());
sender.setText(item.getFrom().toString()); adapter.notifyDataSetChanged();
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; doUpdateList(label);
} }
});
private void doUpdateList(final Label label) {
if (label == null) {
if (getActivity() instanceof ActionBarListener) { if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateTitle(label.toString()); ((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name));
} }
adapter.setData(null, Collections.<Plaintext>emptyList());
adapter.notifyDataSetChanged();
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 @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected void onPostExecute(List<Plaintext> messages) {
if (adapter != null) {
adapter.setData(label, messages);
adapter.notifyDataSetChanged();
}
}
}.execute();
}
@Override
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)) {
messageRepo.remove(message);
} }
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
updateList(currentLabel); 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,23 +1,14 @@
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()

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) {
@ -115,30 +123,32 @@ public class AndroidAddressRepository implements AddressRepository {
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();
Cursor c = db.query( try (Cursor c = db.query(
TABLE_NAME, projection, TABLE_NAME, projection,
where, where,
null, null, null, null null, null, null, null
); )) {
c.moveToFirst(); while (c.moveToNext()) {
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(
"SELECT COUNT(*) FROM Address WHERE address=?",
new String[]{address.getAddress()}
)) {
cursor.moveToFirst(); cursor.moveToFirst();
return cursor.getInt(0) > 0; 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();
if (address.getAlias() != null) {
values.put(COLUMN_ALIAS, address.getAlias()); 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);
} }
if (address.getPrivateKey() != null) {
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); 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) {
synchronized (cache) {
if (cache.get(stream) == null) {
result = new ConcurrentHashMap<>();
cache.put(stream, result);
String[] projection = { String[] projection = {
COLUMN_HASH COLUMN_HASH, COLUMN_EXPIRES
}; };
SQLiteDatabase db = sql.getReadableDatabase(); SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query( try (Cursor c = db.query(
TABLE_NAME, projection, TABLE_NAME, projection,
(includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")", "stream = " + stream,
null, null, null, null null, null, null, null
); )) {
c.moveToFirst(); while (c.moveToNext()) {
List<InventoryVector> result = new LinkedList<>();
while (!c.isAfterLast()) {
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
result.add(new InventoryVector(blob)); long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES));
c.moveToNext(); 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;
} }
@ -103,13 +129,12 @@ public class AndroidInventory implements Inventory {
}; };
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;
} }
@ -118,6 +143,7 @@ public class AndroidInventory implements Inventory {
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
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
@ -139,18 +165,18 @@ public class AndroidInventory implements Inventory {
} }
SQLiteDatabase db = sql.getReadableDatabase(); SQLiteDatabase db = sql.getReadableDatabase();
Cursor c = db.query( List<ObjectMessage> result = new LinkedList<>();
try (Cursor c = db.query(
TABLE_NAME, projection, TABLE_NAME, projection,
where.toString(), where.toString(),
null, null, null, null null, null, null, null
); )) {
c.moveToFirst(); while (c.moveToNext()) {
List<ObjectMessage> result = new LinkedList<>();
while (!c.isAfterLast()) {
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), blob.length)); result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob),
c.moveToNext(); 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,14 +64,23 @@ 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,
new ServerPowEngine(ctx),
new ServicePowEngine(ctx)
))
.cryptography(new AndroidCryptography())
.nodeRegistry(new AndroidNodeRegistry(sqlHelper))
.inventory(new AndroidInventory(sqlHelper)) .inventory(new AndroidInventory(sqlHelper))
.addressRepo(new AndroidAddressRepository(sqlHelper)) .addressRepo(new AndroidAddressRepository(sqlHelper))
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) .messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
.networkHandler(new DefaultNetworkHandler()) .powRepo(powRepo)
.networkHandler(new NioNetworkHandler())
.listener(getMessageListener(ctx)) .listener(getMessageListener(ctx))
.doNotSendPubkeyOnIdentityCreation()
.build(); .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