diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..656384c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 diff --git a/app/build.gradle b/app/build.gradle index 0e3dcb4..76984f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,42 +1,85 @@ apply plugin: 'idea' apply plugin: 'com.android.application' +ext { + appName = "Abit" +} +if (project.hasProperty("project.configs") + && new File(project.property("project.configs") + appName + ".gradle").exists()) { + apply from: project.property("project.configs") + appName + ".gradle"; +} + +//noinspection GroovyMissingReturnStatement android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 25 + buildToolsVersion "25.0.2" defaultConfig { - applicationId "ch.dissem.apps.abit" - minSdkVersion 15 - targetSdkVersion 23 - versionCode 1 - versionName "1.0" + applicationId "ch.dissem.apps." + appName.toLowerCase() + minSdkVersion 19 + targetSdkVersion 25 + versionCode 12 + versionName "1.0-beta12" + jackOptions.enabled = false + multiDexEnabled true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 } buildTypes { release { minifyEnabled false + shrinkResources false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } } } +//ext.jabitVersion = '2.0.4' +ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' +ext.supportVersion = '25.3.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.android.support:support-v4:23.0.1' - compile 'com.android.support:design:23.0.1' - compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' - compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT' + compile "com.android.support:appcompat-v7:$supportVersion" + compile "com.android.support:support-v4:$supportVersion" + compile "com.android.support:design:$supportVersion" + compile "com.android.support:multidex:1.0.1" - compile 'org.slf4j:slf4j-android:1.7.12' + compile "ch.dissem.jabit:jabit-core:$jabitVersion" + compile "ch.dissem.jabit:jabit-networking:$jabitVersion" + compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion" + compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" + compile "ch.dissem.jabit:jabit-wif:$jabitVersion" - compile('com.mikepenz:materialdrawer:3.1.0@aar') { + compile 'org.slf4j:slf4j-android:1.7.25' + + compile 'com.mikepenz:materialize:1.0.1@aar' + compile('com.mikepenz:materialdrawer:5.9.0@aar') { transitive = true } - compile 'com.mikepenz:iconics:1.6.2@aar' - compile 'com.mikepenz:community-material-typeface:1.1.71@aar' + compile('com.mikepenz:aboutlibraries:5.9.5@aar') { + transitive = true + } + compile "com.mikepenz:iconics-core:2.8.3@aar" + compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' + compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar' + + compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' + compile 'com.google.zxing:core:3.3.0' + + compile 'io.github.yavski:fab-speed-dial:1.0.6' + compile 'com.github.amlcurran.showcaseview:library:5.4.3' + compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { + transitive = true + } + compile 'com.github.angads25:filepicker:1.1.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:2.7.22' } idea.module { @@ -48,4 +91,4 @@ android { lintOptions { abortOnError false } -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19861c3..d46c295 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,29 @@ - + - + + + + + + android:allowBackup="false" + 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=".MainActivity" + android:label="@string/app_name"> @@ -24,30 +31,34 @@ + android:name=".MessageDetailActivity" + android:label="@string/title_message_detail" + android:parentActivityName=".MainActivity" + tools:ignore="UnusedAttribute"> + android:name="android.support.PARENT_ACTIVITY" + android:value=".MainActivity"/> + android:name=".AddressDetailActivity" + android:label="@string/title_subscription_detail" + android:parentActivityName=".MainActivity" + tools:ignore="UnusedAttribute"> + android:name="android.support.PARENT_ACTIVITY" + android:value=".MainActivity"/> + android:name=".dialog.FullNodeDialogActivity" + android:label="@string/full_node" + android:theme="@style/Theme.AppCompat.Light.Dialog"/> + + android:name="android.support.PARENT_ACTIVITY" + android:value=".MainActivity"/> @@ -74,20 +85,19 @@ + android:name=".SettingsActivity" + android:label="@string/settings" + android:parentActivityName=".MainActivity"> - + android:name=".CreateAddressActivity" + android:label="@string/title_activity_open_bitmessage_link" + android:theme="@style/Theme.AppCompat.Light.Dialog"> @@ -99,6 +109,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql index 79e32f2..ae72a6f 100644 --- a/app/src/main/assets/db/migration/V1.2__Create_table_message.sql +++ b/app/src/main/assets/db/migration/V1.2__Create_table_message.sql @@ -8,6 +8,7 @@ CREATE TABLE Message ( sent INTEGER, received INTEGER, status VARCHAR(20) NOT NULL, + initial_hash BINARY(64) UNIQUE, FOREIGN KEY (sender) REFERENCES Address (address), FOREIGN KEY (recipient) REFERENCES Address (address) diff --git a/app/src/main/assets/db/migration/V2.0__Update_table_message.sql b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql new file mode 100644 index 0000000..2b39a6e --- /dev/null +++ b/app/src/main/assets/db/migration/V2.0__Update_table_message.sql @@ -0,0 +1,7 @@ +-- This is done in V1.2, as SQLite doesn't support ADD CONSTRAINT and a proper migration +-- wasn't really necessary yet. +-- +-- This file is here to reduce confusion regarding to the original migration files. + +--ALTER TABLE Message ADD COLUMN initial_hash BINARY(64); +--ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash); \ No newline at end of file diff --git a/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql new file mode 100644 index 0000000..b39c6c5 --- /dev/null +++ b/app/src/main/assets/db/migration/V2.1__Create_table_POW.sql @@ -0,0 +1,7 @@ +CREATE TABLE POW ( + initial_hash BINARY(64) PRIMARY KEY, + data BLOB NOT NULL, + version BIGINT NOT NULL, + nonce_trials_per_byte BIGINT NOT NULL, + extra_bytes BIGINT NOT NULL +); diff --git a/app/src/main/assets/db/migration/V3.0__Update_table_address.sql b/app/src/main/assets/db/migration/V3.0__Update_table_address.sql new file mode 100644 index 0000000..01e036b --- /dev/null +++ b/app/src/main/assets/db/migration/V3.0__Update_table_address.sql @@ -0,0 +1 @@ +ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0'; diff --git a/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql b/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql new file mode 100644 index 0000000..d67a1b5 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.1__Update_table_POW.sql @@ -0,0 +1,2 @@ +ALTER TABLE POW ADD COLUMN expiration_time BIGINT; +ALTER TABLE POW ADD COLUMN message_id BIGINT; diff --git a/app/src/main/assets/db/migration/V3.2__Update_table_message.sql b/app/src/main/assets/db/migration/V3.2__Update_table_message.sql new file mode 100644 index 0000000..1eba39f --- /dev/null +++ b/app/src/main/assets/db/migration/V3.2__Update_table_message.sql @@ -0,0 +1,4 @@ +ALTER TABLE Message ADD COLUMN ack_data BINARY(32); +ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN next_try BIGINT; diff --git a/app/src/main/assets/db/migration/V3.3__Create_table_node.sql b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql new file mode 100644 index 0000000..5d03bb5 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.3__Create_table_node.sql @@ -0,0 +1,9 @@ +CREATE TABLE Node ( + stream BIGINT NOT NULL, + address BINARY(32) NOT NULL, + port INT NOT NULL, + services BIGINT NOT NULL, + time BIGINT NOT NULL, + PRIMARY KEY (stream, address, port) +); +CREATE INDEX idx_time on Node(time); diff --git a/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql b/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql new file mode 100644 index 0000000..d61bcc9 --- /dev/null +++ b/app/src/main/assets/db/migration/V3.4__Add_label_outbox.sql @@ -0,0 +1 @@ +INSERT INTO Label(label, type, ord) VALUES ('Outbox', 'OUTBOX', 15); diff --git a/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql new file mode 100644 index 0000000..855140a --- /dev/null +++ b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql @@ -0,0 +1,11 @@ +ALTER TABLE Message ADD COLUMN conversation BINARY[16]; + +CREATE TABLE Message_Parent ( + parent BINARY(64) NOT NULL, + child BINARY(64) NOT NULL, + pos INT NOT NULL, + conversation BINARY[16] NOT NULL, + + PRIMARY KEY (parent, child), + FOREIGN KEY (child) REFERENCES Message (iv) +); diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000..ef455ee Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 7ddfce9..b61de46 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -16,20 +16,18 @@ package ch.dissem.apps.abit; -import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; -import ch.dissem.apps.abit.listeners.ListSelectionListener; -import ch.dissem.apps.abit.service.Singleton; -import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.valueobject.Label; + +import ch.dissem.apps.abit.listener.ListSelectionListener; /** - * Created by chris on 07.09.15. + * @author Christian Basler */ -public abstract class AbstractItemListFragment extends ListFragment { +public abstract class AbstractItemListFragment extends ListFragment implements ListHolder { /** * The serialization (saved instance state) Bundle key representing the * activated item position. Only used on tablets. @@ -39,12 +37,13 @@ public abstract class AbstractItemListFragment extends ListFragment { * A dummy implementation of the {@link ListSelectionListener} interface that does * nothing. Used only when this fragment is not attached to an activity. */ - private static ListSelectionListener dummyCallbacks = new ListSelectionListener() { - @Override - public void onItemSelected(Object plaintext) { - } - }; - protected BitmessageContext bmc; + private static final ListSelectionListener dummyCallbacks = + new ListSelectionListener() { + @Override + public void onItemSelected(Object item) { + // NO OP + } + }; /** * The fragment's current callback object, which is notified of list item * clicks. @@ -54,15 +53,7 @@ public abstract class AbstractItemListFragment extends ListFragment { * The current activated item position. Only used on tablets. */ private int activatedPosition = ListView.INVALID_POSITION; - - abstract void updateList(Label label); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - bmc = Singleton.getBitmessageContext(getActivity()); - } + private boolean activateOnItemClick; @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -70,21 +61,34 @@ public abstract class AbstractItemListFragment extends ListFragment { // Restore the previously serialized activated item position. if (savedInstanceState != null - && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { + && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); } } @Override - public void onAttach(Activity activity) { - super.onAttach(activity); + public void onResume() { + super.onResume(); + + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + getListView().setChoiceMode(activateOnItemClick + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); // Activities containing this fragment must implement its callbacks. - if (!(activity instanceof ListSelectionListener)) { + if (context instanceof ListSelectionListener) { + //noinspection unchecked + callbacks = (ListSelectionListener) context; + } else { throw new IllegalStateException("Activity must implement fragment's callbacks."); } - callbacks = (ListSelectionListener) activity; } @Override @@ -101,6 +105,7 @@ public abstract class AbstractItemListFragment extends ListFragment { // Notify the active callbacks interface (the activity, if the // fragment is attached to one) that an item has been selected. + //noinspection unchecked callbacks.onItemSelected((T) listView.getItemAtPosition(position)); } @@ -118,11 +123,15 @@ public abstract class AbstractItemListFragment extends ListFragment { * given the 'activated' state when touched. */ public void setActivateOnItemClick(boolean activateOnItemClick) { - // When setting CHOICE_MODE_SINGLE, ListView will automatically - // give items the 'activated' state when touched. - getListView().setChoiceMode(activateOnItemClick + this.activateOnItemClick = activateOnItemClick; + + if (isVisible()) { + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + getListView().setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); + } } private void setActivatedPosition(int position) { diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java similarity index 54% rename from app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java rename to app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java index 57bf32b..a00e613 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java @@ -16,34 +16,23 @@ 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; /** * An activity representing a single Subscription detail screen. This * activity is only used on handset devices. On tablet-size devices, * item details are presented side-by-side with a list of items - * in a {@link MessageListActivity}. + * in a {@link MainActivity}. *

* This activity is mostly just a 'shell' activity containing nothing - * more than a {@link SubscriptionDetailFragment}. + * more than a {@link AddressDetailFragment}. */ -public class SubscriptionDetailActivity extends AppCompatActivity { +public class AddressDetailActivity extends DetailActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.toolbar_layout); - - final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - // Show the Up button in the action bar. - getSupportActionBar().setDisplayHomeAsUpEnabled(true); // savedInstanceState is non-null when there is fragment state // saved from previous configurations of this activity @@ -58,30 +47,13 @@ public class SubscriptionDetailActivity extends AppCompatActivity { // Create the detail fragment and add it to the activity // using a fragment transaction. Bundle arguments = new Bundle(); - arguments.putSerializable(SubscriptionDetailFragment.ARG_ITEM, - getIntent().getSerializableExtra(SubscriptionDetailFragment.ARG_ITEM)); - SubscriptionDetailFragment fragment = new SubscriptionDetailFragment(); + arguments.putSerializable(AddressDetailFragment.ARG_ITEM, + getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM)); + AddressDetailFragment fragment = new AddressDetailFragment(); fragment.setArguments(arguments); getSupportFragmentManager().beginTransaction() .add(R.id.content, fragment) .commit(); } } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - // This ID represents the Home or Up button. In the case of this - // activity, the Up button is shown. Use NavUtils to allow users - // to navigate up one level in the application structure. For - // more details, see the Navigation pattern on Android Design: - // - // http://developer.android.com/design/patterns/navigation.html#up-vs-back - // - NavUtils.navigateUpTo(this, new Intent(this, MessageListActivity.class)); - return true; - } - return super.onOptionsItemSelected(item); - } } diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java new file mode 100644 index 0000000..704726d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -0,0 +1,263 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import com.mikepenz.community_material_typeface_library.CommunityMaterial; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.util.Drawables; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.wif.WifExporter; + + +/** + * A fragment representing a single Message detail screen. + * This fragment is either contained in a {@link MainActivity} + * in two-pane mode (on tablets) or a {@link MessageDetailActivity} + * on handsets. + */ +public class AddressDetailFragment extends Fragment { + /** + * The fragment argument representing the item ID that this fragment + * represents. + */ + public static final String ARG_ITEM = "item"; + public static final String EXPORT_POSTFIX = ".keys.dat"; + + /** + * The content this fragment is presenting. + */ + private BitmessageAddress item; + + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public AddressDetailFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments().containsKey(ARG_ITEM)) { + // Load the dummy content specified by the fragment + // arguments. In a real-world scenario, use a Loader + // to load content from a content provider. + item = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM); + } + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.address, menu); + + FragmentActivity activity = getActivity(); + Drawables.addIcon(activity, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); + Drawables.addIcon(activity, menu, R.id.share, GoogleMaterial.Icon.gmd_share); + Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); + Drawables.addIcon(activity, menu, R.id.export, + CommunityMaterial.Icon.cmd_export) + .setVisible(item != null && item.getPrivateKey() != null); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + final Activity ctx = getActivity(); + switch (menuItem.getItemId()) { + case R.id.write_message: { + BitmessageAddress identity = Singleton.getIdentity(ctx); + if (identity == null) { + Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show(); + } else { + Intent intent = new Intent(ctx, ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity); + intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); + startActivity(intent); + } + return true; + } + case R.id.delete: { + int warning; + if (item.getPrivateKey() != null) + warning = R.string.delete_identity_warning; + else + warning = R.string.delete_contact_warning; + new AlertDialog.Builder(ctx) + .setMessage(warning) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Singleton.getAddressRepository(ctx).remove(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (item.getPrivateKey() != null && mainActivity != null) { + mainActivity.removeIdentityEntry(item); + } + item = null; + ctx.onBackPressed(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + return true; + } + case R.id.export: { + new AlertDialog.Builder(ctx) + .setMessage(R.string.confirm_export) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TITLE, item + + EXPORT_POSTFIX); + WifExporter exporter = new WifExporter(Singleton + .getBitmessageContext(ctx)); + exporter.addIdentity(item); + shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString + ()); + startActivity(Intent.createChooser(shareIntent, null)); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + return true; + } + case R.id.share: { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress()); + startActivity(Intent.createChooser(shareIntent, null)); + } + default: + return false; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_address_detail, container, false); + + // Show the dummy content as text in a TextView. + if (item != null) { + FragmentActivity activity = getActivity(); + if (item.isChan()) { + activity.setTitle(R.string.title_chan_detail); + } else if (item.getPrivateKey() != null) { + activity.setTitle(R.string.title_identity_detail); + } else if (item.isSubscribed()) { + activity.setTitle(R.string.title_subscription_detail); + } else { + activity.setTitle(R.string.title_contact_detail); + } + + ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); + TextView name = (TextView) rootView.findViewById(R.id.name); + name.setText(item.toString()); + name.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Nothing to do + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Nothing to do + } + + @Override + public void afterTextChanged(Editable s) { + item.setAlias(s.toString()); + } + }); + TextView address = (TextView) rootView.findViewById(R.id.address); + address.setText(item.getAddress()); + address.setSelected(true); + ((TextView) rootView.findViewById(R.id.stream_number)).setText( + getString(R.string.stream_number, item.getStream())); + if (item.getPrivateKey() == null) { + Switch active = (Switch) rootView.findViewById(R.id.active); + active.setChecked(item.isSubscribed()); + active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton button, boolean checked) { + item.setSubscribed(checked); + } + }); + + ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id + .pubkey_available); + + if (item.getPubkey() == null) { + pubkeyAvailableImg.setAlpha(0.3f); + TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id + .pubkey_available_desc); + pubkeyAvailableDesc.setText(R.string.pubkey_not_available); + } + } else { + rootView.findViewById(R.id.active).setVisibility(View.GONE); + rootView.findViewById(R.id.pubkey_available).setVisibility(View.GONE); + rootView.findViewById(R.id.pubkey_available_desc).setVisibility(View.GONE); + } + + // QR code + ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); + qrCode.setImageBitmap(Drawables.qrCode(item)); + } + + return rootView; + } + + @Override + public void onPause() { + if (item != null) { + Singleton.getAddressRepository(getContext()).save(item); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null && item.getPrivateKey() != null) { + mainActivity.updateIdentityEntry(item); + } + } + super.onPause(); + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java similarity index 50% rename from app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java rename to app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index a3c7b00..93e1f2e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SubscriptionListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -16,25 +16,37 @@ 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 ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.valueobject.Label; + +import com.google.zxing.integration.android.IntentIntegrator; import java.util.Collections; import java.util.Comparator; import java.util.List; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.valueobject.Label; +import io.github.yavski.fabspeeddial.FabSpeedDial; +import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter; + /** - * Created by chris on 06.09.15. + * Fragment that shows a list of all contacts, the ones we subscribed to first. */ -public class SubscriptionListFragment extends AbstractItemListFragment { +public class AddressListFragment extends AbstractItemListFragment { @Override public void onResume() { super.onResume(); @@ -43,18 +55,15 @@ public class SubscriptionListFragment extends AbstractItemListFragment addresses = bmc.addresses().getContacts(); + List addresses = Singleton.getAddressRepository(getContext()) + .getContacts(); Collections.sort(addresses, new Comparator() { - /** - * Yields the following order: - *

    - *
  1. Subscribed addresses come first
  2. - *
  3. Addresses with Aliases (alphabetically)
  4. - *
  5. Addresses (alphabetically)
  6. - *
- */ @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) { @@ -76,38 +85,82 @@ public class SubscriptionListFragment extends AbstractItemListFragment( - getActivity(), - android.R.layout.simple_list_item_activated_1, - android.R.id.text1, - addresses) { + getActivity(), + android.R.layout.simple_list_item_activated_1, + android.R.id.text1, + addresses) { + @NonNull @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View getView(int position, View convertView, @NonNull ViewGroup parent) { if (convertView == null) { LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.subscription_row, null, false); + convertView = inflater.inflate(R.layout.subscription_row, parent, false); } BitmessageAddress item = getItem(position); - ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); + 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); + 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 rootView = inflater.inflate(R.layout.fragment_subscribtions, container, false); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_address_list, container, false); - return rootView; + FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact); + fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() { + @Override + public boolean onMenuItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.action_read_qr_code: + IntentIntegrator.forSupportFragment(AddressListFragment.this) + .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) + .initiateScan(); + return true; + case R.id.action_create_contact: + Intent intent = new Intent(getActivity(), CreateAddressActivity.class); + startActivity(intent); + return true; + default: + return false; + } + } + }); + + return view; } @Override - void updateList(Label label) { + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null && data.hasExtra("SCAN_RESULT")) { + Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT")); + Intent intent = new Intent(getActivity(), CreateAddressActivity.class); + intent.setData(uri); + startActivity(intent); + } + } + + @Override + public void updateList(Label label) { updateList(); } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index a8f35fc..1cde1b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -1,9 +1,34 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; +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. @@ -11,6 +36,11 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; 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) { @@ -20,6 +50,7 @@ public class ComposeMessageActivity extends AppCompatActivity { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + //noinspection ConstantConditions getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(false); @@ -28,7 +59,47 @@ public class ComposeMessageActivity extends AppCompatActivity { ComposeMessageFragment fragment = new ComposeMessageFragment(); fragment.setArguments(getIntent().getExtras()); getSupportFragmentManager().beginTransaction() - .replace(R.id.content, fragment) - .commit(); + .replace(R.id.content, fragment) + .commit(); + } + + public static void launchReplyTo(Fragment fragment, Plaintext item) { + fragment.startActivity(getReplyIntent(fragment.getActivity(), item)); + } + + public static void launchReplyTo(Activity activity, Plaintext item) { + activity.startActivity(getReplyIntent(activity, item)); + } + + private static Intent getReplyIntent(Context ctx, Plaintext item) { + Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class); + 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; } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 88145fb..142e8f8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -1,24 +1,71 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ch.dissem.apps.abit; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.view.*; -import android.view.inputmethod.EditorInfo; +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 BitmessageContext bmCtx; 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 @@ -33,10 +80,34 @@ public class ComposeMessageFragment extends Fragment { if (getArguments() != null) { if (getArguments().containsKey(EXTRA_IDENTITY)) { identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY); + if (getActivity() != null) { + if (identity == null || identity.getPrivateKey() == null) { + identity = Singleton.getIdentity(getActivity()); + } + } + } else { + throw new RuntimeException("No identity set for ComposeMessageFragment"); } + 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); } @@ -45,16 +116,60 @@ public class ComposeMessageFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false); - if (recipient != null) { - EditText recipientInput = (EditText) rootView.findViewById(R.id.recipient); - recipientInput.setText(recipient.toString()); + 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()); + } } - EditText body = (EditText) rootView.findViewById(R.id.body); - body.setInputType(EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE); - body.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_ENTER_ACTION); + subjectInput = (EditText) rootView.findViewById(R.id.subject); + 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); @@ -65,11 +180,87 @@ public class ComposeMessageFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.send: - Toast.makeText(getActivity(), "TODO: Send", Toast.LENGTH_SHORT).show(); + 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 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(); + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java new file mode 100644 index 0000000..6ed8060 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Base64; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Switch; +import android.widget.TextView; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.payload.V2Pubkey; +import ch.dissem.bitmessage.entity.payload.V3Pubkey; +import ch.dissem.bitmessage.entity.payload.V4Pubkey; + +import static android.util.Base64.URL_SAFE; + +public class CreateAddressActivity extends AppCompatActivity { + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$"); + private byte[] pubkeyBytes; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Uri uri = getIntent().getData(); + if (uri != null) + setContentView(R.layout.activity_open_bitmessage_link); + else + setContentView(R.layout.activity_create_bitmessage_address); + + final TextView address = (TextView) findViewById(R.id.address); + final EditText label = (EditText) findViewById(R.id.label); + final Switch subscribe = (Switch) findViewById(R.id.subscribe); + + if (uri != null) { + String addressText = getAddress(uri); + String[] parameters = getParameters(uri); + for (String parameter : parameters) { + Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); + if (matcher.find()) { + String key = matcher.group(1).toLowerCase(); + String value = matcher.group(2); + switch (key) { + case "label": + label.setText(value.trim()); + break; + case "action": + subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); + break; + case "pubkey": + pubkeyBytes = Base64.decode(value, URL_SAFE); + break; + } + } + } + + address.setText(addressText); + } + + final Button cancel = (Button) findViewById(R.id.cancel); + cancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setResult(Activity.RESULT_CANCELED); + finish(); + } + }); + final Button ok = (Button) findViewById(R.id.do_import); + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String addressText = String.valueOf(address.getText()).trim(); + try { + BitmessageAddress bmAddress = new BitmessageAddress(addressText); + bmAddress.setAlias(label.getText().toString()); + + BitmessageContext bmc = Singleton.getBitmessageContext + (CreateAddressActivity.this); + bmc.addContact(bmAddress); + if (subscribe.isChecked()) { + bmc.addSubscribtion(bmAddress); + } + if (pubkeyBytes != null) { + try { + final Pubkey pubkey; + InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes); + long stream = bmAddress.getStream(); + switch ((int) bmAddress.getVersion()) { + case 2: + pubkey = V2Pubkey.read(pubkeyStream, stream); + break; + case 3: + pubkey = V3Pubkey.read(pubkeyStream, stream); + break; + case 4: + pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream)); + break; + default: + pubkey = null; + } + if (pubkey != null) { + bmAddress.setPubkey(pubkey); + } + } catch (Exception ignore) { + } + } + + setResult(Activity.RESULT_OK); + finish(); + } catch (RuntimeException e) { + address.setError(getString(R.string.error_illegal_address)); + } + } + }); + } + + private String getAddress(Uri uri) { + StringBuilder result = new StringBuilder(); + String schemeSpecificPart = uri.getSchemeSpecificPart(); + if (!schemeSpecificPart.startsWith("BM-")) { + result.append("BM-"); + } + if (schemeSpecificPart.contains("?")) { + result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?'))); + } else if (schemeSpecificPart.contains("#")) { + result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#'))); + } else { + result.append(schemeSpecificPart); + } + return result.toString(); + } + + private String[] getParameters(Uri uri) { + int index = uri.getSchemeSpecificPart().indexOf('?'); + if (index >= 0) { + String parameterPart = uri.getSchemeSpecificPart().substring(index + 1); + return parameterPart.split("&"); + } else { + return new String[0]; + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java new file mode 100644 index 0000000..0289b64 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/DetailActivity.java @@ -0,0 +1,53 @@ +package ch.dissem.apps.abit; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; + +import com.mikepenz.materialize.MaterializeBuilder; + +/** + * @author Christian Basler + */ +public abstract class DetailActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.scrolling_toolbar_layout); + + final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + // Show the Up button in the action bar. + //noinspection ConstantConditions + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + new MaterializeBuilder() + .withActivity(this) + .withStatusBarColorRes(R.color.colorPrimaryDark) + .withTranslucentStatusBarProgrammatically(true) + .withStatusBarPadding(true) + .build(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // This ID represents the Home or Up button. In the case of this + // activity, the Up button is shown. Use NavUtils to allow users + // to navigate up one level in the application structure. For + // more details, see the Navigation pattern on Android Design: + // + // http://developer.android.com/design/patterns/navigation.html#up-vs-back + // + NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.java b/app/src/main/java/ch/dissem/apps/abit/Identicon.java index 08ad67e..a6f5fcd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.java +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.java @@ -16,63 +16,95 @@ 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 float width; - private float height; + private final int color; + private final int background; + private final boolean[][] fields; + private final boolean chan; + private final TextPaint textPaint; - private float cellWidth; - private float cellHeight; - private byte[] hash; - private int color; - private int background; - private boolean[][] fields; - - public Identicon(BitmessageAddress input) { + 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)); - hash = input.getRipe(); + 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}); + color = Color.HSVToColor(new float[]{ + Math.abs(hash[0] * hash[1] + hash[2]) % 360, + 0.8f, + 1.0f + }); + background = Color.HSVToColor(new float[]{ + Math.abs(hash[1] * hash[2] + hash[0]) % 360, + 0.8f, + 1.0f + }); for (int row = 0; row < SIZE; row++) { - for (int column = 0; column < SIZE; column++) { - fields[row][column] = hash[(row * (column < CENTER_COLUMN ? column : SIZE - column - 1)) % hash.length] >= 0; + 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(Canvas canvas) { + public void draw(@NonNull Canvas canvas) { float x, y; + float width = canvas.getWidth(); + float height = canvas.getHeight(); + float cellWidth = width / (float) SIZE; + float cellHeight = height / (float) SIZE; paint.setColor(background); - canvas.drawCircle(width/2, height/2, width/2, paint); + canvas.drawCircle(width / 2, height / 2, width / 2, paint); paint.setColor(color); for (int row = 0; row < SIZE; row++) { for (int column = 0; column < SIZE; column++) { if (fields[row][column]) { x = cellWidth * column; y = cellHeight * row; - - canvas.drawCircle(x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, paint); + canvas.drawCircle( + x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, + paint + ); } } } + if (chan) { + textPaint.setTextSize(2 * cellHeight); + canvas.drawText("[chan]", width / 2, 6.7f * cellHeight, textPaint); + } } @Override @@ -89,16 +121,4 @@ public class Identicon extends Drawable { public int getOpacity() { return PixelFormat.TRANSPARENT; } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - - width = bounds.width(); - height = bounds.height(); - - cellWidth = bounds.width() / (float) SIZE; - cellHeight = bounds.height() / (float) SIZE; - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java new file mode 100644 index 0000000..be9d954 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator; + +import java.io.IOException; + +import ch.dissem.apps.abit.adapter.AddressSelectorAdapter; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.wif.WifImporter; + +/** + * @author Christian Basler + */ + +public class ImportIdentitiesFragment extends Fragment { + public static final String WIF_DATA = "wif_data"; + private AddressSelectorAdapter adapter; + private WifImporter importer; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + String wifData = getArguments().getString(WIF_DATA); + BitmessageContext bmc = Singleton.getBitmessageContext(getActivity()); + View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false); + try { + importer = new WifImporter(bmc, wifData); + adapter = new AddressSelectorAdapter(importer.getIdentities()); + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), + LinearLayoutManager.VERTICAL, + false); + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(adapter); + + recyclerView.addItemDecoration(new SimpleListDividerDecorator( + ContextCompat.getDrawable(getActivity(), R.drawable.list_divider_h), true)); + } catch (IOException e) { + return super.onCreateView(inflater, container, savedInstanceState); + } + view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + importer.importAll(adapter.getSelected()); + MainActivity mainActivity = MainActivity.getInstance(); + if (mainActivity != null) { + for (BitmessageAddress selected : adapter.getSelected()) { + mainActivity.addIdentityEntry(selected); + } + } + getActivity().finish(); + } + }); + return view; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java b/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java new file mode 100644 index 0000000..725ae7d --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentityActivity.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.os.Bundle; + +import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; + +/** + * @author Christian Basler + */ + +public class ImportIdentityActivity extends DetailActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String wifData; + if (savedInstanceState == null) { + wifData = null; + } else { + wifData = savedInstanceState.getString(WIF_DATA); + } + if (wifData == null) { + getFragmentManager().beginTransaction() + .replace(R.id.content, new InputWifFragment()) + .commit(); + } else { + Bundle bundle = new Bundle(); + bundle.putString(WIF_DATA, wifData); + + ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); + fragment.setArguments(bundle); + + getFragmentManager().beginTransaction() + .replace(R.id.content, fragment) + .commit(); + } + } + +} diff --git a/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java new file mode 100644 index 0000000..2e6f227 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/InputWifFragment.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.angads25.filepicker.controller.DialogSelectionListener; +import com.github.angads25.filepicker.model.DialogConfigs; +import com.github.angads25.filepicker.model.DialogProperties; +import com.github.angads25.filepicker.view.FilePickerDialog; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; + +/** + * @author Christian Basler + */ + +public class InputWifFragment extends Fragment { + private TextView wifData; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_import_input, container, false); + wifData = (TextView) view.findViewById(R.id.wif_input); + + view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View 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; + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ListHolder.java b/app/src/main/java/ch/dissem/apps/abit/ListHolder.java new file mode 100644 index 0000000..19f2ab6 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ListHolder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * @author Christian Basler + */ +public interface ListHolder { + void updateList(Label label); + + void setActivateOnItemClick(boolean activateOnItemClick); +} diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java new file mode 100644 index 0000000..f7d2c07 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -0,0 +1,622 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.apps.abit; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import com.github.amlcurran.showcaseview.ShowcaseView; +import com.github.amlcurran.showcaseview.targets.Target; +import com.mikepenz.community_material_typeface_library.CommunityMaterial; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.IconicsDrawable; +import com.mikepenz.materialdrawer.AccountHeader; +import com.mikepenz.materialdrawer.AccountHeaderBuilder; +import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.DrawerBuilder; +import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener; +import com.mikepenz.materialdrawer.model.DividerDrawerItem; +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; +import com.mikepenz.materialdrawer.model.ProfileDrawerItem; +import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem; +import com.mikepenz.materialdrawer.model.SwitchDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IProfile; +import com.mikepenz.materialdrawer.model.interfaces.Nameable; + +import java.io.Serializable; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; +import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; +import ch.dissem.apps.abit.listener.ActionBarListener; +import ch.dissem.apps.abit.listener.ListSelectionListener; +import ch.dissem.apps.abit.repository.AndroidMessageRepository; +import ch.dissem.apps.abit.service.BitmessageService; +import ch.dissem.apps.abit.service.Singleton; +import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Drawables; +import ch.dissem.apps.abit.util.Labels; +import ch.dissem.apps.abit.util.Preferences; +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +import static android.widget.Toast.LENGTH_LONG; +import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; +import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; +import static ch.dissem.apps.abit.service.BitmessageService.isRunning; + + +/** + * An activity representing a list of Messages. This activity + * has different presentations for handset and tablet-size devices. On + * handsets, the activity presents a list of items, which when touched, + * lead to a {@link MessageDetailActivity} representing + * item details. On tablets, the activity presents the list of items and + * item details side-by-side using two vertical panes. + *

+ * 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}. + *

+ * This activity also implements the required + * {@link ListSelectionListener} interface + * to listen for item selections. + *

+ */ +public class MainActivity extends AppCompatActivity + implements ListSelectionListener, 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 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 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 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 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>() { + @Override + protected List doInBackground(Void... params) { + List identities = bmc.addresses().getIdentities(); + if (identities.isEmpty()) { + // Create an initial identity + Singleton.getIdentity(MainActivity.this); + } + return identities; + } + + @Override + protected void onPostExecute(List identities) { + for (BitmessageAddress identity : identities) { + addIdentityEntry(identity); + } + } + }.execute(); + + new AsyncTask>() { + @Override + protected List