Merge branch 'release/1.0-beta18'

This commit is contained in:
Christian Basler 2018-01-18 17:25:33 +01:00
commit 396f1a23a6
190 changed files with 9223 additions and 8103 deletions

View File

@ -1,32 +1,36 @@
apply plugin: 'idea'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'idea'
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";
apply from: project.property("project.configs") + appName + ".gradle"
}
//noinspection GroovyMissingReturnStatement
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion 27
buildToolsVersion "26.0.2"
defaultConfig {
applicationId "ch.dissem.apps." + appName.toLowerCase()
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19
targetSdkVersion 25
versionCode 12
versionName "1.0-beta12"
jackOptions.enabled = false
targetSdkVersion 27
versionCode 18
versionName "1.0-beta18"
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
lintOptions {
abortOnError false
}
buildTypes {
release {
minifyEnabled false
@ -35,60 +39,77 @@ android {
signingConfig signingConfigs.release
}
}
packagingOptions {
exclude 'META-INF/core.kotlin_module'
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
//ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT'
ext.supportVersion = '25.3.1'
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
ext.supportVersion = '27.0.2'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version"
compile "com.android.support:appcompat-v7:$supportVersion"
compile "com.android.support:support-v4:$supportVersion"
compile "com.android.support:design:$supportVersion"
compile "com.android.support:multidex:1.0.1"
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:preference-v7:$supportVersion"
implementation "com.android.support:cardview-v7:$supportVersion"
implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:multidex:1.0.2"
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"
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
compile 'org.slf4j:slf4j-android:1.7.25'
implementation 'org.slf4j:slf4j-android:1.7.25'
compile 'com.mikepenz:materialize:1.0.1@aar'
compile('com.mikepenz:materialdrawer:5.9.0@aar') {
implementation 'com.mikepenz:materialize:1.1.2@aar'
implementation('com.mikepenz:materialdrawer:6.0.2@aar') {
transitive = true
}
compile('com.mikepenz:aboutlibraries:5.9.5@aar') {
implementation('com.mikepenz:aboutlibraries:6.0.2@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'
implementation "com.mikepenz:iconics-core:3.0.0@aar"
implementation "com.mikepenz:iconics-views:3.0.0@aar"
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
compile 'com.google.zxing:core:3.3.0'
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
implementation 'com.google.zxing:core:3.3.1'
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') {
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8'
implementation 'com.github.amlcurran.showcaseview:library:5.4.3'
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
transitive = true
}
compile 'com.github.angads25:filepicker:1.1.0'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.github.angads25:filepicker:1.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.7.22'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.13.0'
testImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.6.1'
testImplementation "org.robolectric:shadows-multidex:3.6.1"
androidTestImplementation "com.android.support:multidex:1.0.2"
}
idea.module {
downloadJavadoc = true
downloadSources = true
}
android {
lintOptions {
abortOnError false
}
}

View File

@ -4,9 +4,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
@ -19,8 +19,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="android.support.multidex.MultiDexApplication"
tools:replace="android:allowBackup">
android:name="android.support.multidex.MultiDexApplication">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
@ -84,16 +83,6 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".CreateAddressActivity"
android:label="@string/title_activity_open_bitmessage_link"
@ -144,6 +133,17 @@
android:exported="false"
android:syncable="true"/>
<!-- Exports -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="ch.dissem.apps.abit.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".synchronization.AuthenticatorService"
android:exported="true"
@ -173,19 +173,28 @@
android:exported="false"/>
<!-- Receive Wi-Fi connection state changes -->
<receiver android:name=".listener.WifiReceiver">
<receiver android:name=".listener.WifiReceiver" android:enabled="@bool/is_pre_api_21">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
<receiver android:name=".service.StartServiceReceiver" android:enabled="@bool/is_post_api_21">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".service.StartupNodeOnWifiService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<activity
android:name=".StatusActivity"
android:label="@string/title_activity_status"
android:parentActivityName=".SettingsActivity">
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".SettingsActivity"/>
android:value=".MainActivity"/>
</activity>
</application>

View File

@ -1,146 +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.content.Context;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.View;
import android.widget.ListView;
import ch.dissem.apps.abit.listener.ListSelectionListener;
/**
* @author Christian Basler
*/
public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
private static final String STATE_ACTIVATED_POSITION = "activated_position";
/**
* A dummy implementation of the {@link ListSelectionListener} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static final ListSelectionListener<Object> dummyCallbacks =
new ListSelectionListener<Object>() {
@Override
public void onItemSelected(Object item) {
// NO OP
}
};
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
private ListSelectionListener<? super T> callbacks = dummyCallbacks;
/**
* The current activated item position. Only used on tablets.
*/
private int activatedPosition = ListView.INVALID_POSITION;
private boolean activateOnItemClick;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore the previously serialized activated item position.
if (savedInstanceState != null
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
}
}
@Override
public void onResume() {
super.onResume();
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
// Activities containing this fragment must implement its callbacks.
if (context instanceof ListSelectionListener) {
//noinspection unchecked
callbacks = (ListSelectionListener) context;
} else {
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
}
@Override
public void onDetach() {
super.onDetach();
// Reset the active callbacks interface to the dummy implementation.
callbacks = dummyCallbacks;
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
//noinspection unchecked
callbacks.onItemSelected((T) listView.getItemAtPosition(position));
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (activatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition);
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
public void setActivateOnItemClick(boolean activateOnItemClick) {
this.activateOnItemClick = activateOnItemClick;
if (isVisible()) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE);
}
}
private void setActivatedPosition(int position) {
if (position == ListView.INVALID_POSITION) {
getListView().setItemChecked(activatedPosition, false);
} else {
getListView().setItemChecked(position, true);
}
activatedPosition = position;
}
}

View File

@ -0,0 +1,147 @@
/*
* 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.content.Context
import android.os.Bundle
import android.support.v4.app.ListFragment
import android.view.View
import android.widget.ListView
import ch.dissem.apps.abit.listener.ListSelectionListener
/**
* @author Christian Basler
*/
abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> {
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
@Suppress("UNCHECKED_CAST")
private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T>
/**
* The current activated item position. Only used on tablets.
*/
private var activatedPosition = ListView.INVALID_POSITION
private var activateOnItemClick: Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Restore the previously serialized activated item position.
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION))
}
}
override fun onResume() {
super.onResume()
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
listView.choiceMode = if (activateOnItemClick)
ListView.CHOICE_MODE_SINGLE
else
ListView.CHOICE_MODE_NONE
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// Activities containing this fragment must implement its callbacks.
if (context is ListSelectionListener<*>) {
@Suppress("UNCHECKED_CAST")
callbacks = context as ListSelectionListener<T>
} else {
throw IllegalStateException("Activity must implement fragment's callbacks.")
}
}
override fun onDetach() {
super.onDetach()
// Reset the active callbacks interface to the dummy implementation.
@Suppress("UNCHECKED_CAST")
callbacks = DummyCallback as ListSelectionListener<T>
}
override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) {
super.onListItemClick(listView, view, position, id)
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
@Suppress("UNCHECKED_CAST")
(listView.getItemAtPosition(position) as? T)?.let {
callbacks.onItemSelected(it)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (activatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition)
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
if (isVisible) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
listView.choiceMode = if (activateOnItemClick)
ListView.CHOICE_MODE_SINGLE
else
ListView.CHOICE_MODE_NONE
}
}
private fun setActivatedPosition(position: Int) {
if (position == ListView.INVALID_POSITION) {
listView.setItemChecked(activatedPosition, false)
} else {
listView.setItemChecked(position, true)
}
activatedPosition = position
}
override fun showPreviousList() = false
/**
* A dummy implementation of the [ListSelectionListener] interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
internal object DummyCallback : ListSelectionListener<Any> {
override fun onItemSelected(item: Any) = Unit // NO OP
}
companion object {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
internal const val STATE_ACTIVATED_POSITION = "activated_position"
}
}

View File

@ -14,25 +14,25 @@
* limitations under the License.
*/
package ch.dissem.apps.abit;
package ch.dissem.apps.abit
import android.os.Bundle;
import android.os.Bundle
/**
* An activity representing a single Subscription detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a {@link MainActivity}.
* <p/>
* in a [MainActivity].
*
*
* This activity is mostly just a 'shell' activity containing nothing
* more than a {@link AddressDetailFragment}.
* more than a [AddressDetailFragment].
*/
public class AddressDetailActivity extends DetailActivity {
class AddressDetailActivity : DetailActivity() {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
@ -42,18 +42,18 @@ public class AddressDetailActivity extends DetailActivity {
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
val arguments = Bundle()
arguments.putSerializable(AddressDetailFragment.ARG_ITEM,
getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM));
AddressDetailFragment fragment = new AddressDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM))
val fragment = AddressDetailFragment()
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.add(R.id.content, fragment)
.commit();
.commit()
}
}
}

View File

@ -1,263 +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.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

@ -0,0 +1,210 @@
/*
* 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.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.Toast
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.wif.WifExporter
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import kotlinx.android.synthetic.main.fragment_address_detail.*
/**
* A fragment representing a single Message detail screen.
* This fragment is either contained in a [MainActivity]
* in two-pane mode (on tablets) or a [MessageDetailActivity]
* on handsets.
*/
class AddressDetailFragment : Fragment() {
/**
* The content this fragment is presenting.
*/
private var item: BitmessageAddress? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
if (arguments.containsKey(ARG_ITEM)) {
item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress
}
}
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.address, menu)
val ctx = activity!!
Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail)
Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share)
Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val item = item ?: return false
val ctx = activity ?: return false
when (menuItem.itemId) {
R.id.write_message -> {
val identity = Singleton.getIdentity(ctx)
if (identity == null) {
Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show()
} else {
val intent = Intent(ctx, ComposeMessageActivity::class.java)
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity)
intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item)
startActivity(intent)
}
return true
}
R.id.delete -> {
val warning = if (item.privateKey != null)
R.string.delete_identity_warning
else
R.string.delete_contact_warning
AlertDialog.Builder(ctx)
.setMessage(warning)
.setPositiveButton(android.R.string.yes) { _, _ ->
Singleton.getAddressRepository(ctx).remove(item)
MainActivity.apply {
if (item.privateKey != null) {
removeIdentityEntry(item)
}
}
this.item = null
ctx.onBackPressed()
}
.setNegativeButton(android.R.string.no, null)
.show()
return true
}
R.id.export -> {
AlertDialog.Builder(ctx)
.setMessage(R.string.confirm_export)
.setPositiveButton(android.R.string.yes) { _, _ ->
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_TITLE,
"$item$EXPORT_POSTFIX"
)
putExtra(
Intent.EXTRA_TEXT,
WifExporter(Singleton.getBitmessageContext(ctx)).apply {
addIdentity(item)
}.toString()
)
}
startActivity(Intent.createChooser(shareIntent, null))
}
.setNegativeButton(android.R.string.no, null)
.show()
return true
}
R.id.share -> {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TEXT, item.address)
startActivity(Intent.createChooser(shareIntent, null))
return true
}
else -> return false
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View
= inflater.inflate(R.layout.fragment_address_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Show the dummy content as text in a TextView.
item?.let { item ->
activity?.let { activity ->
when {
item.isChan -> activity.setTitle(R.string.title_chan_detail)
item.privateKey != null -> activity.setTitle(R.string.title_identity_detail)
item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail)
else -> activity.setTitle(R.string.title_contact_detail)
}
}
avatar.setImageDrawable(Identicon(item))
name.setText(item.toString())
name.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do
override fun afterTextChanged(s: Editable) {
item.alias = s.toString()
}
})
address.text = item.address
address.isSelected = true
stream_number.text = getString(R.string.stream_number, item.stream)
if (item.privateKey == null) {
active.isChecked = item.isSubscribed
active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked }
if (item.pubkey == null) {
pubkey_available.alpha = 0.3f
pubkey_available_desc.setText(R.string.pubkey_not_available)
}
} else {
active.visibility = View.GONE
pubkey_available.visibility = View.GONE
pubkey_available_desc.visibility = View.GONE
}
// QR code
qr_code.setImageBitmap(Drawables.qrCode(item))
}
}
override fun onPause() {
item?.let { item ->
Singleton.getAddressRepository(context!!).save(item)
if (item.privateKey != null) {
MainActivity.apply { updateIdentityEntry(item) }
}
}
super.onPause()
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
val ARG_ITEM = "item"
val EXPORT_POSTFIX = ".keys.dat"
}
}

View File

@ -1,166 +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.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.zxing.integration.android.IntentIntegrator;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import ch.dissem.apps.abit.listener.ActionBarListener;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.valueobject.Label;
import io.github.yavski.fabspeeddial.FabSpeedDial;
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
/**
* Fragment that shows a list of all contacts, the ones we subscribed to first.
*/
public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> {
@Override
public void onResume() {
super.onResume();
updateList();
}
public void updateList() {
List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext())
.getContacts();
Collections.sort(addresses, new Comparator<BitmessageAddress>() {
@Override
public int compare(BitmessageAddress lhs, BitmessageAddress rhs) {
// Yields the following order:
// * Subscribed addresses come first
// * Addresses with Aliases (alphabetically)
// * Addresses (alphabetically)
if (lhs.isSubscribed() == rhs.isSubscribed()) {
if (lhs.getAlias() != null) {
if (rhs.getAlias() != null) {
return lhs.getAlias().compareTo(rhs.getAlias());
} else {
return -1;
}
} else if (rhs.getAlias() != null) {
return 1;
} else {
return lhs.getAddress().compareTo(rhs.getAddress());
}
}
if (lhs.isSubscribed()) {
return -1;
} else {
return 1;
}
}
});
setListAdapter(new ArrayAdapter<BitmessageAddress>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
addresses) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.subscription_row, parent, false);
}
BitmessageAddress item = getItem(position);
assert item != null;
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new
Identicon(item));
TextView name = (TextView) convertView.findViewById(R.id.name);
name.setText(item.toString());
TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number);
streamNumber.setText(getContext().getString(R.string.stream_number,
item.getStream()));
convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ?
View.VISIBLE : View.INVISIBLE);
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
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_address_list, container, false);
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
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();
}
}

View File

@ -0,0 +1,145 @@
/*
* 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.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.integration.android.IntentIntegrator
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
/**
* Fragment that shows a list of all contacts, the ones we subscribed to first.
*/
class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() {
private lateinit var adapter: ArrayAdapter<BitmessageAddress>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = object : ArrayAdapter<BitmessageAddress>(
activity,
R.layout.subscription_row,
R.id.name,
LinkedList()) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val result: View
val v: ViewHolder
if (convertView == null) {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.subscription_row, parent, false)
v = ViewHolder(
ctx = context,
avatar = view.findViewById(R.id.avatar),
name = view.findViewById(R.id.name),
streamNumber = view.findViewById(R.id.stream_number),
subscribed = view.findViewById(R.id.subscribed)
)
view.tag = v
result = view
} else {
v = convertView.tag as ViewHolder
result = convertView
}
getItem(position)?.let { item ->
v.avatar.setImageDrawable(Identicon(item))
v.name.text = item.toString()
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
v.subscribed.visibility = if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
}
return result
}
}
listAdapter = adapter
}
override fun onResume() {
super.onResume()
initFab(activity as MainActivity)
updateList()
}
fun updateList() {
adapter.clear()
context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context)
doAsync {
addressRepo.getContactIds()
.map { addressRepo.getAddress(it) }
.forEach { address -> uiThread { adapter.add(address) } }
}
}
}
private fun initFab(activity: MainActivity) {
activity.updateTitle(getString(R.string.contacts_and_subscriptions))
val menu = FabSpeedDialMenu(activity)
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu)
.addOnMenuItemClickListener { _, _, itemId ->
when (itemId) {
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
.initiateScan()
2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
startActivity(intent)
}
else -> {
}
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_address_list, container, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && data.hasExtra("SCAN_RESULT")) {
val uri = Uri.parse(data.getStringExtra("SCAN_RESULT"))
val intent = Intent(activity, CreateAddressActivity::class.java)
intent.data = uri
startActivity(intent)
}
}
override fun updateList(label: Void) = updateList()
private data class ViewHolder(
val ctx: Context,
val avatar: ImageView,
val name: TextView,
val streamNumber: TextView,
val subscribed: View
)
}

View File

@ -1,105 +0,0 @@
/*
* 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.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
/**
* Compose a new message.
*/
public class ComposeMessageActivity extends AppCompatActivity {
public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER";
public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT";
public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT";
public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT";
public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST";
public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING";
public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.toolbar_layout);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(false);
// Display the fragment as the main content.
ComposeMessageFragment fragment = new ComposeMessageFragment();
fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, fragment)
.commit();
}
public static void launchReplyTo(Fragment fragment, Plaintext item) {
fragment.startActivity(getReplyIntent(fragment.getActivity(), item));
}
public static void launchReplyTo(Activity activity, Plaintext item) {
activity.startActivity(getReplyIntent(activity, item));
}
private static Intent getReplyIntent(Context ctx, Plaintext item) {
Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class);
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

@ -0,0 +1,104 @@
/*
* 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.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
import kotlinx.android.synthetic.main.toolbar_layout.*
/**
* Compose a new message.
*/
class ComposeMessageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.toolbar_layout)
setSupportActionBar(toolbar)
supportActionBar?.apply {
setHomeAsUpIndicator(R.drawable.ic_action_close)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(false)
}
// Display the fragment as the main content.
val fragment = ComposeMessageFragment()
fragment.arguments = intent.extras
supportFragmentManager
.beginTransaction()
.replace(R.id.content, fragment)
.commit()
}
companion object {
const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"
const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"
const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"
const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"
const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"
const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"
const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"
fun launchReplyTo(fragment: Fragment, item: Plaintext) =
fragment.startActivity(getReplyIntent(
ctx = fragment.activity ?: throw IllegalStateException("Fragment not attached to an activity"),
item = item
))
fun launchReplyTo(activity: Activity, item: Plaintext) =
activity.startActivity(getReplyIntent(activity, item))
private fun getReplyIntent(ctx: Context, item: Plaintext): Intent {
val replyIntent = Intent(ctx, ComposeMessageActivity::class.java)
val receivingIdentity = item.to
if (receivingIdentity?.isChan == true) {
// 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.from)
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.encoding == EXTENDED) {
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED)
}
replyIntent.putExtra(EXTRA_PARENT, item)
item.subject?.let { subject ->
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals("RE:", ignoreCase = true)) {
""
} else {
"RE: "
}
replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject)
}
replyIntent.putExtra(EXTRA_CONTENT,
"\n\n------------------------------------------------------\n" + item.text!!)
return replyIntent
}
}
}

View File

@ -1,266 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
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.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.Toast;
import java.util.List;
import ch.dissem.apps.abit.adapter.ContactAdapter;
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.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_PARENT;
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT;
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT;
import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
/**
* Compose a new message.
*/
public class ComposeMessageFragment extends Fragment {
private BitmessageAddress identity;
private BitmessageAddress recipient;
private String subject;
private String content;
private AutoCompleteTextView recipientInput;
private EditText subjectInput;
private EditText bodyInput;
private boolean broadcast;
private Plaintext.Encoding encoding;
private Plaintext parent;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public ComposeMessageFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
if (getArguments().containsKey(EXTRA_IDENTITY)) {
identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY);
if (getActivity() != null) {
if (identity == null || identity.getPrivateKey() == null) {
identity = Singleton.getIdentity(getActivity());
}
}
} else {
throw new RuntimeException("No identity set for ComposeMessageFragment");
}
broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false);
if (getArguments().containsKey(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);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false);
recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient);
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) {
recipientInput.setText(recipient.toString());
}
}
subjectInput = (EditText) rootView.findViewById(R.id.subject);
subjectInput.setText(subject);
bodyInput = (EditText) rootView.findViewById(R.id.body);
bodyInput.setText(content);
if (recipient == null) {
recipientInput.requestFocus();
} else if (subject == null || subject.isEmpty()) {
subjectInput.requestFocus();
} else {
bodyInput.requestFocus();
bodyInput.setSelection(0);
}
return rootView;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (identity == null || identity.getPrivateKey() == null) {
identity = Singleton.getIdentity(context);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.compose, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.send:
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;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0 && resultCode == RESULT_OK) {
encoding = (Plaintext.Encoding) data.getSerializableExtra(EXTRA_ENCODING);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void send() {
Plaintext.Builder builder;
BitmessageContext bmc = Singleton.getBitmessageContext(getContext());
if (broadcast) {
builder = new Plaintext.Builder(BROADCAST)
.from(identity);
} else {
String inputString = recipientInput.getText().toString();
if (recipient == null || !recipient.toString().equals(inputString)) {
try {
recipient = new BitmessageAddress(inputString);
} catch (Exception e) {
List<BitmessageAddress> contacts = Singleton.getAddressRepository
(getContext()).getContacts();
for (BitmessageAddress contact : contacts) {
if (inputString.equalsIgnoreCase(contact.getAlias())) {
recipient = contact;
if (inputString.equals(contact.getAlias()))
break;
}
}
}
}
builder = new Plaintext.Builder(MSG)
.from(identity)
.to(recipient);
}
switch (encoding) {
case SIMPLE:
builder.message(
subjectInput.getText().toString(),
bodyInput.getText().toString()
);
break;
case EXTENDED:
builder.message(
new 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,217 @@
/*
* 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.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.AdapterView
import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
import ch.dissem.apps.abit.adapter.ContactAdapter
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
import ch.dissem.bitmessage.entity.valueobject.extended.Message
import kotlinx.android.synthetic.main.fragment_compose_message.*
/**
* Compose a new message.
*/
class ComposeMessageFragment : Fragment() {
private lateinit var identity: BitmessageAddress
private var recipient: BitmessageAddress? = null
private var subject: String = ""
private var content: String = ""
private var broadcast: Boolean = false
private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE
private var parent: Plaintext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
var id = arguments.getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && (id == null || id.privateKey == null)) {
id = Singleton.getIdentity(context!!)
}
if (id?.privateKey != null) {
identity = id
} else {
throw IllegalStateException("No identity set for ComposeMessageFragment")
}
broadcast = arguments.getBoolean(EXTRA_BROADCAST, false)
if (arguments.containsKey(EXTRA_RECIPIENT)) {
recipient = arguments.getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
}
if (arguments.containsKey(EXTRA_SUBJECT)) {
subject = arguments.getString(EXTRA_SUBJECT)
}
if (arguments.containsKey(EXTRA_CONTENT)) {
content = arguments.getString(EXTRA_CONTENT)
}
encoding = arguments.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
if (arguments.containsKey(EXTRA_PARENT)) {
parent = arguments.getSerializable(EXTRA_PARENT) as Plaintext
}
} ?: {
throw IllegalStateException("No identity set for ComposeMessageFragment")
}.invoke()
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_compose_message, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (broadcast) {
recipient_input.visibility = View.GONE
} else {
val adapter = ContactAdapter(context!!)
recipient_input.setAdapter(adapter)
recipient_input.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> adapter.getItem(pos) }
recipient_input.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
recipient = adapter.getItem(position)
}
override fun onNothingSelected(parent: AdapterView<*>) = Unit // leave current selection
}
recipient?.let { recipient_input.setText(it.toString()) }
}
subject_input.setText(subject)
body_input.setText(content)
when {
recipient == null -> recipient_input.requestFocus()
subject.isEmpty() -> subject_input.requestFocus()
else -> {
body_input.requestFocus()
body_input.setSelection(0)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
inflater.inflate(R.menu.compose, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.send -> {
send()
return true
}
R.id.select_encoding -> {
val encodingDialog = SelectEncodingDialogFragment()
val args = Bundle()
args.putSerializable(EXTRA_ENCODING, encoding)
encodingDialog.arguments = args
encodingDialog.setTargetFragment(this, 0)
encodingDialog.show(fragmentManager, "select encoding dialog")
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = if (requestCode == 0 && data != null && resultCode == RESULT_OK) {
encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding
} else {
super.onActivityResult(requestCode, resultCode, data)
}
private fun send() {
val builder: Plaintext.Builder
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
val bmc = Singleton.getBitmessageContext(ctx)
if (broadcast) {
builder = Plaintext.Builder(BROADCAST).from(identity)
} else {
val inputString = recipient_input.text.toString()
if (recipient == null || recipient?.toString() != inputString) {
try {
recipient = BitmessageAddress(inputString)
} catch (e: Exception) {
val contacts = Singleton.getAddressRepository(ctx).getContacts()
for (contact in contacts) {
if (inputString.equals(contact.alias, ignoreCase = true)) {
recipient = contact
if (inputString == contact.alias)
break
}
}
}
}
if (recipient == null) {
Toast.makeText(context, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG).show()
return
}
builder = Plaintext.Builder(MSG)
.from(identity)
.to(recipient)
}
if (!Preferences.requestAcknowledgements(ctx)) {
builder.preventAck()
}
when (encoding) {
Plaintext.Encoding.SIMPLE -> builder.message(
subject_input.text.toString(),
body_input.text.toString()
)
Plaintext.Encoding.EXTENDED -> builder.message(
Message.Builder()
.subject(subject_input.text.toString())
.body(body_input.text.toString())
.addParent(parent)
.build()
)
else -> {
Toast.makeText(
ctx,
ctx.getString(R.string.error_unsupported_encoding, encoding),
Toast.LENGTH_LONG
).show()
builder.message(
subject_input.text.toString(),
body_input.text.toString()
)
}
}
bmc.send(builder.build())
ctx.finish()
}
}

View File

@ -1,169 +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.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,146 @@
/*
* 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.util.Base64.URL_SAFE
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.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.V2Pubkey
import ch.dissem.bitmessage.entity.payload.V3Pubkey
import ch.dissem.bitmessage.entity.payload.V4Pubkey
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.regex.Pattern
class CreateAddressActivity : AppCompatActivity() {
private var pubkeyBytes: ByteArray? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data
if (uri != null)
setContentView(R.layout.activity_open_bitmessage_link)
else
setContentView(R.layout.activity_create_bitmessage_address)
val address = findViewById<TextView>(R.id.address)
val label = findViewById<EditText>(R.id.label)
val subscribe = findViewById<Switch>(R.id.subscribe)
if (uri != null) {
val addressText = getAddress(uri)
val parameters = getParameters(uri)
for (parameter in parameters) {
val matcher = KEY_VALUE_PATTERN.matcher(parameter)
if (matcher.find()) {
val key = matcher.group(1).toLowerCase()
val value = matcher.group(2)
when (key) {
"label" -> label.setText(value.trim { it <= ' ' })
"action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true)
"pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE)
else -> LOG.debug("Unknown attribute: $key=$value")
}
}
}
address.text = addressText
}
val cancel = findViewById<Button>(R.id.cancel)
cancel.setOnClickListener {
setResult(Activity.RESULT_CANCELED)
finish()
}
findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) }
}
private fun onOK(address: TextView, label: EditText, subscribe: Switch) {
val addressText = address.text.toString().trim { it <= ' ' }
try {
val bmAddress = BitmessageAddress(addressText)
bmAddress.alias = label.text.toString()
val bmc = Singleton.getBitmessageContext(applicationContext)
bmc.addContact(bmAddress)
if (subscribe.isChecked) {
bmc.addSubscribtion(bmAddress)
}
pubkeyBytes?.let { pubkeyBytes ->
try {
val pubkeyStream = ByteArrayInputStream(pubkeyBytes)
val stream = bmAddress.stream
when (bmAddress.version.toInt()) {
2 -> V2Pubkey.read(pubkeyStream, stream)
3 -> V3Pubkey.read(pubkeyStream, stream)
4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream))
else -> null
}?.let { bmAddress.pubkey = it }
} catch (ignore: Exception) {
}
}
setResult(Activity.RESULT_OK)
finish()
} catch (e: RuntimeException) {
address.error = getString(R.string.error_illegal_address)
}
}
private fun getAddress(uri: Uri): String {
val result = StringBuilder()
val schemeSpecificPart = uri.schemeSpecificPart
if (!schemeSpecificPart.startsWith("BM-")) {
result.append("BM-")
}
when {
schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?')))
schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#')))
else -> result.append(schemeSpecificPart)
}
return result.toString()
}
private fun getParameters(uri: Uri): Array<String> {
val index = uri.schemeSpecificPart.indexOf('?')
return if (index >= 0) {
uri.schemeSpecificPart
.substring(index + 1)
.split("&".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()
} else {
emptyArray()
}
}
companion object {
private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java)
private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$")
}
}

View File

@ -1,53 +0,0 @@
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

@ -0,0 +1,48 @@
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.view.MenuItem
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*
/**
* @author Christian Basler
*/
abstract class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.scrolling_toolbar_layout)
setSupportActionBar(toolbar)
// Show the Up button in the action bar.
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
}
MaterializeBuilder()
.withActivity(this)
.withStatusBarColorRes(R.color.colorPrimaryDark)
.withTranslucentStatusBarProgrammatically(true)
.withStatusBarPadding(true)
.build()
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
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, Intent(this, MainActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

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

View File

@ -0,0 +1,98 @@
/*
* 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.graphics.*
import android.graphics.drawable.Drawable
import android.text.TextPaint
import ch.dissem.bitmessage.entity.BitmessageAddress
/**
* @author Christian Basler
*/
class Identicon(input: BitmessageAddress) : Drawable() {
private val paint = Paint().apply {
style = Paint.Style.FILL
isAntiAlias = true
}
private val hash = input.ripe
private val isChan = input.isChan
private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply {
for (row in 0 until SIZE) {
if (!isChan || row < 5 || row > 6) {
for (column in 0..CENTER_COLUMN) {
if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) {
this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0
this[row][SIZE - column - 1] = this[row][column]
}
}
}
}
}
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f))
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f))
private val textPaint = TextPaint().apply {
textAlign = Paint.Align.CENTER
color = 0xFF607D8B.toInt()
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
}
override fun draw(canvas: Canvas) {
var x: Float
var y: Float
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
val cellWidth = width / SIZE.toFloat()
val cellHeight = height / SIZE.toFloat()
paint.color = background
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
paint.color = color
for (row in 0 until SIZE) {
for (column in 0 until SIZE) {
if (fields[row][column]) {
x = cellWidth * column
y = cellHeight * row
canvas.drawCircle(
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint
)
}
}
}
if (isChan) {
textPaint.textSize = 2 * cellHeight
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint)
}
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
paint.colorFilter = cf
}
override fun getOpacity() = PixelFormat.TRANSPARENT
companion object {
private val SIZE = 9
private val CENTER_COLUMN = 5
}
}

View File

@ -1,85 +0,0 @@
/*
* 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,77 @@
/*
* 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.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 android.widget.Button
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import ch.dissem.apps.abit.adapter.AddressSelectorAdapter
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.wif.WifImporter
/**
* @author Christian Basler
*/
class ImportIdentitiesFragment : Fragment() {
private lateinit var adapter: AddressSelectorAdapter
private lateinit var importer: WifImporter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val wifData = arguments.getString(WIF_DATA)
val bmc = Singleton.getBitmessageContext(activity)
importer = WifImporter(bmc, wifData)
adapter = AddressSelectorAdapter(importer.getIdentities())
val layoutManager = LinearLayoutManager(activity,
LinearLayoutManager.VERTICAL,
false)
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
recyclerView.addItemDecoration(SimpleListDividerDecorator(
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true))
view.findViewById<Button>(R.id.finish).setOnClickListener {
importer.importAll(adapter.selected)
MainActivity.apply {
for (selected in adapter.selected) {
addIdentityEntry(selected)
}
}
activity.finish()
}
}
companion object {
val WIF_DATA = "wif_data"
}
}

View File

@ -1,56 +0,0 @@
/*
* 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,48 @@
/*
* 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
/**
* @author Christian Basler
*/
class ImportIdentityActivity : DetailActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
if (wifData == null) {
fragmentManager.beginTransaction()
.replace(R.id.content, InputWifFragment())
.commit()
} else {
val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
val fragment = ImportIdentitiesFragment()
fragment.arguments = bundle
fragmentManager.beginTransaction()
.replace(R.id.content, fragment)
.commit()
}
}
}

View File

@ -1,122 +0,0 @@
/*
* 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,101 @@
/*
* 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.view.*
import android.widget.Toast
import com.github.angads25.filepicker.model.DialogConfigs
import com.github.angads25.filepicker.model.DialogProperties
import com.github.angads25.filepicker.view.FilePickerDialog
import kotlinx.android.synthetic.main.fragment_import_input.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
/**
* @author Christian Basler
*/
class InputWifFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_import_input, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
next.setOnClickListener {
val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString())
val fragment = ImportIdentitiesFragment().apply {
arguments = bundle
}
fragmentManager.beginTransaction()
.replace(R.id.content, fragment)
.commit()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) =
inflater.inflate(R.menu.import_input_data, menu)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val properties = DialogProperties()
properties.selection_mode = DialogConfigs.SINGLE_MODE
properties.selection_type = DialogConfigs.FILE_SELECT
properties.root = File(DialogConfigs.DEFAULT_DIR)
properties.error_dir = File(DialogConfigs.DEFAULT_DIR)
properties.extensions = null
val dialog = FilePickerDialog(activity, properties)
dialog.setTitle(getString(R.string.select_file_title))
dialog.setDialogSelectionListener { files ->
if (files.isNotEmpty()) {
try {
FileInputStream(files[0]).use { inputStream ->
val data = ByteArrayOutputStream()
val buffer = ByteArray(1024)
var length: Int = inputStream.read(buffer)
while (length != -1) {
data.write(buffer, 0, length)
length = inputStream.read(buffer)
}
wif_input.setText(data.toByteArray().toString())
}
} catch (e: IOException) {
Toast.makeText(
activity,
R.string.error_loading_data,
Toast.LENGTH_SHORT
).show()
}
}
}
dialog.show()
return true
}
}

View File

@ -14,15 +14,15 @@
* limitations under the License.
*/
package ch.dissem.apps.abit;
import ch.dissem.bitmessage.entity.valueobject.Label;
package ch.dissem.apps.abit
/**
* @author Christian Basler
*/
public interface ListHolder {
void updateList(Label label);
interface ListHolder<L> {
fun updateList(label: L)
void setActivateOnItemClick(boolean activateOnItemClick);
fun setActivateOnItemClick(activateOnItemClick: Boolean)
fun showPreviousList(): Boolean
}

View File

@ -1,622 +0,0 @@
/*
* 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

@ -0,0 +1,505 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Intent
import android.graphics.Point
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.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.NetworkUtils
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 com.github.amlcurran.showcaseview.ShowcaseView
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.model.*
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Nameable
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.Serializable
import java.lang.ref.WeakReference
import java.util.*
/**
* 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 [MessageDetailActivity] representing
* item details. On tablets, the activity presents the list of items and
* item details side-by-side using two vertical panes.
*
*
* The activity makes heavy use of fragments. The list of items is a
* [MessageListFragment] and the item details
* (if present) is a [MessageDetailFragment].
*
*
* This activity also implements the required
* [ListSelectionListener] interface
* to listen for item selections.
*
*/
class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private var active: Boolean = false
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
var hasDetailPane: Boolean = false
private set
private lateinit var bmc: BitmessageContext
private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer
private lateinit var nodeSwitch: SwitchDrawerItem
val floatingActionButton: FabSpeedDial?
get() = findViewById(R.id.fab)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this)
setContentView(R.layout.activity_main)
fab.hide()
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val listFragment = MessageListFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.item_list, listFragment)
.commit()
if (findViewById<View>(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.
hasDetailPane = true
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true)
}
createDrawer(toolbar)
// handle intents
val intent = intent
if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) {
onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE))
}
if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) {
val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext
ComposeMessageActivity.launchReplyTo(this, item)
}
if (Preferences.useTrustedNode(this)) {
SyncAdapter.startSync(this)
} else {
SyncAdapter.stopSync(this)
}
if (drawer.isDrawerOpen) {
val lps = RelativeLayout.LayoutParams(ViewGroup
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
lps.setMargins(margin, margin, margin, margin)
ShowcaseView.Builder(this)
.withMaterialShowcase()
.setStyle(R.style.CustomShowcaseTheme)
.setContentTitle(R.string.full_node)
.setContentText(R.string.full_node_description)
.setTarget {
val view = drawer.stickyFooter
val location = IntArray(2)
view.getLocationInWindow(location)
val x = location[0] + 7 * view.width / 8
val y = location[1] + view.height / 2
Point(x, y)
}
.replaceEndButton(R.layout.showcase_button)
.hideOnTouchOutside()
.build()
.setButtonPosition(lps)
}
}
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
if (active) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.item_list, listFragment)
supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let {
transaction.remove(it)
}
transaction.addToBackStack(null).commit()
if (hasDetailPane) {
// In two-pane mode, list items should be given the
// 'activated' state when touched.
listFragment.setActivateOnItemClick(true)
}
}
}
private fun createDrawer(toolbar: Toolbar) {
val profiles = ArrayList<IProfile<*>>()
profiles.add(ProfileSettingDrawerItem()
.withName(getString(R.string.add_identity))
.withDescription(getString(R.string.add_identity_summary))
.withIcon(IconicsDrawable(this, GoogleMaterial.Icon.gmd_add)
.actionBar()
.paddingDp(5)
.colorRes(R.color.icons))
.withIdentifier(ADD_IDENTITY.toLong())
)
profiles.add(ProfileSettingDrawerItem()
.withName(getString(R.string.manage_identity))
.withIcon(GoogleMaterial.Icon.gmd_settings)
.withIdentifier(MANAGE_IDENTITY.toLong())
)
// Create the AccountHeader
accountHeader = AccountHeaderBuilder()
.withActivity(this)
.withHeaderBackground(R.drawable.header)
.withProfiles(profiles)
.withOnAccountHeaderProfileImageListener(ProfileImageListener(this))
.withOnAccountHeaderListener(ProfileSelectionListener(this@MainActivity, supportFragmentManager))
.build()
if (profiles.size > 2) { // There's always the add and manage identity items
accountHeader.setActiveProfile(profiles[0], true)
}
val drawerItems = ArrayList<IDrawerItem<*, *>>()
drawerItems.add(PrimaryDrawerItem()
.withName(R.string.archive)
.withTag(LABEL_ARCHIVE)
.withIcon(CommunityMaterial.Icon.cmd_archive)
)
drawerItems.add(DividerDrawerItem())
drawerItems.add(PrimaryDrawerItem()
.withName(R.string.contacts_and_subscriptions)
.withIcon(GoogleMaterial.Icon.gmd_contacts))
drawerItems.add(PrimaryDrawerItem()
.withName(R.string.settings)
.withIcon(GoogleMaterial.Icon.gmd_settings))
nodeSwitch = SwitchDrawerItem()
.withIdentifier(ID_NODE_SWITCH)
.withName(R.string.full_node)
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
.withChecked(Preferences.isFullNodeActive(this))
.withOnCheckedChangeListener { _, _, isChecked ->
if (isChecked) {
NetworkUtils.enableNode(this@MainActivity)
} else {
NetworkUtils.disableNode(this@MainActivity)
}
}
drawer = DrawerBuilder()
.withActivity(this)
.withToolbar(toolbar)
.withAccountHeader(accountHeader)
.withDrawerItems(drawerItems)
.addStickyDrawerItems(nodeSwitch)
.withOnDrawerItemClickListener(DrawerItemClickListener())
.withShowDrawerOnFirstLaunch(true)
.build()
loadDrawerItemsAsynchronously()
}
private fun loadDrawerItemsAsynchronously() {
doAsync {
val identities = bmc.addresses.getIdentities()
if (identities.isEmpty()) {
// Create an initial identity
Singleton.getIdentity(this@MainActivity)
}
uiThread {
for (identity in identities) {
addIdentityEntry(identity)
}
}
}
doAsync {
val labels = bmc.labels.getLabels()
uiThread {
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
} else if (currentLabel.value == null) {
currentLabel.value = labels[0]
}
for (label in labels) {
addLabelEntry(label)
}
currentLabel.value?.let {
drawer.setSelection(it.id as Long)
}
updateUnread()
}
}
}
override fun onBackPressed() {
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
super.onBackPressed()
}
}
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean {
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
val tag = item.tag
if (tag is Label) {
currentLabel.value = tag
if (itemList !is MessageListFragment) {
changeList(MessageListFragment())
}
return false
} else if (item is Nameable<*>) {
when (item.name.textRes) {
R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) {
itemList.updateList()
} else {
changeList(AddressListFragment())
}
return false
}
R.string.settings -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.item_list, SettingsFragment())
.addToBackStack(null)
.commit()
return false
}
R.string.full_node -> return true
else -> return false
}
}
return false
}
}
override fun onResume() {
updateUnread()
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
NetworkUtils.enableNode(this, false)
}
Singleton.getMessageListener(this).resetNotification()
currentLabel.addObserver(this) { label ->
if (label != null) {
drawer.setSelection(label.id as Long)
}
}
active = true
super.onResume()
}
override fun onPause() {
currentLabel.removeObserver(this)
super.onPause()
active = false
}
fun addIdentityEntry(identity: BitmessageAddress) {
val newProfile = ProfileDrawerItem()
.withIcon(Identicon(identity))
.withName(identity.toString())
.withNameShown(true)
.withEmail(identity.address)
.withTag(identity)
if (accountHeader.profiles != null) {
// we know that there are 2 setting elements.
// Set the new profile above them ;)
accountHeader.addProfile(
newProfile, accountHeader.profiles.size - 2)
} else {
accountHeader.addProfiles(newProfile)
}
}
private fun addLabelEntry(label: Label) {
val item = PrimaryDrawerItem()
.withIdentifier(label.id as Long)
.withName(label.toString())
.withTag(label)
.withIcon(Labels.getIcon(label))
.withIconColor(Labels.getColor(label))
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
}
fun updateIdentityEntry(identity: BitmessageAddress) {
for (profile in accountHeader.profiles) {
if (profile is ProfileDrawerItem) {
if (identity == profile.tag) {
profile
.withName(identity.toString())
.withTag(identity)
return
}
}
}
}
fun removeIdentityEntry(identity: BitmessageAddress) {
for (profile in accountHeader.profiles) {
if (profile is ProfileDrawerItem) {
if (identity == profile.tag) {
accountHeader.removeProfile(profile)
return
}
}
}
}
fun updateUnread() {
for (item in drawer.drawerItems) {
if (item.tag is Label) {
val label = item.tag as Label
if (label !== LABEL_ARCHIVE) {
val unread = bmc.messages.countUnread(label)
if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString())
} else {
(item as PrimaryDrawerItem).withBadge(null as String?)
}
drawer.updateItem(item)
}
}
}
}
/**
* Callback method from [ListSelectionListener]
* indicating that the item with the given ID was selected.
*/
override fun onItemSelected(item: Serializable) {
if (hasDetailPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
val fragment = when (item) {
is Plaintext -> MessageDetailFragment()
is BitmessageAddress -> AddressDetailFragment()
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
}
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
val detailIntent = when (item) {
is Plaintext -> {
Intent(this, MessageDetailActivity::class.java)
}
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java)
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
}
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
startActivity(detailIntent)
}
}
fun setDetailView(fragment: Fragment) {
if (hasDetailPane) {
supportFragmentManager.beginTransaction()
.replace(R.id.message_detail_container, fragment)
.commit()
}
}
fun updateTitle(title: CharSequence) {
supportActionBar?.title = title
}
companion object {
val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"
val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"
val ADD_IDENTITY = 1
val MANAGE_IDENTITY = 2
private val ID_NODE_SWITCH: Long = 1
private var instance: WeakReference<MainActivity>? = null
fun updateNodeSwitch() {
apply {
runOnUiThread {
nodeSwitch.withChecked(Preferences.isFullNodeActive(this))
drawer.updateStickyFooterItem(nodeSwitch)
}
}
}
/**
* Runs the given code in the main activity context, if it currently exists. Otherwise,
* it's ignored.
*/
fun apply(run: MainActivity.() -> Unit) {
instance?.get()?.let { run.invoke(it) }
}
}
}

View File

@ -1,63 +0,0 @@
package ch.dissem.apps.abit;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import ch.dissem.bitmessage.entity.valueobject.Label;
/**
* An activity representing a single Message detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a {@link MainActivity}.
* <p/>
* This activity is mostly just a 'shell' activity containing nothing
* more than a {@link MessageDetailFragment}.
*/
public class MessageDetailActivity extends DetailActivity {
private Label label;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL);
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM));
MessageDetailFragment fragment = new MessageDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.add(R.id.content, fragment)
.commit();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent parentIntent = new Intent(this, MainActivity.class);
parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label);
NavUtils.navigateUpTo(this, parentIntent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

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.view.MenuItem
/**
* An activity representing a single Message detail screen. This
* activity is only used on handset devices. On tablet-size devices,
* item details are presented side-by-side with a list of items
* in a [MainActivity].
*
* This activity is mostly just a 'shell' activity containing nothing
* more than a [MessageDetailFragment].
*/
class MessageDetailActivity : DetailActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM))
val fragment = MessageDetailFragment()
fragment.arguments = arguments
supportFragmentManager.beginTransaction()
.add(R.id.content, fragment)
.commit()
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@ -1,367 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.view.IconicsImageView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import ch.dissem.apps.abit.listener.ActionBarListener;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.apps.abit.util.Assets;
import ch.dissem.apps.abit.util.Drawables;
import ch.dissem.apps.abit.util.Labels;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import static android.text.util.Linkify.WEB_URLS;
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN;
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA;
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
/**
* 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 MessageDetailFragment 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 Plaintext item;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public MessageDetailFragment() {
}
@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 = (Plaintext) getArguments().getSerializable(ARG_ITEM);
}
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_message_detail, container, false);
// Show the dummy content as text in a TextView.
if (item != null) {
((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject());
ImageView status = (ImageView) rootView.findViewById(R.id.status);
status.setImageResource(Assets.getStatusDrawable(item.getStatus()));
status.setContentDescription(getString(Assets.getStatusString(item.getStatus())));
BitmessageAddress sender = item.getFrom();
((ImageView) rootView.findViewById(R.id.avatar))
.setImageDrawable(new Identicon(sender));
((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString());
if (item.getTo() != null) {
((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString());
} else if (item.getType() == Plaintext.Type.BROADCAST) {
((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast);
}
RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels);
LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels());
labelView.setAdapter(labelAdapter);
labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
TextView messageBody = (TextView) rootView.findViewById(R.id.text);
messageBody.setText(item.getText());
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;
Iterator<Label> labels = item.getLabels().iterator();
while (labels.hasNext()) {
if (labels.next().getType() == Label.Type.UNREAD) {
labels.remove();
removed = true;
}
}
MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext());
if (removed) {
if (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;
}
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
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message, menu);
Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply);
Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete);
Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon
.gmd_markunread);
Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
MessageRepository messageRepo = Singleton.getMessageRepository(getContext());
switch (menuItem.getItemId()) {
case R.id.reply:
ComposeMessageActivity.launchReplyTo(this, item);
return true;
case R.id.delete:
if (isInTrash(item)) {
messageRepo.remove(item);
} else {
item.getLabels().clear();
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
messageRepo.save(item);
}
getActivity().onBackPressed();
return true;
case R.id.mark_unread:
item.addLabels(messageRepo.getLabels(Label.Type.UNREAD));
messageRepo.save(item);
if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
return true;
case R.id.archive:
if (item.isUnread() && getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
item.getLabels().clear();
messageRepo.save(item);
return true;
default:
return false;
}
}
public static boolean isInTrash(Plaintext item) {
for (Label label : item.getLabels()) {
if (label.getType() == Label.Type.TRASH) {
return true;
}
}
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

@ -0,0 +1,281 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.annotation.IdRes
import android.support.v4.app.Fragment
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.util.Linkify
import android.text.util.Linkify.WEB_URLS
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.view.IconicsImageView
import kotlinx.android.synthetic.main.fragment_message_detail.*
import java.util.*
/**
* A fragment representing a single Message detail screen.
* This fragment is either contained in a [MainActivity]
* in two-pane mode (on tablets) or a [MessageDetailActivity]
* on handsets.
*/
class MessageDetailFragment : Fragment() {
/**
* The content this fragment is presenting.
*/
private var item: Plaintext? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { arguments ->
if (arguments.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 = arguments.getSerializable(ARG_ITEM) as Plaintext
}
}
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_message_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
// Show the dummy content as text in a TextView.
item?.let { item ->
subject.text = item.subject
status.setImageResource(Assets.getStatusDrawable(item.status))
status.contentDescription = getString(Assets.getStatusString(item.status))
avatar.setImageDrawable(Identicon(item.from))
sender.text = item.from.toString()
item.to?.let { to ->
recipient.text = to.toString()
} ?: {
if (item.type == Plaintext.Type.BROADCAST) {
recipient.setText(R.string.broadcast)
}
}.invoke()
val labelAdapter = LabelAdapter(ctx, item.labels)
labels.adapter = labelAdapter
labels.layoutManager = GridLayoutManager(activity, 2)
text.text = item.text
Linkify.addLinks(text, WEB_URLS)
Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null,
Linkify.TransformFilter { match, _ -> match.group() }
)
text.linksClickable = true
text.setTextIsSelectable(true)
val messageRepo = Singleton.getMessageRepository(ctx)
if (item.isUnread()) {
Singleton.labeler.markAsRead(item)
(activity as? MainActivity)?.updateUnread()
messageRepo.save(item)
}
val parents = ArrayList<Plaintext>(item.parents.size)
for (parentIV in item.parents) {
val parent = messageRepo.getMessage(parentIV)
if (parent != null) {
parents.add(parent)
}
}
showRelatedMessages(ctx, view, R.id.parents, parents)
showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item))
}
}
private fun showRelatedMessages(ctx: Context, rootView: View, @IdRes id: Int, messages: List<Plaintext>) {
val recyclerView = rootView.findViewById<RecyclerView>(id)
val adapter = RelatedMessageAdapter(ctx, messages)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message, menu)
activity?.let { activity ->
Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply)
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
Drawables.addIcon(activity, menu, R.id.mark_unread, GoogleMaterial.Icon
.gmd_markunread)
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
val messageRepo = Singleton.getMessageRepository(
context ?: throw IllegalStateException("No context available")
)
item?.let { item ->
when (menuItem.itemId) {
R.id.reply -> {
ComposeMessageActivity.launchReplyTo(this, item)
return true
}
R.id.delete -> {
if (isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
(activity as? MainActivity)?.updateUnread()
activity?.onBackPressed()
return true
}
R.id.mark_unread -> {
Singleton.labeler.markAsUnread(item)
messageRepo.save(item)
(activity as? MainActivity)?.updateUnread()
return true
}
R.id.archive -> {
if (item.isUnread() && activity is MainActivity) {
(activity as MainActivity).updateUnread()
}
Singleton.labeler.archive(item)
messageRepo.save(item)
(activity as? MainActivity)?.updateUnread()
return true
}
else -> return false
}
}
return false
}
private class RelatedMessageAdapter internal constructor(private val ctx: Context, private val messages: List<Plaintext>) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RelatedMessageAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val message = messages[position]
viewHolder.avatar.setImageDrawable(Identicon(message.from))
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status))
viewHolder.sender.text = message.from.toString()
viewHolder.extract.text = prepareMessageExtract(message.text)
viewHolder.item = message
}
// Returns the total count of items in the list
override fun getItemCount() = messages.size
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal val avatar = itemView.findViewById<ImageView>(R.id.avatar)
internal val status = itemView.findViewById<ImageView>(R.id.status)
internal val sender = itemView.findViewById<TextView>(R.id.sender)
internal val extract = itemView.findViewById<TextView>(R.id.text)
internal var item: Plaintext? = null
init {
itemView.setOnClickListener {
if (ctx is MainActivity) {
item?.let { ctx.onItemSelected(it) }
} else {
val detailIntent = Intent(ctx, MessageDetailActivity::class.java)
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item)
ctx.startActivity(detailIntent)
}
}
}
}
}
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) : RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
private val labels = labels.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
// Inflate the custom layout
val contactView = inflater.inflate(R.layout.item_label, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
// Involves populating data into the item through holder
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
// Get the data model based on position
val label = labels[position]
viewHolder.icon.icon?.color(Labels.getColor(label))
viewHolder.icon.icon?.icon(Labels.getIcon(label))
viewHolder.label.text = Labels.getText(label, ctx)
}
override fun getItemCount() = labels.size
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
var label = itemView.findViewById<TextView>(R.id.label)!!
}
}
companion object {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
val ARG_ITEM = "item"
fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true
}
}

View File

@ -1,336 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.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.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import io.github.yavski.fabspeeddial.FabSpeedDial;
import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter;
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST;
import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY;
import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash;
/**
* A list fragment representing a list of Messages. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a {@link MessageDetailFragment}.
* <p/>
* Activities containing this fragment MUST implement the {@link ListSelectionListener}
* interface.
*/
public class MessageListFragment extends Fragment implements ListHolder {
private RecyclerView recyclerView;
private RecyclerView.LayoutManager layoutManager;
private SwipeableMessageAdapter adapter;
private RecyclerView.Adapter wrappedAdapter;
private RecyclerViewSwipeManager recyclerViewSwipeManager;
private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager;
private Label currentLabel;
private MenuItem emptyTrashMenuItem;
private MessageRepository messageRepo;
private boolean activateOnItemClick;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public MessageListFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onResume() {
super.onResume();
MainActivity activity = (MainActivity) getActivity();
messageRepo = Singleton.getMessageRepository(activity);
doUpdateList(activity.getSelectedLabel());
}
@Override
public void updateList(Label label) {
if (!isResumed()) {
currentLabel = label;
return;
}
if (!Objects.equals(currentLabel, label)) {
adapter.setData(label, Collections.<Plaintext>emptyList());
adapter.notifyDataSetChanged();
}
doUpdateList(label);
}
private void doUpdateList(final Label label) {
if (label == null) {
if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name));
}
adapter.setData(null, Collections.<Plaintext>emptyList());
adapter.notifyDataSetChanged();
return;
}
currentLabel = label;
if (emptyTrashMenuItem != null) {
emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH);
}
if (getActivity() instanceof ActionBarListener) {
ActionBarListener actionBarListener = (ActionBarListener) getActivity();
if ("archive".equals(label.toString())) {
actionBarListener.updateTitle(getString(R.string.archive));
} else {
actionBarListener.updateTitle(label.toString());
}
}
new AsyncTask<Void, Void, List<Plaintext>>() {
@Override
protected List<Plaintext> doInBackground(Void... params) {
return messageRepo.findMessages(label);
}
@Override
protected void onPostExecute(List<Plaintext> messages) {
if (adapter != null) {
adapter.setData(label, messages);
adapter.notifyDataSetChanged();
}
}
}.execute();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_message_list, container, false);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
// Show the dummy content as text in a TextView.
FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id
.fab_compose_message);
fab.setMenuListener(new SimpleMenuListenerAdapter() {
@Override
public boolean onMenuItemSelected(MenuItem menuItem) {
BitmessageAddress identity = Singleton.getIdentity(getActivity());
if (identity == null) {
Toast.makeText(getActivity(), R.string.no_identity_warning,
Toast.LENGTH_LONG).show();
return false;
} else {
switch (menuItem.getItemId()) {
case R.id.action_compose_message: {
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
intent.putExtra(EXTRA_IDENTITY, identity);
startActivity(intent);
return true;
}
case R.id.action_compose_broadcast: {
Intent intent = new Intent(getActivity(), ComposeMessageActivity.class);
intent.putExtra(EXTRA_IDENTITY, identity);
intent.putExtra(EXTRA_BROADCAST, true);
startActivity(intent);
return true;
}
default:
return false;
}
}
}
});
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// 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;
}
@Override
public void onDestroyView() {
if (recyclerViewSwipeManager != null) {
recyclerViewSwipeManager.release();
recyclerViewSwipeManager = null;
}
if (recyclerViewTouchActionGuardManager != null) {
recyclerViewTouchActionGuardManager.release();
recyclerViewTouchActionGuardManager = null;
}
if (recyclerView != null) {
recyclerView.setItemAnimator(null);
recyclerView.setAdapter(null);
recyclerView = null;
}
if (wrappedAdapter != null) {
WrapperAdapterUtils.releaseAll(wrappedAdapter);
wrappedAdapter = null;
}
adapter = null;
layoutManager = null;
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message_list, menu);
emptyTrashMenuItem = menu.findItem(R.id.empty_trash);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.empty_trash:
if (currentLabel.getType() != Label.Type.TRASH) return true;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
for (Plaintext message : messageRepo.findMessages(currentLabel)) {
messageRepo.remove(message);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
updateList(currentLabel);
}
}.execute();
return true;
default:
return false;
}
}
@Override
public void setActivateOnItemClick(boolean activateOnItemClick) {
if (adapter != null) {
adapter.setActivateOnItemClick(activateOnItemClick);
}
this.activateOnItemClick = activateOnItemClick;
}
}

View File

@ -0,0 +1,324 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.RecyclerView.OnScrollListener
import android.view.*
import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
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 io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import java.util.*
private const val PAGE_SIZE = 15
/**
* A list fragment representing a list of Messages. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a [MessageDetailFragment].
*
*
* Activities containing this fragment MUST implement the [ListSelectionListener]
* interface.
*/
class MessageListFragment : Fragment(), ListHolder<Label> {
private var isLoading = false
private var isLastPage = false
private var layoutManager: LinearLayoutManager? = null
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
&& firstVisibleItemPosition >= 0) {
loadMoreItems()
}
}
}
}
}
private var emptyTrashMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false
private val backStack = Stack<Label>()
fun loadMoreItems() {
isLoading = true
swipeableMessageAdapter?.let { messageAdapter ->
doAsync {
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
onUiThread {
messageAdapter.addAll(messages)
isLoading = false
isLastPage = messages.size < PAGE_SIZE
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
val activity = activity as MainActivity
initFab(activity)
messageRepo = Singleton.getMessageRepository(activity)
currentLabel.addObserver(this) { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
currentLabel.removeObserver(this)
super.onPause()
}
private fun doUpdateList(label: Label?) {
val mainActivity = activity as? MainActivity
swipeableMessageAdapter?.clear(label)
if (label == null) {
mainActivity?.updateTitle(getString(R.string.app_name))
swipeableMessageAdapter?.notifyDataSetChanged()
return
}
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
mainActivity?.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
} else {
updateTitle(label.toString())
}
}
loadMoreItems()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val context = context ?: throw IllegalStateException("No context available")
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
setInterceptVerticalScrollingWhileAnimationRunning(true)
isEnabled = true
}
// swipe manager
val swipeManager = RecyclerViewSwipeManager()
//swipeableMessageAdapter
val adapter = SwipeableMessageAdapter().apply {
setActivateOnItemClick(activateOnItemClick)
}
adapter.eventListener = object : SwipeableMessageAdapter.EventListener {
override fun onItemDeleted(item: Plaintext) {
if (MessageDetailFragment.isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
}
override fun onItemArchived(item: Plaintext) {
Singleton.labeler.archive(item)
}
override fun onItemViewClicked(v: View?) {
val position = recycler_view.getChildAdapterPosition(v)
adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) {
val item = adapter.getItem(position)
(activity as MainActivity).onItemSelected(item)
}
}
}
// wrap for swiping
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
val animator = 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.supportsChangeAnimations = false
recycler_view.layoutManager = layoutManager
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableMessageAdapter
recycler_view.itemAnimator = animator
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(SimpleListDividerDecorator(
ContextCompat.getDrawable(context, 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
touchActionGuardManager.attachRecyclerView(recycler_view)
swipeManager.attachRecyclerView(recycler_view)
recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager
this.swipeableMessageAdapter = adapter
Singleton.updateMessageListAdapterInListener(adapter)
}
private fun initFab(context: MainActivity) {
val menu = FabSpeedDialMenu(context)
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
.addOnMenuItemClickListener { _, _, itemId ->
val identity = Singleton.getIdentity(context)
if (identity == null) {
Toast.makeText(activity, R.string.no_identity_warning,
Toast.LENGTH_LONG).show()
} else {
when (itemId) {
1 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
intent.putExtra(EXTRA_BROADCAST, true)
startActivity(intent)
}
2 -> {
val intent = Intent(activity, ComposeMessageActivity::class.java)
intent.putExtra(EXTRA_IDENTITY, identity)
startActivity(intent)
}
else -> {
}
}
}
}
}
override fun onDestroyView() {
recyclerViewSwipeManager?.release()
recyclerViewSwipeManager = null
recyclerViewTouchActionGuardManager?.release()
recyclerViewTouchActionGuardManager = null
recycler_view.itemAnimator = null
recycler_view.adapter = null
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
wrappedAdapter = null
swipeableMessageAdapter = null
layoutManager = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.empty_trash -> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
return true
}
else -> return false
}
}
override fun updateList(label: Label) {
currentLabel.value = label
}
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick)
this.activateOnItemClick = activateOnItemClick
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.value = backStack.pop()
true
}
}

View File

@ -1,18 +0,0 @@
package ch.dissem.apps.abit;
import android.os.Bundle;
/**
* @author Christian Basler
*/
public class SettingsActivity extends DetailActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Display the fragment as the main content.
getFragmentManager().beginTransaction()
.replace(R.id.content, new SettingsFragment())
.commit();
}
}

View File

@ -1,140 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.widget.Toast;
import com.mikepenz.aboutlibraries.Libs;
import com.mikepenz.aboutlibraries.LibsBuilder;
import ch.dissem.apps.abit.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;
/**
* @author Christian Basler
*/
public class SettingsFragment
extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
Preference about = findPreference("about");
about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
new LibsBuilder()
.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,200 @@
/*
* 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.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v4.content.FileProvider.getUriForFile
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.widget.Toast
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.Preferences
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.indeterminateProgressDialog
import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.uiThread
/**
* @author Christian Basler
*/
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
val cleanup = findPreference("cleanup")
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
findPreference("export")?.onPreferenceClickListener = exportClickListener()
findPreference("import")?.onPreferenceClickListener = importClickListener()
findPreference("status").onPreferenceClickListener = statusClickListener()
}
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
(activity as? MainActivity)?.let { activity ->
val libsBuilder = LibsBuilder()
.withActivityTitle(activity.getString(R.string.about))
.withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutDescription(getString(R.string.about_app))
if (activity.hasDetailPane) {
activity.setDetailView(libsBuilder.supportFragment())
} else {
libsBuilder.start(activity)
}
}
return@OnPreferenceClickListener true
}
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available")
cleanup.isEnabled = false
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
doAsync {
val bmc = Singleton.getBitmessageContext(ctx)
bmc.internals.nodeRegistry.clear()
bmc.cleanup()
Preferences.cleanupExportDirectory(ctx)
uiThread {
Toast.makeText(
ctx,
R.string.cleanup_notification_end,
Toast.LENGTH_LONG
).show()
cleanup.isEnabled = true
}
}
return@OnPreferenceClickListener true
}
private fun exportClickListener() = Preference.OnPreferenceClickListener {
val ctx = context ?: throw IllegalStateException("No context available")
indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
doAsync {
val exportDirectory = Preferences.getExportDirectory(ctx)
exportDirectory.mkdirs()
val file = Exports.exportData(exportDirectory, ctx)
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
val intent = Intent(android.content.Intent.ACTION_SEND)
intent.type = "application/zip"
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
uiThread {
dismiss()
}
}
}
return@OnPreferenceClickListener true
}
private fun importClickListener() = Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/zip"
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
return@OnPreferenceClickListener true
}
private fun statusClickListener() = Preference.OnPreferenceClickListener {
val activity = activity as MainActivity
if (activity.hasDetailPane) {
activity.setDetailView(StatusFragment())
} else {
startActivity<StatusActivity>()
}
return@OnPreferenceClickListener true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val ctx = context ?: throw IllegalStateException("No context available")
when (requestCode) {
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx)
READ_IMPORT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK && data?.data != null) {
indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
doAsync {
Exports.importData(data.data, ctx)
uiThread {
dismiss()
}
}
}
}
}
}
}
override fun onAttach(ctx: Context?) {
super.onAttach(ctx)
(ctx as? MainActivity)?.floatingActionButton?.hide()
PreferenceManager.getDefaultSharedPreferences(ctx)
.registerOnSharedPreferenceChangeListener(this)
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
}
}
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
val ctx = context ?: throw IllegalStateException("No context available")
if (node != null) {
SyncAdapter.startSync(ctx)
} else {
SyncAdapter.stopSync(ctx)
}
}
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
if (node != null) {
val ctx = context ?: throw IllegalStateException("No context available")
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
SyncAdapter.startPowSync(ctx)
} else {
SyncAdapter.stopPowSync(ctx)
}
}
}
companion object {
const val WRITE_EXPORT_REQUEST_CODE = 1
const val READ_IMPORT_REQUEST_CODE = 2
}
}

View File

@ -1,61 +0,0 @@
/*
* 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

@ -0,0 +1,54 @@
/*
* 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 ch.dissem.apps.abit.service.Singleton
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.activity_status.*
class StatusActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_status)
setSupportActionBar(toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(false)
}
MaterializeBuilder()
.withActivity(this)
.withStatusBarColorRes(R.color.colorPrimaryDark)
.withTranslucentStatusBarProgrammatically(true)
.withStatusBarPadding(true)
.build()
val bmc = Singleton.getBitmessageContext(this)
val status = StringBuilder()
for (address in bmc.addresses.getIdentities()) {
status.append(address.address).append('\n')
}
status.append('\n')
status.append(bmc.status())
content.text = status
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
class StatusFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_status, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bmc = Singleton.getBitmessageContext(
context ?: throw IllegalStateException("No context available")
)
val status = StringBuilder()
for (address in bmc.addresses.getIdentities()) {
status.append(address.address).append('\n')
}
status.append('\n')
status.append(bmc.status())
view.findViewById<TextView>(R.id.content).text = status
}
}

View File

@ -1,99 +0,0 @@
/*
* 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

@ -0,0 +1,70 @@
/*
* 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.TextView
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.BitmessageAddress
import java.util.*
/**
* @author Christian Basler
*/
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
private val data = identities.map { Selectable(it) }.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.select_identity_row, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val selectable = data[position]
holder.data = selectable
holder.checkbox.isChecked = selectable.selected
holder.checkbox.text = selectable.data.toString()
holder.address.text = selectable.data.address
}
override fun getItemCount() = data.size
class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) {
var data: Selectable<BitmessageAddress>? = null
val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!!
val address = v.findViewById<TextView>(R.id.address)!!
init {
checkbox.setOnCheckedChangeListener { _, isChecked ->
data?.selected = isChecked
}
}
}
val selected: List<BitmessageAddress>
get() {
return data
.filter { it.selected }
.mapTo(LinkedList()) { it.data }
}
}

View File

@ -14,16 +14,16 @@
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter;
package ch.dissem.apps.abit.adapter
import ch.dissem.apps.abit.util.PRNGFixes;
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography;
import ch.dissem.apps.abit.util.PRNGFixes
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
/**
* @author Christian Basler
*/
public class AndroidCryptography extends SpongyCryptography {
public AndroidCryptography() {
PRNGFixes.apply();
class AndroidCryptography : SpongyCryptography() {
init {
PRNGFixes.apply()
}
}

View File

@ -1,140 +0,0 @@
/*
* 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,128 @@
/*
* 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 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.
*/
class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable {
private val inflater = LayoutInflater.from(ctx)
private val originalData = Singleton.getAddressRepository(ctx).getContacts()
private var data: List<BitmessageAddress> = originalData
override fun getCount() = data.size
override fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val viewHolder = if (convertView == null) {
ViewHolder(inflater.inflate(R.layout.contact_row, parent, false))
} else {
convertView.tag as ViewHolder
}
val item = getItem(position)
viewHolder.avatar.setImageDrawable(Identicon(item))
viewHolder.name.text = item.toString()
viewHolder.address.text = item.address
return viewHolder.view
}
override fun getFilter(): Filter = ContactFilter()
private inner class ViewHolder(val view: View) {
val avatar = view.findViewById<ImageView>(R.id.avatar)!!
val name = view.findViewById<TextView>(R.id.name)!!
val address = view.findViewById<TextView>(R.id.address)!!
init {
view.tag = this
}
}
private inner class ContactFilter : Filter() {
override fun performFiltering(prefix: CharSequence?): Filter.FilterResults {
val results = Filter.FilterResults()
if (prefix?.isEmpty() == false) {
val prefixString = prefix.toString().toLowerCase()
val newValues = ArrayList<BitmessageAddress>()
originalData
.forEach { value ->
value.alias?.toLowerCase()?.let { alias ->
if (alias.startsWith(prefixString)) {
newValues.add(value)
} else {
val words = alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (word in words) {
if (word.startsWith(prefixString)) {
newValues.add(value)
break
}
}
}
} ?: {
val address = value.address.toLowerCase()
if (address.contains(prefixString)) {
newValues.add(value)
}
}.invoke()
}
results.values = newValues
results.count = newValues.size
} else {
results.values = originalData
results.count = originalData.size
}
return results
}
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
@Suppress("UNCHECKED_CAST")
data = results.values as List<BitmessageAddress>
if (results.count > 0) {
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}

View File

@ -14,16 +14,11 @@
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter;
package ch.dissem.apps.abit.adapter
/**
* @author Christian Basler
*/
class Selectable<T> {
final T data;
boolean selected = false;
Selectable(T data) {
this.data = data;
}
class Selectable<out T>(val data: T) {
var selected = false
}

View File

@ -1,329 +0,0 @@
/*
* 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,250 @@
/*
* 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 ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
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.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Plaintext>()
var eventListener: EventListener? = null
private val itemViewOnClickListener: View.OnClickListener
private val swipeableViewContainerOnClickListener: View.OnClickListener
private var label: Label? = null
private var selectedPosition = -1
private var activateOnItemClick: Boolean = false
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
}
interface EventListener {
fun onItemDeleted(item: Plaintext)
fun onItemArchived(item: Plaintext)
fun onItemViewClicked(v: View?)
}
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
override fun getSwipeableContainerView() = container
}
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
fun add(item: Plaintext) {
data.add(item)
notifyDataSetChanged()
}
fun addFirst(item: Plaintext) {
val index = data.size
data.addFirst(item)
notifyItemInserted(index)
}
fun addAll(items: Collection<Plaintext>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: Plaintext) {
val index = data.indexOf(item)
data.removeAll { it.id == item.id }
notifyItemRemoved(index)
}
fun update(item: Plaintext) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
private fun onItemViewClick(v: View) {
eventListener?.onItemViewClicked(v)
}
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
}
fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = data[position].id as Long
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.message_row, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
itemView.setOnClickListener(itemViewOnClickListener)
// (if the item is *not pinned*, click event comes to the container)
container.setOnClickListener(swipeableViewContainerOnClickListener)
// set data
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(Assets.getStatusDrawable(item.status))
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
sender.text = item.from.toString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.text)
if (item.isUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
override fun getItemCount() = data.size
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
} else {
REACTION_CAN_SWIPE_BOTH_H
}
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
})
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
when (result) {
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
else -> null
}
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
fun setSelectedPosition(selectedPosition: Int) {
val oldPosition = this.selectedPosition
this.selectedPosition = selectedPosition
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemDeleted(item)
}
override fun onCleanUp() {
adapter = null
}
}
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemArchived(item)
}
override fun onCleanUp() {
adapter = null
}
}
}

View File

@ -1,65 +0,0 @@
/*
* 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,48 @@
/*
* 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.preference.PreferenceManager
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
/**
* Switches between two [ProofOfWorkEngine]s depending on the configuration.
*
* @author Christian Basler
*/
class SwitchingProofOfWorkEngine(
private val ctx: Context,
private val preference: String,
private val option: ProofOfWorkEngine,
private val fallback: ProofOfWorkEngine
) : ProofOfWorkEngine, InternalContext.ContextHolder {
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
if (preferences.getBoolean(preference, false)) {
option.calculateNonce(initialHash, target, callback)
} else {
fallback.calculateNonce(initialHash, target, callback)
}
}
override fun setContext(context: InternalContext) = listOf(option, fallback)
.filterIsInstance<InternalContext.ContextHolder>()
.forEach { it.setContext(context) }
}

View File

@ -1,162 +0,0 @@
/*
* 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,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.dialog
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import ch.dissem.apps.abit.ImportIdentityActivity
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_identity.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.uiThread
/**
* @author Christian Basler
*/
class AddIdentityDialogFragment : AppCompatDialogFragment() {
private lateinit var bmc: BitmessageContext
private var parent: ViewGroup? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
bmc = Singleton.getBitmessageContext(context!!)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(R.string.add_identity)
parent = container
return inflater.inflate(R.layout.dialog_add_identity, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ok.setOnClickListener(View.OnClickListener {
val ctx = activity?.baseContext ?: throw IllegalStateException("No context available")
when (radioGroup.checkedRadioButtonId) {
R.id.create_identity -> {
Toast.makeText(ctx,
R.string.toast_long_running_operation,
Toast.LENGTH_SHORT).show()
doAsync {
val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK)
uiThread {
Toast.makeText(ctx,
R.string.toast_identity_created,
Toast.LENGTH_SHORT).show()
MainActivity.apply {
addIdentityEntry(identity)
}
}
}
}
R.id.import_identity -> startActivity<ImportIdentityActivity>()
R.id.add_chan -> addChanDialog()
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
else -> return@OnClickListener
}
dismiss()
})
dismiss.setOnClickListener { dismiss() }
}
private fun addChanDialog() {
val activity = activity ?: throw IllegalStateException("No activity available")
val ctx = activity.baseContext ?: throw IllegalStateException("No context available")
val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent)
AlertDialog.Builder(activity)
.setTitle(R.string.add_chan)
.setView(dialogView)
.setPositiveButton(R.string.ok) { _, _ ->
val passphrase = dialogView.findViewById<TextView>(R.id.passphrase)
Toast.makeText(ctx, R.string.toast_long_running_operation,
Toast.LENGTH_SHORT).show()
val pass = passphrase.text.toString()
doAsync {
val chan = bmc.createChan(pass)
chan.alias = pass
bmc.addresses.save(chan)
uiThread {
Toast.makeText(ctx,
R.string.toast_chan_created,
Toast.LENGTH_SHORT).show()
MainActivity.apply { addIdentityEntry(chan) }
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun getTheme() = R.style.FixedDialog
}

View File

@ -1,136 +0,0 @@
/*
* 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,96 @@
/*
* 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.Bundle
import android.support.v7.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
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.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
/**
* @author Christian Basler
*/
class DeterministicIdentityDialogFragment : AppCompatDialogFragment() {
private lateinit var bmc: BitmessageContext
override fun onAttach(context: Context?) {
super.onAttach(context)
bmc = Singleton.getBitmessageContext(context!!)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(R.string.add_deterministic_address)
return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false)
}
override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) {
super.onViewCreated(dialogView, savedInstanceState)
ok.setOnClickListener {
dismiss()
val context = activity?.baseContext ?: throw IllegalStateException("No context available")
val passphraseText = passphrase.text.toString()
Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show()
doAsync {
val identities = bmc.createDeterministicAddresses(
passphraseText,
number_of_identities.text.toString().toInt(),
Pubkey.LATEST_VERSION,
1L,
shorter.isChecked
)
for ((i, identity) in identities.withIndex()) {
if (identities.size == 1) {
identity.alias = label.text.toString()
} else {
identity.alias = "${label.text} (${i + 1})"
}
bmc.addresses.save(identity)
}
uiThread {
val messageRes = if (identities.size == 1) {
R.string.toast_identity_created
} else {
R.string.toast_identities_created
}
Toast.makeText(context,
messageRes,
Toast.LENGTH_SHORT).show()
MainActivity.apply {
identities.forEach { identity ->
addIdentityEntry(identity)
}
}
}
}
}
dismiss.setOnClickListener { dismiss() }
}
override fun getTheme() = R.style.FixedDialog
}

View File

@ -1,53 +0,0 @@
/*
* 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,46 @@
/*
* 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.os.Build
import android.os.Bundle
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import kotlinx.android.synthetic.main.dialog_full_node.*
/**
* @author Christian Basler
*/
class FullNodeDialogActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_full_node)
ok.setOnClickListener {
Preferences.setWifiOnly(this@FullNodeDialogActivity, false)
NetworkUtils.enableNode(applicationContext)
finish()
}
dismiss.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
NetworkUtils.scheduleNodeStart(applicationContext)
}
finish()
}
}
}

View File

@ -1,98 +0,0 @@
/*
* 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

@ -0,0 +1,74 @@
/*
* 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.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED
import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE
import kotlinx.android.synthetic.main.dialog_select_message_encoding.*
import org.slf4j.LoggerFactory
/**
* @author Christian Basler
*/
class SelectEncodingDialogFragment : AppCompatDialogFragment() {
private lateinit var encoding: Plaintext.Encoding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE
dialog.setTitle(R.string.select_encoding_title)
return inflater.inflate(R.layout.dialog_select_message_encoding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (encoding) {
SIMPLE -> radioGroup.check(R.id.simple)
EXTENDED -> radioGroup.check(R.id.extended)
else -> LOG.warn("Unexpected encoding: " + encoding)
}
ok.setOnClickListener(View.OnClickListener {
encoding = when (radioGroup.checkedRadioButtonId) {
R.id.extended -> EXTENDED
R.id.simple -> SIMPLE
else -> {
dismiss()
return@OnClickListener
}
}
val result = Intent()
result.putExtra(EXTRA_ENCODING, encoding)
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result)
dismiss()
})
dismiss.setOnClickListener { dismiss() }
}
companion object {
private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java)
}
}

View File

@ -0,0 +1,56 @@
package ch.dissem.apps.abit.drawer
import android.app.Dialog
import android.content.Context
import android.graphics.Point
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import android.widget.RelativeLayout
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.interfaces.IProfile
class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener {
override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean {
if (current) {
// Show QR code in modal dialog
val dialog = Dialog(ctx)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val imageView = ImageView(ctx)
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx)))
imageView.setOnClickListener { dialog.dismiss() }
dialog.addContentView(
imageView,
RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
val window = dialog.window
if (window != null) {
val display = window.windowManager.defaultDisplay
val size = Point()
display.getSize(size)
val dim = if (size.x < size.y) size.x else size.y
val lp = WindowManager.LayoutParams()
lp.copyFrom(window.attributes)
lp.width = dim
lp.height = dim
window.attributes = lp
}
dialog.show()
return true
}
return false
}
override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false
}

View File

@ -0,0 +1,53 @@
package ch.dissem.apps.abit.drawer
import android.content.Context
import android.content.Intent
import android.support.v4.app.FragmentManager
import android.view.View
import android.widget.Toast
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import ch.dissem.apps.abit.AddressDetailActivity
import ch.dissem.apps.abit.AddressDetailFragment
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import android.widget.Toast.LENGTH_LONG
class ProfileSelectionListener(
private val ctx: Context,
private val fragmentManager: FragmentManager
) : AccountHeader.OnAccountHeaderListener {
override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean {
when (profile.identifier.toInt()) {
MainActivity.ADD_IDENTITY -> addIdentityDialog()
MainActivity.MANAGE_IDENTITY -> {
val identity = Singleton.getIdentity(ctx)
if (identity == null) {
Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show()
} else {
val show = Intent(ctx, AddressDetailActivity::class.java)
show.putExtra(AddressDetailFragment.ARG_ITEM, identity)
ctx.startActivity(show)
}
}
else -> if (profile is ProfileDrawerItem) {
val tag = profile.tag
if (tag is BitmessageAddress) {
Singleton.setIdentity(tag)
}
}
}
// false if it should close the drawer
return false
}
private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog")
}

View File

@ -1,26 +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.listener;
/**
* @author Christian Basler
*/
public interface ActionBarListener {
void updateTitle(CharSequence title);
void updateUnread();
}

View File

@ -14,16 +14,16 @@
* limitations under the License.
*/
package ch.dissem.apps.abit.listener;
package ch.dissem.apps.abit.listener
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface ListSelectionListener<T> {
interface ListSelectionListener<in T> {
/**
* Callback for when an item has been selected.
*/
void onItemSelected(T item);
fun onItemSelected(item: T)
}

View File

@ -1,85 +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.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,68 @@
/*
* 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 ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.notification.NewMessageNotification
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
import java.util.*
import java.util.concurrent.Executors
/**
* Listens for decrypted Bitmessage messages. Does show a notification.
*
*
* Should show a notification when the app isn't running, but update the message list when it is.
* Also,
* notifications should be combined.
*
*/
class MessageListener(ctx: Context) : BitmessageContext.Listener {
private val unacknowledged = LinkedList<Plaintext>()
private var numberOfUnacknowledgedMessages = 0
private val notification = NewMessageNotification(ctx)
private val pool = Executors.newSingleThreadExecutor()
override fun receive(plaintext: Plaintext) {
pool.submit {
unacknowledged.addFirst(plaintext)
numberOfUnacknowledgedMessages++
if (unacknowledged.size > 5) {
unacknowledged.removeLast()
}
if (numberOfUnacknowledgedMessages == 1) {
notification.singleNotification(plaintext)
} else {
notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages)
}
notification.show()
// If MainActivity is shown, update the sidebar badges
MainActivity.apply { updateUnread() }
}
}
fun resetNotification() {
pool.submit {
notification.hide()
unacknowledged.clear()
numberOfUnacknowledgedMessages = 0
}
}
}

View File

@ -1,62 +0,0 @@
/*
* 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

@ -0,0 +1,36 @@
/*
* 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 ch.dissem.apps.abit.service.BitmessageService
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Preferences
import org.jetbrains.anko.connectivityManager
class WifiReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
val bmc = Singleton.getBitmessageContext(ctx)
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
ctx.startService(Intent(ctx, BitmessageService::class.java))
}
}
}
}

View File

@ -1,53 +0,0 @@
/*
* 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,49 @@
/*
* 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
import org.jetbrains.anko.notificationManager
/**
* Some base class to create and handle notifications.
*/
abstract class AbstractNotification(ctx: Context) {
protected val ctx = ctx.applicationContext!!
private val manager = ctx.notificationManager
var notification: Notification? = null
protected set
protected var showing = false
private set
/**
* @return an id unique to this notification class
*/
protected abstract val notificationId: Int
open fun show() {
manager.notify(notificationId, notification)
showing = true
}
fun hide() {
showing = false
manager.cancel(notificationId)
}
}

View File

@ -1,61 +0,0 @@
/*
* 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,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.notification
import android.content.Context
import android.support.annotation.StringRes
import android.support.v4.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
*/
class ErrorNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.error")
.setContentTitle(ctx.getString(R.string.app_name))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification {
builder.setSmallIcon(R.drawable.ic_notification_warning)
.setContentText(ctx.getString(resId, *args))
notification = builder.build()
return this
}
fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification {
builder.setSmallIcon(R.drawable.ic_notification_error)
.setContentText(ctx.getString(resId, *args))
notification = builder.build()
return this
}
override val notificationId = ERROR_NOTIFICATION_ID
companion object {
val ERROR_NOTIFICATION_ID = 4
}
}

View File

@ -1,143 +0,0 @@
/*
* 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,128 @@
/*
* 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.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
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 java.util.*
import kotlin.concurrent.fixedRateTimer
/**
* Shows the network status (as long as the client is connected as a full node)
*/
class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.network")
private var timer: Timer? = null
init {
val showAppIntent = Intent(ctx, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0)
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")
private fun update(): Boolean {
val running = BitmessageService.isRunning
builder.setOngoing(running)
val connections = BitmessageService.status.getProperty("network", "connections")
if (!running) {
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
} else if (connections == null || connections.properties.isEmpty()) {
builder.setContentText(ctx.getString(R.string.connection_info_pending))
} else {
val info = StringBuilder()
for (stream in connections.properties) {
val streamNumber = Integer.parseInt(stream.name.substring("stream ".length))
val nodeCount = stream.getProperty("nodes")!!.value as Int?
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()
val intent = Intent(ctx, BitmessageIntentService::class.java)
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 fun show() {
super.show()
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
if (!update()) {
cancel()
ctx.stopService(Intent(ctx, BitmessageService::class.java))
}
super@NetworkNotification.show()
}
}
fun showShutdown() {
timer?.cancel()
update()
super.show()
}
override val notificationId = NETWORK_NOTIFICATION_ID
fun connecting() {
builder.setOngoing(true)
builder.setContentText(ctx.getString(R.string.connection_info_pending))
val intent = Intent(ctx, BitmessageIntentService::class.java)
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()
}
companion object {
val NETWORK_NOTIFICATION_ID = 2
}
}

View File

@ -1,122 +0,0 @@
/*
* 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,118 @@
/*
* 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.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.app.NotificationCompat.InboxStyle
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.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.bitmessage.entity.Plaintext
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
import ch.dissem.apps.abit.util.Drawables.toBitmap
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
fun singleNotification(plaintext: Plaintext): NewMessageNotification {
val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text)
plaintext.subject?.let { subject ->
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setLargeIcon(toBitmap(Identicon(plaintext.from), 192))
.setContentTitle(plaintext.from.toString())
.setContentText(plaintext.subject)
.setStyle(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 fun createActivityIntent(action: String, message: Plaintext): PendingIntent {
val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra(action, message)
return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT)
}
private fun createServiceIntent(ctx: Context, action: String, message: Plaintext): PendingIntent {
val intent = Intent(ctx, BitmessageIntentService::class.java)
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 `synchronized(unacknowledged)
* * {}` block
*/
fun multiNotification(unacknowledged: Collection<Plaintext>, numberOfUnacknowledgedMessages: Int): NewMessageNotification {
val builder = NotificationCompat.Builder(ctx, CHANNEL_ID)
builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages))
.setContentText(ctx.getString(R.string.app_name))
val inboxStyle = InboxStyle()
synchronized(unacknowledged) {
for (msg in unacknowledged) {
val sb = SpannableString(msg.from.toString() + " " + msg.subject)
sb.setSpan(SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable
.SPAN_INCLUSIVE_EXCLUSIVE)
inboxStyle.addLine(sb)
}
}
builder.setStyle(inboxStyle)
val intent = Intent(ctx, MainActivity::class.java)
intent.action = MainActivity.ACTION_SHOW_INBOX
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
builder.setContentIntent(pendingIntent)
notification = builder.build()
return this
}
override val notificationId = NEW_MESSAGE_NOTIFICATION_ID
companion object {
private val NEW_MESSAGE_NOTIFICATION_ID = 1
private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD)
private val CHANNEL_ID = "abit.message"
}
}

View File

@ -1,63 +0,0 @@
/*
* 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,104 @@
/*
* 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.v4.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.ProofOfWorkService
import ch.dissem.apps.abit.util.PowStats
import java.util.*
import kotlin.concurrent.fixedRateTimer
/**
* Ongoing notification while proof of work is in progress.
*/
class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) {
private val builder = NotificationCompat.Builder(ctx, "abit.pow")
.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))
private var startTime = 0L
private var progress = 0
private var progressMax = 0
private var timer: Timer? = null
init {
update(0)
}
override val notificationId = ONGOING_NOTIFICATION_ID
fun update(numberOfItems: Int): ProofOfWorkNotification {
val showMessageIntent = Intent(ctx, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
builder.setContentText(if (numberOfItems == 0)
ctx.getString(R.string.proof_of_work_text_0)
else
ctx.getString(R.string.proof_of_work_text_n, numberOfItems))
.setContentIntent(pendingIntent)
notification = builder.build()
return this
}
companion object {
const val ONGOING_NOTIFICATION_ID = 3
}
fun start(item: ProofOfWorkService.PowItem) {
val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue)
val delta = (expectedPowTimeInMilliseconds / 3).toInt()
startTime = System.currentTimeMillis()
progress = 0
progressMax = delta
builder.setProgress(progressMax, progress, false)
notification = builder.build()
show()
timer = fixedRateTimer(initialDelay = 2000, period = 2000){
val elapsedTime = System.currentTimeMillis() - startTime
progress = elapsedTime.toInt()
progressMax = progress + delta
builder.setProgress(progressMax, progress, false)
notification = builder.build()
show()
}
}
fun finished() {
timer?.cancel()
progress = 0
progressMax = 0
if (showing) {
builder.setProgress(0, 0, false)
notification = builder.build()
show()
}
}
}

View File

@ -1,97 +0,0 @@
/*
* 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

@ -0,0 +1,83 @@
/*
* 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 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.extensions.CryptoCustomMessage
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import ch.dissem.bitmessage.utils.Singleton.cryptography
import org.slf4j.LoggerFactory
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* @author Christian Basler
*/
class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder {
private lateinit var context: InternalContext
private val pool: ExecutorService
init {
pool = Executors.newCachedThreadPool { r ->
val thread = Executors.defaultThreadFactory().newThread(r)
thread.priority = Thread.MIN_PRIORITY
thread
}
}
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) =
pool.execute {
val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW")
val request = ProofOfWorkRequest(identity, initialHash,
CALCULATE, target)
SyncAdapter.startPowSync(ctx)
try {
val cryptoMsg = CryptoCustomMessage(request)
cryptoMsg.signAndEncrypt(
identity,
cryptography().createPublicKey(identity.publicDecryptionKey)
)
val node = Preferences.getTrustedNode(ctx)
if (node == null) {
LOG.error("trusted node is not defined")
} else {
context.networkHandler.send(
node,
Preferences.getTrustedNodePort(ctx),
cryptoMsg)
}
} catch (e: Exception) {
LOG.error(e.message, e)
}
}
override fun setContext(context: InternalContext) {
this.context = context
}
companion object {
private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java)
}
}

View File

@ -1,261 +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.repository;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.utils.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* {@link AddressRepository} implementation using the Android SQL API.
*/
public class AndroidAddressRepository implements AddressRepository {
private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class);
private static final String TABLE_NAME = "Address";
private static final String COLUMN_ADDRESS = "address";
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_ALIAS = "alias";
private static final String COLUMN_PUBLIC_KEY = "public_key";
private static final String COLUMN_PRIVATE_KEY = "private_key";
private static final String COLUMN_SUBSCRIBED = "subscribed";
private static final String COLUMN_CHAN = "chan";
private final SqlHelper sql;
public AndroidAddressRepository(SqlHelper sql) {
this.sql = sql;
}
@Override
public BitmessageAddress findContact(byte[] ripeOrTag) {
for (BitmessageAddress address : find("public_key is null")) {
if (address.getVersion() > 3) {
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
} else {
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
}
}
return null;
}
@Override
public BitmessageAddress findIdentity(byte[] ripeOrTag) {
for (BitmessageAddress address : find("private_key is not null")) {
if (address.getVersion() > 3) {
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
} else {
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
}
}
return null;
}
@Override
public List<BitmessageAddress> getIdentities() {
return find("private_key IS NOT NULL");
}
@Override
public List<BitmessageAddress> getChans() {
return find("chan = '1'");
}
@Override
public List<BitmessageAddress> getSubscriptions() {
return find("subscribed = '1'");
}
@Override
public List<BitmessageAddress> getSubscriptions(long broadcastVersion) {
if (broadcastVersion > 4) {
return find("subscribed = '1' AND version > 3");
} else {
return find("subscribed = '1' AND version <= 3");
}
}
@Override
public List<BitmessageAddress> getContacts() {
return find("private_key IS NULL OR chan = '1'");
}
private List<BitmessageAddress> find(String where) {
List<BitmessageAddress> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_ADDRESS,
COLUMN_ALIAS,
COLUMN_PUBLIC_KEY,
COLUMN_PRIVATE_KEY,
COLUMN_SUBSCRIBED,
COLUMN_CHAN
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null, null
)) {
while (c.moveToNext()) {
BitmessageAddress address;
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
if (privateKeyBytes != null) {
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream
(privateKeyBytes));
address = new BitmessageAddress(privateKey);
} else {
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
if (publicKeyBytes != null) {
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address
.getStream(),
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length,
false);
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
pubkey = new V4Pubkey((V3Pubkey) pubkey);
}
address.setPubkey(pubkey);
}
}
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS)));
address.setChan(c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1);
address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
result.add(address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return result;
}
@Override
public void save(BitmessageAddress address) {
if (exists(address)) {
update(address);
} else {
insert(address);
}
}
private boolean exists(BitmessageAddress address) {
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor cursor = db.rawQuery(
"SELECT COUNT(*) FROM Address WHERE address=?",
new String[]{address.getAddress()}
)) {
cursor.moveToFirst();
return cursor.getInt(0) > 0;
}
}
private void update(BitmessageAddress address) {
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
if (address.getAlias() != null) {
values.put(COLUMN_ALIAS, address.getAlias());
}
if (address.getPubkey() != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
address.getPubkey().writeUnencrypted(out);
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
}
if (address.getPrivateKey() != null) {
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
}
if (address.isChan()) {
values.put(COLUMN_CHAN, true);
}
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
int update = db.update(TABLE_NAME, values, "address=?",
new String[]{address.getAddress()});
if (update < 0) {
LOG.error("Could not update address " + address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
private void insert(BitmessageAddress address) {
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(COLUMN_ADDRESS, address.getAddress());
values.put(COLUMN_VERSION, address.getVersion());
values.put(COLUMN_ALIAS, address.getAlias());
if (address.getPubkey() != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
address.getPubkey().writeUnencrypted(out);
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
} else {
values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
}
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
values.put(COLUMN_CHAN, address.isChan());
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
long insert = db.insert(TABLE_NAME, null, values);
if (insert < 0) {
LOG.error("Could not insert address " + address);
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
@Override
public void remove(BitmessageAddress address) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()});
}
@Override
public BitmessageAddress getAddress(String address) {
List<BitmessageAddress> result = find("address = '" + address + "'");
if (result.size() > 0) return result.get(0);
return null;
}
}

View File

@ -0,0 +1,227 @@
/*
* 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.database.Cursor
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.V3Pubkey
import ch.dissem.bitmessage.entity.payload.V4Pubkey
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
import ch.dissem.bitmessage.factory.Factory
import ch.dissem.bitmessage.ports.AddressRepository
import ch.dissem.bitmessage.utils.Encode
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.*
/**
* [AddressRepository] implementation using the Android SQL API.
*/
class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository {
override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag)
override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag)
private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? {
for (address in find(where)) {
if (address.version > 3) {
if (Arrays.equals(ripeOrTag, address.tag)) return address
} else {
if (Arrays.equals(ripeOrTag, address.ripe)) return address
}
}
return null
}
override fun getIdentities() = find("private_key IS NOT NULL")
override fun getChans() = find("chan = '1'")
override fun getSubscriptions() = find("subscribed = '1'")
override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) {
find("subscribed = '1' AND version > 3")
} else {
find("subscribed = '1' AND version <= 3")
}
override fun getContacts() = find("private_key IS NULL OR chan = '1'")
/**
* Returns the contacts in the following order:
*
* * Subscribed addresses come first
* * Addresses with Aliases (alphabetically)
* * Addresses (alphabetically)
*
*
* @return the ordered list of ids (address strings)
*/
fun getContactIds(): List<String> = findIds(
"private_key IS NULL OR chan = '1'",
"$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS IS NULL, $COLUMN_ALIAS, $COLUMN_ADDRESS"
)
private fun findIds(where: String, orderBy: String): List<String> {
val result = LinkedList<String>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null,
orderBy
).use { c ->
while (c.moveToNext()) {
result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS)))
}
}
return result
}
private fun find(where: String): List<BitmessageAddress> {
val result = LinkedList<BitmessageAddress>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null, null
).use { c ->
while (c.moveToNext()) {
result.add(getAddress(c))
}
}
return result
}
private fun getAddress(c: Cursor): BitmessageAddress {
fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let {
BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it)))
}
fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address ->
c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes ->
Factory.readPubkey(
version = address.version, stream = address.stream,
input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size,
encrypted = false
).let {
address.pubkey = if (address.version == 4L && it is V3Pubkey) {
V4Pubkey(it)
} else {
it
}
}
}
}
return (getIdentity(c) ?: getContact(c)).apply {
alias = c.getString(c.getColumnIndex(COLUMN_ALIAS))
isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1
isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1
}
}
override fun save(address: BitmessageAddress) = if (exists(address)) {
update(address)
} else {
insert(address)
}
private fun exists(address: BitmessageAddress): Boolean {
val db = sql.readableDatabase
db.rawQuery(
"SELECT COUNT(*) FROM Address WHERE address=?",
arrayOf(address.address)
).use { cursor ->
cursor.moveToFirst()
return cursor.getInt(0) > 0
}
}
private fun update(address: BitmessageAddress) {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = getContentValues(address)
val update = db.update(TABLE_NAME, values, "address=?", arrayOf(address.address))
if (update < 0) {
LOG.error("Could not update address {}", address)
}
}
private fun insert(address: BitmessageAddress) {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = getContentValues(address)
values.put(COLUMN_ADDRESS, address.address)
values.put(COLUMN_VERSION, address.version)
values.put(COLUMN_CHAN, address.isChan)
val insert = db.insert(TABLE_NAME, null, values)
if (insert < 0) {
LOG.error("Could not insert address {}", address)
}
}
private fun getContentValues(address: BitmessageAddress): ContentValues {
val values = ContentValues()
address.alias?.let { values.put(COLUMN_ALIAS, it) }
address.pubkey?.let { pubkey ->
val out = ByteArrayOutputStream()
pubkey.writer().writeUnencrypted(out)
values.put(COLUMN_PUBLIC_KEY, out.toByteArray())
}
address.privateKey?.let { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) }
if (address.isChan) {
values.put(COLUMN_CHAN, true)
}
values.put(COLUMN_SUBSCRIBED, address.isSubscribed)
return values
}
override fun remove(address: BitmessageAddress) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "address = ?", arrayOf(address.address))
}
override fun getAddress(address: String) = find("address = '$address'").firstOrNull()
companion object {
private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java)
private const val TABLE_NAME = "Address"
private const val COLUMN_ADDRESS = "address"
private const val COLUMN_VERSION = "version"
private const val COLUMN_ALIAS = "alias"
private const val COLUMN_PUBLIC_KEY = "public_key"
private const val COLUMN_PRIVATE_KEY = "private_key"
private const val COLUMN_SUBSCRIBED = "subscribed"
private const val COLUMN_CHAN = "chan"
}
}

View File

@ -1,232 +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.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.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.ObjectType;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.Inventory;
import ch.dissem.bitmessage.utils.Encode;
import static ch.dissem.apps.abit.repository.SqlHelper.join;
import static ch.dissem.bitmessage.utils.UnixTime.MINUTE;
import static ch.dissem.bitmessage.utils.UnixTime.now;
import static java.lang.String.valueOf;
/**
* {@link Inventory} implementation using the Android SQL API.
*/
public class AndroidInventory implements Inventory {
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
private static final String TABLE_NAME = "Inventory";
private static final String COLUMN_HASH = "hash";
private static final String COLUMN_STREAM = "stream";
private static final String COLUMN_EXPIRES = "expires";
private static final String COLUMN_DATA = "data";
private static final String COLUMN_TYPE = "type";
private static final String COLUMN_VERSION = "version";
private final SqlHelper sql;
private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>();
public AndroidInventory(SqlHelper sql) {
this.sql = sql;
}
@Override
public List<InventoryVector> getInventory(long... streams) {
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;
}
private Map<InventoryVector, Long> getCache(long stream) {
Map<InventoryVector, Long> result = cache.get(stream);
if (result == null) {
synchronized (cache) {
if (cache.get(stream) == null) {
result = new ConcurrentHashMap<>();
cache.put(stream, result);
String[] projection = {
COLUMN_HASH, COLUMN_EXPIRES
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
"stream = " + stream,
null, null, null, null
)) {
while (c.moveToNext()) {
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES));
result.put(new InventoryVector(blob), expires);
}
}
LOG.info("Stream #" + stream + " inventory size: " + result.size());
}
}
}
return result;
}
@Override
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) {
for (long stream : streams) {
offer.removeAll(getCache(stream).keySet());
}
LOG.info(offer.size() + " objects missing.");
return offer;
}
@Override
public ObjectMessage getObject(InventoryVector vector) {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_VERSION,
COLUMN_DATA
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
"hash = X'" + vector + "'",
null, null, null, null
)) {
if (!c.moveToFirst()) {
LOG.info("Object requested that we don't have. IV: " + vector);
return null;
}
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
}
}
@Override
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_VERSION,
COLUMN_DATA
};
StringBuilder where = new StringBuilder("1=1");
if (stream > 0) {
where.append(" AND stream = ").append(stream);
}
if (version > 0) {
where.append(" AND version = ").append(version);
}
if (types.length > 0) {
where.append(" AND type IN (").append(join(types)).append(")");
}
SQLiteDatabase db = sql.getReadableDatabase();
List<ObjectMessage> result = new LinkedList<>();
try (Cursor c = db.query(
TABLE_NAME, projection,
where.toString(),
null, null, null, null
)) {
while (c.moveToNext()) {
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob),
blob.length));
}
}
return result;
}
@Override
public void storeObject(ObjectMessage object) {
InventoryVector iv = object.getInventoryVector();
if (getCache(object.getStream()).containsKey(iv))
return;
LOG.trace("Storing object " + iv);
try {
SQLiteDatabase db = sql.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(COLUMN_HASH, object.getInventoryVector().getHash());
values.put(COLUMN_STREAM, object.getStream());
values.put(COLUMN_EXPIRES, object.getExpiresTime());
values.put(COLUMN_DATA, Encode.bytes(object));
values.put(COLUMN_TYPE, object.getType());
values.put(COLUMN_VERSION, object.getVersion());
db.insertOrThrow(TABLE_NAME, null, values);
getCache(object.getStream()).put(iv, object.getExpiresTime());
} catch (SQLiteConstraintException e) {
LOG.trace(e.getMessage(), e);
}
}
@Override
public boolean contains(ObjectMessage object) {
return getCache(object.getStream()).keySet().contains(object.getInventoryVector());
}
@Override
public void cleanup() {
long fiveMinutesAgo = now() - 5 * MINUTE;
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "expires < ?", 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,182 @@
/*
* 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.database.sqlite.SQLiteConstraintException
import ch.dissem.bitmessage.entity.ObjectMessage
import ch.dissem.bitmessage.entity.payload.ObjectType
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.factory.Factory
import ch.dissem.bitmessage.ports.Inventory
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
import ch.dissem.bitmessage.utils.UnixTime.now
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* [Inventory] implementation using the Android SQL API.
*/
class AndroidInventory(private val sql: SqlHelper) : Inventory {
private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>()
override fun getInventory(vararg streams: Long): List<InventoryVector> {
val result = LinkedList<InventoryVector>()
val now = now
for (stream in streams) {
for ((key, value) in getCache(stream)) {
if (value > now) {
result.add(key)
}
}
}
return result
}
private fun getCache(stream: Long): MutableMap<InventoryVector, Long> {
fun addToCache(stream: Long): MutableMap<InventoryVector, Long> {
val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap()
cache.put(stream, result)
val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"stream = $stream", null, null, null, null
).use { c ->
while (c.moveToNext()) {
val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH))
val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES))
InventoryVector.fromHash(blob)?.let { result.put(it, expires) }
}
}
LOG.info("Stream #$stream inventory size: ${result.size}")
return result
}
return cache[stream] ?: synchronized(cache) {
return@synchronized cache[stream] ?: addToCache(stream)
}
}
override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys }
override fun getObject(vector: InventoryVector): ObjectMessage? {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"hash = X'$vector'", null, null, null, null
).use { c ->
if (!c.moveToFirst()) {
LOG.info("Object requested that we don't have. IV: {}", vector)
return null
}
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size)
}
}
override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA)
val where = StringBuilder("1=1")
if (stream > 0) {
where.append(" AND stream = ").append(stream)
}
if (version > 0) {
where.append(" AND version = ").append(version)
}
if (types.isNotEmpty()) {
where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")")
}
val db = sql.readableDatabase
val result = LinkedList<ObjectMessage>()
db.query(
TABLE_NAME, projection,
where.toString(), null, null, null, null
).use { c ->
while (c.moveToNext()) {
val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) }
}
}
return result
}
override fun storeObject(objectMessage: ObjectMessage) {
val iv = objectMessage.inventoryVector
if (getCache(objectMessage.stream).containsKey(iv))
return
LOG.trace("Storing object {}", iv)
try {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_HASH, objectMessage.inventoryVector.hash)
values.put(COLUMN_STREAM, objectMessage.stream)
values.put(COLUMN_EXPIRES, objectMessage.expiresTime)
values.put(COLUMN_DATA, Encode.bytes(objectMessage))
values.put(COLUMN_TYPE, objectMessage.type)
values.put(COLUMN_VERSION, objectMessage.version)
db.insertOrThrow(TABLE_NAME, null, values)
getCache(objectMessage.stream).put(iv, objectMessage.expiresTime)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector)
override fun cleanup() {
val fiveMinutesAgo = now - 5 * MINUTE
val db = sql.writableDatabase
db.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString()))
cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } }
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
private const val TABLE_NAME = "Inventory"
private const val COLUMN_HASH = "hash"
private const val COLUMN_STREAM = "stream"
private const val COLUMN_EXPIRES = "expires"
private const val COLUMN_DATA = "data"
private const val COLUMN_TYPE = "type"
private const val COLUMN_VERSION = "version"
}
}

View File

@ -0,0 +1,109 @@
/*
* 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 ch.dissem.apps.abit.util.Labels
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractLabelRepository
import ch.dissem.bitmessage.ports.MessageRepository
import org.jetbrains.anko.db.transaction
import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) : AbstractLabelRepository() {
override fun find(where: String): List<Label> {
val result = LinkedList<Label>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR)
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
COLUMN_ORDER
).use { c ->
while (c.moveToNext()) {
result.add(getLabel(c, context))
}
}
return result
}
override fun save(label: Label) {
val db = sql.writableDatabase
if (label.id != null) {
val values = ContentValues()
values.put(COLUMN_LABEL, label.toString())
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
} else {
db.transaction {
val exists = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "label=?", arrayOf(label.toString())) > 0
if (exists) {
val values = ContentValues()
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString()))
} else {
val values = ContentValues()
values.put(COLUMN_LABEL, label.toString())
values.put(COLUMN_TYPE, label.type?.name)
values.put(COLUMN_COLOR, label.color)
values.put(COLUMN_ORDER, label.ord)
db.insertOrThrow(TABLE_NAME, null, values)
}
}
}
}
internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
companion object {
val LABEL_ARCHIVE = Label("archive", null, 0)
private const val TABLE_NAME = "Label"
private const val COLUMN_ID = "id"
private const val COLUMN_LABEL = "label"
private const val COLUMN_TYPE = "type"
private const val COLUMN_COLOR = "color"
private const val COLUMN_ORDER = "ord"
internal fun getLabel(c: Cursor, context: Context): Label {
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
val text: String? = Labels.getText(type, null, context)
val label = Label(
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
type,
c.getInt(c.getColumnIndex(COLUMN_COLOR)))
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
return label
}
}
}

View File

@ -1,386 +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.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,266 @@
/*
* 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.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractMessageRepository
import ch.dissem.bitmessage.ports.AlreadyStoredException
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Strings.hex
import org.jetbrains.anko.db.transaction
import java.io.ByteArrayInputStream
import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
super.findMessages(null as Label?, offset, limit)
} else {
super.findMessages(label, offset, limit)
}
override fun countUnread(label: Label?) = when {
label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(Label.Type.UNREAD.name)
).toInt()
else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
).toInt()
}
override fun findConversations(label: Label?): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
label == null -> null
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
}
val result = LinkedList<UUID>()
sql.readableDatabase.query(
true,
TABLE_NAME, projection, where,
null, null, null, null, null
).use { c ->
while (c.moveToNext()) {
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
result.add(asUuid(uuidBytes))
}
}
return result
}
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
val inventoryVector = message.inventoryVector
if (inventoryVector == null || message.parents.isEmpty()) {
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
return
}
val childIV = inventoryVector.hash
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
// save new parents
var order = 0
val values = ContentValues()
for (parentIV in message.parents) {
getMessage(parentIV)?.let { parent ->
mergeConversations(db, parent.conversationId, message.conversationId)
order++
values.put("parent", parentIV.hash)
values.put("child", childIV)
values.put("pos", order)
values.put("conversation", UuidUtils.asBytes(message.conversationId))
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 fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
val values = ContentValues()
values.put("conversation", UuidUtils.asBytes(target))
val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
db.update(TABLE_NAME, values, where, null)
db.update(PARENTS_TABLE_NAME, values, where, null)
}
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> {
val result = LinkedList<Plaintext>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(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)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
while (c.moveToNext()) {
result.add(getMessage(c))
}
}
return result
}
private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature(
Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))),
ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA)))
).build {
id = c.getLong(c.getColumnIndex(COLUMN_ID))
inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV)))
c.getString(c.getColumnIndex(COLUMN_SENDER))?.let {
from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
}
c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let {
to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it)
}
ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))
sent = c.getLong(c.getColumnIndex(COLUMN_SENT))
received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED))
status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS)))
ttl = c.getLong(c.getColumnIndex(COLUMN_TTL))
retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES))
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
if (!c.isNull(nextTryColumn)) {
nextTry = c.getLong(nextTryColumn)
}
conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))
labels = findLabels(id!!)
}
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
override fun save(message: Plaintext) {
saveContactIfNecessary(message.from)
saveContactIfNecessary(message.to)
val db = sql.writableDatabase
db.transaction {
// save message
if (message.id == null) {
insert(db, message)
} else {
update(db, message)
}
updateParents(db, message)
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
// save labels
val values = ContentValues()
for (label in message.labels) {
values.put(JT_COLUMN_LABEL, label.id as Long?)
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
}
}
}
private fun getValues(message: Plaintext): ContentValues {
val values = ContentValues()
values.put(COLUMN_IV, message.inventoryVector?.hash)
values.put(COLUMN_TYPE, message.type.name)
values.put(COLUMN_SENDER, message.from.address)
values.put(COLUMN_RECIPIENT, message.to?.address)
values.put(COLUMN_DATA, Encode.bytes(message))
values.put(COLUMN_ACK_DATA, message.ackData)
values.put(COLUMN_SENT, message.sent)
values.put(COLUMN_RECEIVED, message.received)
values.put(COLUMN_STATUS, message.status.name)
values.put(COLUMN_INITIAL_HASH, message.initialHash)
values.put(COLUMN_TTL, message.ttl)
values.put(COLUMN_RETRIES, message.retries)
values.put(COLUMN_NEXT_TRY, message.nextTry)
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
return values
}
private fun insert(db: SQLiteDatabase, message: Plaintext) {
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
message.id = id
}
private fun update(db: SQLiteDatabase, message: Plaintext) {
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
}
override fun remove(message: Plaintext) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
}
companion object {
private const val TABLE_NAME = "Message"
private const val COLUMN_ID = "id"
private const val COLUMN_IV = "iv"
private const val COLUMN_TYPE = "type"
private const val COLUMN_SENDER = "sender"
private const val COLUMN_RECIPIENT = "recipient"
private const val COLUMN_DATA = "data"
private const val COLUMN_ACK_DATA = "ack_data"
private const val COLUMN_SENT = "sent"
private const val COLUMN_RECEIVED = "received"
private const val COLUMN_STATUS = "status"
private const val COLUMN_TTL = "ttl"
private const val COLUMN_RETRIES = "retries"
private const val COLUMN_NEXT_TRY = "next_try"
private const val COLUMN_INITIAL_HASH = "initial_hash"
private const val COLUMN_CONVERSATION = "conversation"
private const val PARENTS_TABLE_NAME = "Message_Parent"
private const val JOIN_TABLE_NAME = "Message_Label"
private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id"
}
}

View File

@ -1,194 +0,0 @@
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,202 @@
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDoneException
import android.database.sqlite.SQLiteStatement
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress
import ch.dissem.bitmessage.exception.ApplicationException
import ch.dissem.bitmessage.ports.NodeRegistry
import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes
import ch.dissem.bitmessage.utils.Collections
import ch.dissem.bitmessage.utils.SqlStrings
import ch.dissem.bitmessage.utils.Strings.hex
import ch.dissem.bitmessage.utils.UnixTime
import ch.dissem.bitmessage.utils.UnixTime.DAY
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
import ch.dissem.bitmessage.utils.UnixTime.now
import ch.dissem.bitmessage.utils.max
import org.jetbrains.anko.db.transaction
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.concurrent.getOrSet
const val MAX_ENTRY_AGE = 7 * DAY
/**
* @author Christian Basler
*/
class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry {
private val loadExistingStatement = ThreadLocal<SQLiteStatement>()
private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap()
get() {
if (field.isEmpty())
field = loadStableNodes()
return field
}
init {
cleanUp()
}
private fun cleanUp() {
sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString()))
}
override fun clear() {
sql.writableDatabase.delete(TABLE_NAME, null, null)
}
private fun loadExistingTime(node: NetworkAddress): Long? {
val statement: SQLiteStatement = loadExistingStatement.getOrSet {
sql.writableDatabase.compileStatement(
"SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?"
)
}
statement.bindLong(1, node.stream)
statement.bindBlob(2, node.IPv6)
statement.bindLong(3, node.port.toLong())
return try {
statement.simpleQueryForLong()
} catch (e: SQLiteDoneException) {
null
}
}
override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> {
val result = LinkedList<NetworkAddress>()
sql.readableDatabase.query(
TABLE_NAME,
arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME),
"stream IN (?)",
arrayOf(SqlStrings.join(*streams)), null, null,
"time DESC",
limit.toString()
).use { c ->
while (c.moveToNext()) {
result.add(NetworkAddress(
time = c.getLong(c.getColumnIndex(COLUMN_TIME)),
stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)),
services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)),
IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)),
port = c.getInt(c.getColumnIndex(COLUMN_PORT))
))
}
}
if (result.isEmpty()) {
streams
.asSequence()
.mapNotNull { stableNodes[it] }
.filterNot { it.isEmpty() }
.mapTo(result) { Collections.selectRandom(it) }
}
return result
}
override fun offerAddresses(nodes: List<NetworkAddress>) {
sql.writableDatabase.transaction {
cleanUp()
nodes
.filter {
// Don't accept nodes from the future, it might be a trap
it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE
}
.forEach { node ->
synchronized(this) {
val existing = loadExistingTime(node)
if (existing == null) {
insert(node)
} else if (node.time > existing) {
update(node)
}
}
}
}
}
private fun insert(node: NetworkAddress) {
try {
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_STREAM, node.stream)
values.put(COLUMN_ADDRESS, node.IPv6)
values.put(COLUMN_PORT, node.port)
values.put(COLUMN_SERVICES, node.services)
values.put(COLUMN_TIME,
if (node.time > UnixTime.now) {
// This might be an attack, let's not use those nodes with priority
UnixTime.now - 7 * UnixTime.DAY
} else {
node.time
}
)
sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun update(node: NetworkAddress) {
try {
val time = if (node.time > UnixTime.now) {
// This might be an attack, let's not use those nodes with priority
UnixTime.now - 7 * UnixTime.DAY
} else {
node.time
}
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_SERVICES, node.services)
values.put(COLUMN_TIME, max(node.time, time))
sql.writableDatabase.update(
TABLE_NAME,
values,
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun remove(node: NetworkAddress) {
try {
sql.writableDatabase.delete(
TABLE_NAME,
"stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun cleanup() {
try {
sql.writableDatabase.delete(
TABLE_NAME,
"time<${UnixTime.now - 8 * DAY}",
null
)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java)
private const val TABLE_NAME = "Node"
private const val COLUMN_STREAM = "stream"
private const val COLUMN_ADDRESS = "address"
private const val COLUMN_PORT = "port"
private const val COLUMN_SERVICES = "services"
private const val COLUMN_TIME = "time"
}
}

View File

@ -1,172 +0,0 @@
/*
* 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

@ -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.repository
import android.content.ContentValues
import android.database.sqlite.SQLiteConstraintException
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.LinkedList
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 ch.dissem.bitmessage.utils.Singleton.cryptography
import ch.dissem.bitmessage.utils.Strings.hex
/**
* @author Christian Basler
*/
class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder {
private lateinit var bmc: InternalContext
override fun setContext(context: InternalContext) {
this.bmc = context
}
override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
"initial_hash=X'${hex(initialHash)}'",
null, null, null, null
).use { c ->
if (c.moveToFirst()) {
val version = c.getInt(c.getColumnIndex(COLUMN_VERSION))
val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA))
return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) {
ProofOfWorkRepository.Item(
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES))
)
} else {
ProofOfWorkRepository.Item(
Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"),
c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)),
c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)),
c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)),
bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID)))
)
}
}
}
throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}")
}
override fun getItems(): List<ByteArray> {
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_INITIAL_HASH)
val db = sql.readableDatabase
val result = LinkedList<ByteArray>()
db.query(
TABLE_NAME, projection, null, null, null, null, null
).use { c ->
while (c.moveToNext()) {
val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH))
result.add(initialHash)
}
}
return result
}
override fun putObject(item: ProofOfWorkRepository.Item) {
try {
val db = sql.writableDatabase
// Create a new map of values, where column names are the keys
val values = ContentValues()
values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage))
values.put(COLUMN_DATA, Encode.bytes(item.objectMessage))
values.put(COLUMN_VERSION, item.objectMessage.version)
values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte)
values.put(COLUMN_EXTRA_BYTES, item.extraBytes)
item.message?.let { message ->
values.put(COLUMN_EXPIRATION_TIME, item.expirationTime)
values.put(COLUMN_MESSAGE_ID, message.id as Long?)
}
db.insertOrThrow(TABLE_NAME, null, values)
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
}
}
override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) =
putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes))
override fun removeObject(initialHash: ByteArray) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null)
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java)
private val TABLE_NAME = "POW"
private val COLUMN_INITIAL_HASH = "initial_hash"
private val COLUMN_DATA = "data"
private val COLUMN_VERSION = "version"
private val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"
private val COLUMN_EXTRA_BYTES = "extra_bytes"
private val COLUMN_EXPIRATION_TIME = "expiration_time"
private val COLUMN_MESSAGE_ID = "message_id"
}
}

View File

@ -1,95 +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.repository;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import ch.dissem.apps.abit.util.Assets;
/**
* Handles database migration and provides access.
*/
public class SqlHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
private static final int DATABASE_VERSION = 7;
private static final String DATABASE_NAME = "jabit.db";
private final Context ctx;
public SqlHelper(Context ctx) {
super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
this.ctx = ctx;
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, DATABASE_VERSION);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 0:
executeMigration(db, "V1.0__Create_table_inventory");
executeMigration(db, "V1.1__Create_table_address");
executeMigration(db, "V1.2__Create_table_message");
case 1:
// executeMigration(db, "V2.0__Update_table_message");
executeMigration(db, "V2.1__Create_table_POW");
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:
// Nothing to do. Let's assume we won't upgrade from a version that's newer than
// DATABASE_VERSION.
}
}
private void executeMigration(SQLiteDatabase db, String name) {
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
db.execSQL(statement);
}
}
static StringBuilder join(long... numbers) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < numbers.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append(numbers[i]);
}
return streamList;
}
static StringBuilder join(Enum<?>... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");
streamList.append('\'').append(types[i].name()).append('\'');
}
return streamList;
}
}

View File

@ -0,0 +1,95 @@
/*
* 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.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import ch.dissem.apps.abit.util.Assets
import ch.dissem.apps.abit.util.UuidUtils
import java.util.*
/**
* Handles database migration and provides access.
*/
class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION)
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf(
0 to {
executeMigration(db, "V1.0__Create_table_inventory")
executeMigration(db, "V1.1__Create_table_address")
executeMigration(db, "V1.2__Create_table_message")
},
1 to {
// executeMigration(db, "V2.0__Update_table_message");
executeMigration(db, "V2.1__Create_table_POW")
},
2 to {
executeMigration(db, "V3.0__Update_table_address")
},
3 to {
executeMigration(db, "V3.1__Update_table_POW")
executeMigration(db, "V3.2__Update_table_message")
},
4 to {
executeMigration(db, "V3.3__Create_table_node")
},
5 to {
executeMigration(db, "V3.4__Add_label_outbox")
},
6 to {
executeMigration(db, "V4.0__Create_table_message_parent")
},
7 to {
setMissingConversationIds(db)
}
).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() }
/**
* Set UUIDs for all messages that have no conversation ID
*/
private fun setMissingConversationIds(db: SQLiteDatabase) = db.query(
"Message", arrayOf("id"),
"conversation IS NULL", null, null, null, null
).use { c ->
while (c.moveToNext()) {
val id = c.getLong(0)
setMissingConversationId(id, db)
}
}
private fun setMissingConversationId(id: Long, db: SQLiteDatabase) {
val values = ContentValues(1)
values.put("conversation", UuidUtils.asBytes(UUID.randomUUID()))
db.update("Message", values, "id=?", arrayOf(id.toString()))
}
private fun executeMigration(db: SQLiteDatabase, name: String) {
for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) {
db.execSQL(statement)
}
}
companion object {
// If you change the database schema, you must increment the database version.
private val DATABASE_VERSION = 7
val DATABASE_NAME = "jabit.db"
}
}

View File

@ -1,73 +0,0 @@
/*
* 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,60 @@
/*
* 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.util.NetworkUtils
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
/**
* @author Christian Basler
*/
class BitmessageIntentService : IntentService("BitmessageIntentService") {
private lateinit var bmc: BitmessageContext
override fun onCreate() {
super.onCreate()
bmc = Singleton.getBitmessageContext(this)
}
override fun onHandleIntent(intent: Intent?) {
intent?.let {
if (it.hasExtra(EXTRA_DELETE_MESSAGE)) {
val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext
bmc.labeler.delete(item)
bmc.messages.save(item)
Singleton.getMessageListener(this).resetNotification()
}
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
NetworkUtils.enableNode(this)
}
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
NetworkUtils.disableNode(this)
}
}
}
companion object {
const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"
const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"
const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"
}
}

View File

@ -1,106 +0,0 @@
/*
* 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,102 @@
/*
* 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.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.connectivityManager
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
class BitmessageService : Service() {
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
private lateinit var notification: NetworkNotification
private val connectivityReceiver: BroadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (bmc.isRunning() && connectivityManager.isActiveNetworkMetered){
bmc.shutdown()
}
}
}
private val cleanupHandler = Handler()
private val cleanupTask: Runnable = object : Runnable {
override fun run() {
bmc.cleanup()
if (isRunning) {
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day
}
}
}
override fun onCreate() {
registerReceiver(connectivityReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
notification = NetworkNotification(this)
running = false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!isRunning) {
running = true
notification.connecting()
startForeground(NETWORK_NOTIFICATION_ID, notification.notification)
if (!bmc.isRunning()) {
bmc.startup()
}
notification.show()
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L)
}
return Service.START_STICKY
}
override fun onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown()
}
running = false
notification.showShutdown()
cleanupHandler.removeCallbacks(cleanupTask)
bmc.cleanup()
stopSelf()
}
override fun onBind(intent: Intent) = null
companion object {
@Volatile private var running = false
val isRunning: Boolean
get() = running && Singleton.bitmessageContext?.isRunning() == true
val status: Property
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
}
}

View File

@ -1,119 +0,0 @@
/*
* 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,115 @@
/*
* 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 ch.dissem.apps.abit.notification.ProofOfWorkNotification
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
import ch.dissem.apps.abit.util.PowStats
import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import java.util.*
/**
* 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.
*/
class ProofOfWorkService : Service() {
private lateinit var notification: ProofOfWorkNotification
override fun onCreate() {
notification = ProofOfWorkNotification(this)
}
override fun onBind(intent: Intent) = PowBinder(this)
class PowBinder internal constructor(private val service: ProofOfWorkService) : Binder() {
private val notification = service.notification
fun process(item: PowItem) = synchronized(queue) {
service.startService(Intent(service, ProofOfWorkService::class.java))
service.startForeground(ONGOING_NOTIFICATION_ID,
notification.notification)
if (!calculating) {
calculating = true
service.calculateNonce(item)
} else {
queue.add(item)
notification.update(queue.size).show()
}
}
}
data class PowItem(val initialHash: ByteArray, val targetValue: ByteArray, val callback: ProofOfWorkEngine.Callback) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PowItem
if (!Arrays.equals(initialHash, other.initialHash)) return false
if (!Arrays.equals(targetValue, other.targetValue)) return false
return true
}
override fun hashCode(): Int {
var result = Arrays.hashCode(initialHash)
result = 31 * result + Arrays.hashCode(targetValue)
return result
}
}
private fun calculateNonce(item: PowItem) {
notification.start(item)
val startTime = System.currentTimeMillis()
engine.calculateNonce(item.initialHash, item.targetValue, object : ProofOfWorkEngine.Callback {
override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) {
notification.finished()
val time = System.currentTimeMillis() - startTime
PowStats.addPow(this@ProofOfWorkService, time, item.targetValue)
try {
item.callback.onNonceCalculated(initialHash, nonce)
} finally {
var next: PowItem? = null
synchronized(queue) {
next = queue.poll()
if (next == null) {
calculating = false
stopForeground(true)
stopSelf()
} else {
notification.update(queue.size).show()
}
}
next?.let { calculateNonce(it) }
}
}
})
}
companion object {
// Object to use as a thread-safe lock
private val engine = MultiThreadedPOWEngine()
private val queue = LinkedList<PowItem>()
private var calculating: Boolean = false
}
}

View File

@ -1,78 +0,0 @@
/*
* 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

@ -0,0 +1,66 @@
/*
* 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 ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder
import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import android.content.Context.BIND_AUTO_CREATE
/**
* Proof of Work engine that uses the Proof of Work service.
*/
class ServicePowEngine(private val ctx: Context) : ProofOfWorkEngine {
private val queue = LinkedList<PowItem>()
private var service: PowBinder? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) = synchronized(lock) {
this@ServicePowEngine.service = service as PowBinder
while (!queue.isEmpty()) {
service.process(queue.poll())
}
}
override fun onServiceDisconnected(name: ComponentName) {
service = null
}
}
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
val item = PowItem(initialHash, target, callback)
synchronized(lock) {
service?.process(item) ?: {
queue.add(item)
ctx.bindService(Intent(ctx, ProofOfWorkService::class.java), connection, BIND_AUTO_CREATE)
}.invoke()
}
}
companion object {
private val lock = Any()
}
}

View File

@ -1,177 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
import java.util.List;
import ch.dissem.apps.abit.MainActivity;
import ch.dissem.apps.abit.R;
import ch.dissem.apps.abit.adapter.AndroidCryptography;
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine;
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.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
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.
*/
public class Singleton {
private static BitmessageContext bitmessageContext;
private static ConversationService conversationService;
private static MessageListener messageListener;
private static BitmessageAddress identity;
private static AndroidProofOfWorkRepository powRepo;
private static boolean creatingIdentity;
public static BitmessageContext getBitmessageContext(Context context) {
if (bitmessageContext == null) {
synchronized (Singleton.class) {
if (bitmessageContext == null) {
final Context ctx = context.getApplicationContext();
SqlHelper sqlHelper = new SqlHelper(ctx);
powRepo = new AndroidProofOfWorkRepository(sqlHelper);
TTL.pubkey(2 * DAY);
bitmessageContext = new BitmessageContext.Builder()
.proofOfWorkEngine(new SwitchingProofOfWorkEngine(
ctx, Constants.PREFERENCE_SERVER_POW,
new ServerPowEngine(ctx),
new ServicePowEngine(ctx)
))
.cryptography(new AndroidCryptography())
.nodeRegistry(new AndroidNodeRegistry(sqlHelper))
.inventory(new AndroidInventory(sqlHelper))
.addressRepo(new AndroidAddressRepository(sqlHelper))
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
.powRepo(powRepo)
.networkHandler(new NioNetworkHandler())
.listener(getMessageListener(ctx))
.doNotSendPubkeyOnIdentityCreation()
.build();
}
}
}
return bitmessageContext;
}
public static MessageListener getMessageListener(Context ctx) {
if (messageListener == null) {
synchronized (Singleton.class) {
if (messageListener == null) {
messageListener = new MessageListener(ctx);
}
}
}
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;
}
}

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