diff --git a/app/build.gradle b/app/build.gradle index 76984f3..c664aec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d46c295..ddca7b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,9 +4,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - + @@ -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"> @@ -84,16 +83,6 @@ - - - - - - - + + + + + - + + + + + + + + android:parentActivityName=".MainActivity"> + android:value=".MainActivity"/> diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java deleted file mode 100644 index b61de46..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ /dev/null @@ -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 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 dummyCallbacks = - new ListSelectionListener() { - @Override - public void onItemSelected(Object item) { - // NO OP - } - }; - /** - * The fragment's current callback object, which is notified of list item - * clicks. - */ - private ListSelectionListener 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; - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.kt b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.kt new file mode 100644 index 0000000..2e919d2 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.kt @@ -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 : ListFragment(), ListHolder { + /** + * The fragment's current callback object, which is notified of list item + * clicks. + */ + @Suppress("UNCHECKED_CAST") + private var callbacks: ListSelectionListener = DummyCallback as ListSelectionListener + /** + * 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 + } 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 + } + + 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 { + 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" + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.kt similarity index 71% rename from app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java rename to app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.kt index a00e613..cad11ec 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailActivity.kt @@ -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}. - *

+ * 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() } } } diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java deleted file mode 100644 index 704726d..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt new file mode 100644 index 0000000..9a4ea03 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt @@ -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" + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java deleted file mode 100644 index 93e1f2e..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ /dev/null @@ -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 { - @Override - public void onResume() { - super.onResume(); - - updateList(); - } - - public void updateList() { - List addresses = Singleton.getAddressRepository(getContext()) - .getContacts(); - Collections.sort(addresses, new Comparator() { - @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( - 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(); - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt new file mode 100644 index 0000000..80db0e4 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt @@ -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() { + private lateinit var adapter: ArrayAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + adapter = object : ArrayAdapter( + 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 + ) +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java deleted file mode 100644 index 1cde1b0..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt new file mode 100644 index 0000000..dcea268 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt @@ -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 + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java deleted file mode 100644 index 142e8f8..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ /dev/null @@ -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 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/ComposeMessageFragment.kt b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt new file mode 100644 index 0000000..75755ba --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt @@ -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() + } +} + diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java deleted file mode 100644 index 6ed8060..0000000 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ /dev/null @@ -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]; - } - } -} diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt new file mode 100644 index 0000000..e569d5a --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt @@ -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(R.id.address) + val label = findViewById(R.id.label) + val subscribe = findViewById(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