diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml new file mode 100644 index 0000000000..bed55d65d2 --- /dev/null +++ b/.github/workflows/ant.yml @@ -0,0 +1,25 @@ +# This workflow will build a Java project with Ant +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-ant + +name: Java CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build with Ant + run: ant -noinput -buildfile build.xml diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml new file mode 100644 index 0000000000..705b63e4b2 --- /dev/null +++ b/.github/workflows/clojure.yml @@ -0,0 +1,19 @@ +name: Clojure CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: lein deps + - name: Run tests + run: lein test diff --git a/app/build.gradle b/app/build.gradle index 01bb3d987e..146bc917c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,7 +49,7 @@ android { def DEFAULT_OPENSEA_API_KEY = "\"...\""; //Put your OpenSea developer API key in here, otherwise you are reliant on the backup NFT fetch method def DEFAULT_POLYGONSCAN_API_KEY = "\"\""; //Put your Polygonscan developer API key in here to get access to Polygon/Mumbai token discovery and transactions - buildConfigField 'int', 'DB_VERSION', '43' + buildConfigField 'int', 'DB_VERSION', '44' buildConfigField "String", XInfuraAPI, DEFAULT_INFURA_API_KEY ndk { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a069c50793..f44d196d21 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -357,6 +357,12 @@ android:name=".ui.QRScanning.QRScanner" android:label="@string/qr_scanner" /> + + + + CREATOR = new Creator() { + @Override + public AddressBookContact createFromParcel(Parcel source) { + return new AddressBookContact(source); + } + + @Override + public AddressBookContact[] newArray(int size) { + return new AddressBookContact[size]; + } + }; + + @Override + public String toString() { + return "UserInfo{" + + "walletAddress='" + walletAddress + '\'' + + ", name='" + name + '\'' + + ", ethName='" + ethName + '\'' + + '}'; + } +} diff --git a/app/src/main/java/com/alphawallet/app/interact/AddressBookInteract.java b/app/src/main/java/com/alphawallet/app/interact/AddressBookInteract.java new file mode 100644 index 0000000000..a76f491f8c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/interact/AddressBookInteract.java @@ -0,0 +1,50 @@ +package com.alphawallet.app.interact; + +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.repository.ContactRepositoryType; +import com.alphawallet.app.util.ItemCallback; + +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class AddressBookInteract { + + private ContactRepositoryType contactRepository; + + public AddressBookInteract(ContactRepositoryType contactRepository) { + this.contactRepository = contactRepository; + } + + public Single> getAllContacts() { + return contactRepository.getContacts(); + } + + public Completable addContact(AddressBookContact addressBookContact) { + return contactRepository.addContact(addressBookContact); + } + + public Completable updateContact(AddressBookContact oldContact, AddressBookContact newContact) { + return contactRepository.updateContact(oldContact, newContact); + } + + public Completable removeContact(AddressBookContact addressBookContact) { + return contactRepository.removeContact(addressBookContact); + } + + public Single searchContact(String walletAddress) { + return contactRepository.searchContact(walletAddress); + } + + /** Search a contact and invoke the callback*/ + public void searchContactAsync(String walletAddress, ItemCallback callback) { + contactRepository.searchContact(walletAddress) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( (callback::call), (throwable -> callback.call(null)) ) + .isDisposed(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java b/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java index 5b3e502bc8..6174ae12ff 100644 --- a/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java +++ b/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java @@ -402,6 +402,18 @@ else if (oldVersion == 8) oldVersion = 43; } + + if (oldVersion == 43) + { + RealmObjectSchema userData = schema.get("RealmContact"); + if (userData == null) + schema.create("RealmContact") + .addField("walletAddress", String.class, FieldAttribute.PRIMARY_KEY) + .addField("name", String.class) + .addField("ethName", String.class); + + oldVersion++; + } } @Override diff --git a/app/src/main/java/com/alphawallet/app/repository/ContactRepository.java b/app/src/main/java/com/alphawallet/app/repository/ContactRepository.java new file mode 100644 index 0000000000..0c4da6add1 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/ContactRepository.java @@ -0,0 +1,154 @@ +package com.alphawallet.app.repository; + +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.repository.entity.RealmContact; +import com.alphawallet.app.service.RealmManager; +import com.alphawallet.app.service.TokensService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Completable; +import io.reactivex.Single; +import io.realm.Realm; +import io.realm.RealmResults; +import timber.log.Timber; + +public class ContactRepository implements ContactRepositoryType { + + private RealmManager realmManager; + private TokensService tokensService; + private Wallet wallet; + + public ContactRepository(TokensService tokensService, RealmManager realmManager) { + this.tokensService = tokensService; + this.realmManager = realmManager; + + wallet = new Wallet(tokensService.getCurrentAddress()); + } + + @Override + public Single> getContacts() { + + return Single.fromCallable(this::getAllContacts); + } + + @Override + public Completable addContact(AddressBookContact contact) { + try (Realm realm = realmManager.getAddressBookRealmInstance()) + { + realm.beginTransaction(); + RealmContact realmContact = realm.createObject(RealmContact.class, contact.getWalletAddress()); + realmContact.setName(contact.getName()); + realmContact.setEthName(contact.getEthName()); + realm.commitTransaction(); + realm.close(); + return Completable.complete(); + } + catch (Exception e) + { + Timber.e(e); + return Completable.error(new RuntimeException("Failed to add Contact")); + } + + } + + @Override + public Completable updateContact(AddressBookContact oldContact, AddressBookContact newContact) { + Timber.d("New: %s, old: %s", newContact, oldContact); + try (Realm realm = realmManager.getAddressBookRealmInstance()) + { + realm.beginTransaction(); + RealmContact realmContact = realm.where(RealmContact.class) + .equalTo("walletAddress", oldContact.getWalletAddress()) + .findFirst(); + if (realmContact != null) { + realmContact.setName(newContact.getName()); + } + realm.commitTransaction(); + realm.close(); + if (realmContact != null) + return Completable.complete(); + else + return Completable.error(new RuntimeException("No matching contact found")); + } + catch (Exception e) + { + Timber.e(e); + return Completable.error(new RuntimeException("Failed to update Contact")); + } + } + + @Override + public Completable removeContact(AddressBookContact contact) { + try (Realm realm = realmManager.getAddressBookRealmInstance()) + { + realm.beginTransaction(); + RealmContact realmContact = realm.where(RealmContact.class) + .equalTo("walletAddress", contact.getWalletAddress()) + .findFirst(); + if (realmContact != null) { + realmContact.deleteFromRealm(); + } + realm.commitTransaction(); + realm.close(); + return Completable.complete(); + } + catch (Exception e) + { + Timber.e(e); + return Completable.error(new RuntimeException("Failed to remove Contact")); + } + } + + @Override + public Single searchContact(String walletAddress) { + return Single.fromCallable(new Callable() { + @Override + public AddressBookContact call() throws Exception { + List contacts = getAllContacts(); + if (contacts != null) { + return searchContactFromList(walletAddress, contacts); + } else { + return null; + } + } + }); + } + + private ArrayList getAllContacts() { + ArrayList contacts = new ArrayList<>(); + try (Realm realm = realmManager.getAddressBookRealmInstance()) { + RealmResults realmContacts = realm.where(RealmContact.class) + .findAll(); + + if (realmContacts != null) { + for (RealmContact item : realmContacts) { + contacts.add(createUserInfo(item)); + } + } + Collections.sort(contacts, (o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())); + return contacts; + } catch (Exception e) { + Timber.e(e); + return null; + } + } + + private AddressBookContact searchContactFromList(String walletAddress, List contacts) { + for (AddressBookContact contact : contacts) { + if (contact.getWalletAddress().equalsIgnoreCase(walletAddress)) { + return contact; + } + } + return null; + } + + private AddressBookContact createUserInfo(RealmContact realmItem) + { + return new AddressBookContact(realmItem.getWalletAddress(), realmItem.getName(), realmItem.getEthName()); + } +} diff --git a/app/src/main/java/com/alphawallet/app/repository/ContactRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/ContactRepositoryType.java new file mode 100644 index 0000000000..63a8b58759 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/ContactRepositoryType.java @@ -0,0 +1,21 @@ +package com.alphawallet.app.repository; + +import com.alphawallet.app.entity.AddressBookContact; + +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Single; + +/** + * Created by Asif Ghanchi on 24/11/21 + * Repository for managing Contacts for Address Book. + */ +public interface ContactRepositoryType { + + Single> getContacts(); + Completable addContact(AddressBookContact contact); + Completable updateContact(AddressBookContact oldContact, AddressBookContact newContact); + Completable removeContact(AddressBookContact contact); + Single searchContact(String walletAddress); +} diff --git a/app/src/main/java/com/alphawallet/app/repository/entity/RealmContact.java b/app/src/main/java/com/alphawallet/app/repository/entity/RealmContact.java new file mode 100644 index 0000000000..d6ed23076a --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/repository/entity/RealmContact.java @@ -0,0 +1,50 @@ +package com.alphawallet.app.repository.entity; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; + +/** + * Created by Chintan on 20/10/2021. + */ +public class RealmContact extends RealmObject +{ + /** + * This is contact wallet address + */ + @PrimaryKey + private String walletAddress; + + /** + * This is contact name + */ + private String name; + + /** + * This is associated ETH name of #walletAddress + */ + private String ethName; + + public String getWalletAddress() { + return walletAddress; + } + + public void setWalletAddress(String walletAddress) { + this.walletAddress = walletAddress; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEthName() { + return ethName; + } + + public void setEthName(String ethName) { + this.ethName = ethName; + } +} diff --git a/app/src/main/java/com/alphawallet/app/router/AddEditAddressRouter.java b/app/src/main/java/com/alphawallet/app/router/AddEditAddressRouter.java new file mode 100644 index 0000000000..b3719ffe6c --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/router/AddEditAddressRouter.java @@ -0,0 +1,39 @@ +package com.alphawallet.app.router; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; + +import com.alphawallet.app.C; +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.ui.AddEditAddressActivity; + +public class AddEditAddressRouter { + + public void open(Context context, int requestCode) { + ((Activity) context).startActivityForResult(new Intent(context, AddEditAddressActivity.class), requestCode); + } + + /** + * Use to auto fill wallet address when called from Transaction Detail screen. + * @param context + * @param walletAddress Wallet Address to auto fill + */ + public void open(Context context, int requestCode, String walletAddress) { + Intent intent = new Intent(context, AddEditAddressActivity.class); + intent.putExtra(C.EXTRA_CONTACT_WALLET_ADDRESS, walletAddress); + ((Activity) context).startActivityForResult(intent, requestCode); + } + + /** + * Use to pass a contact for editing. + * @param context + * @param requestCode + * @param contact + */ + public void open(Context context, int requestCode, AddressBookContact contact) { + Intent intent = new Intent(context, AddEditAddressActivity.class); + intent.putExtra(C.EXTRA_CONTACT, contact); + ((Activity) context).startActivityForResult(intent, requestCode); + } +} diff --git a/app/src/main/java/com/alphawallet/app/router/AddressBookRouter.java b/app/src/main/java/com/alphawallet/app/router/AddressBookRouter.java new file mode 100644 index 0000000000..7ed10d98ae --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/router/AddressBookRouter.java @@ -0,0 +1,19 @@ +package com.alphawallet.app.router; + +import android.content.Context; +import android.content.Intent; + +import com.alphawallet.app.ui.AddressBookActivity; +import com.alphawallet.app.ui.BaseActivity; + +public class AddressBookRouter { + + public void open(Context context) { + context.startActivity(new Intent(context, AddressBookActivity.class)); + } + + public void openForContactSelection(Context context, int requestCode) { + Intent i = new Intent(context, AddressBookActivity.class); + ((BaseActivity) context).startActivityForResult(i, requestCode); + } +} diff --git a/app/src/main/java/com/alphawallet/app/service/RealmManager.java b/app/src/main/java/com/alphawallet/app/service/RealmManager.java index 452891ef9c..738e6dbb1f 100644 --- a/app/src/main/java/com/alphawallet/app/service/RealmManager.java +++ b/app/src/main/java/com/alphawallet/app/service/RealmManager.java @@ -64,4 +64,8 @@ public Realm getWalletDataRealmInstance() { public Realm getWalletTypeRealmInstance() { return getRealmInstanceInternal("WalletType-db.realm"); } + + public Realm getAddressBookRealmInstance() { + return getRealmInstanceInternal("AddressBook-db.realm"); + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java b/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java index 3d4c9798cd..90e066a9bb 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/ActivityFragment.java @@ -180,7 +180,7 @@ private List getTokenTransfersForHash(Realm realm, Transactio private void initViews(View view) { adapter = new ActivityAdapter(viewModel.getTokensService(), viewModel.provideTransactionsInteract(), - viewModel.getAssetDefinitionService(), this); + viewModel.getAssetDefinitionService(), this, viewModel.getAddressBookInteract()); SwipeRefreshLayout refreshLayout = view.findViewById(R.id.refresh_layout); systemView = view.findViewById(R.id.system_view); listView = view.findViewById(R.id.list); diff --git a/app/src/main/java/com/alphawallet/app/ui/AddEditAddressActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddEditAddressActivity.java new file mode 100644 index 0000000000..129a4d3417 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/AddEditAddressActivity.java @@ -0,0 +1,225 @@ +package com.alphawallet.app.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.alphawallet.app.C; +import com.alphawallet.app.R; +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.ui.widget.entity.AddressReadyCallback; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.viewmodel.AddEditAddressViewModel; +import com.alphawallet.app.widget.InputAddress; +import com.alphawallet.app.widget.InputView; + +import java.util.HashMap; +import java.util.Map; + +import dagger.hilt.android.AndroidEntryPoint; +import java8.util.Optional; +import timber.log.Timber; + +@AndroidEntryPoint +public class AddEditAddressActivity extends BaseActivity implements AddressReadyCallback { + private static final String TAG = "AddAddressActivity"; + + AddEditAddressViewModel viewModel; + + private ADDRESS_OPERATION_MODE mode = ADDRESS_OPERATION_MODE.ADD; // default + private InputAddress inputViewAddress; + private InputView inputViewName; + private Button buttonSave; + private AddressBookContact contactToEdit = null; + private Map ensResolutionCache = new HashMap<>(); // address -> ensName cache to get ens name of entered wallet address + private enum ADDRESS_OPERATION_MODE { + ADD, EDIT + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_add_address); + + toolbar(); + + contactToEdit = (AddressBookContact) getIntent().getParcelableExtra(C.EXTRA_CONTACT); + if (contactToEdit == null) { + setTitle(getString(R.string.title_add_address)); + } else { + mode = ADDRESS_OPERATION_MODE.EDIT; + setTitle(getString(R.string.title_edit_address)); + } + initViews(); + + viewModel.address.observe(this, this::onAddressCheck); + viewModel.contactName.observe(this, this::onNameCheck); + viewModel.saveContact.observe(this, this::onSave); + viewModel.updateContact.observe(this, this::onUpdateContact); + viewModel.error.observe(this, this::onError); + } + + private void initViews() { + viewModel = new ViewModelProvider(this) + .get(AddEditAddressViewModel.class); + + inputViewAddress = findViewById(R.id.input_view_address); + inputViewName = findViewById(R.id.input_view_name); + + inputViewAddress.setAddressCallback(this); +// inputViewAddress.enableFocusListener(); + inputViewName.enableFocusListener(); + + inputViewAddress.getEditText().setImeOptions(EditorInfo.IME_ACTION_NEXT); + + inputViewName.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE); + + buttonSave = findViewById(R.id.btn_save); + buttonSave.setOnClickListener( (v) -> { + String address = inputViewAddress.getInputText(); + String name = inputViewName.getText().toString().trim(); + boolean isDataValid = true; + if (address.isEmpty()) { + inputViewAddress.setError(getString(R.string.error_address_empty)); + isDataValid = false; + } else if (!Utils.isAddressValid(address)) { + inputViewAddress.setError(getString(R.string.error_invalid_address)); + isDataValid = false; + } + if (name.isEmpty()) { + inputViewName.setError("Name should not be empty"); + isDataValid = false; + } + if (isDataValid) { + if (mode == ADDRESS_OPERATION_MODE.ADD) { + String ensName = ensResolutionCache.get(inputViewAddress.getInputText()); + ensName = ensName == null ? "" : ensName; + viewModel.onClickSave(address, name, ensName); + } else { + viewModel.updateContact(contactToEdit, new AddressBookContact(address, name, contactToEdit.getEthName())); + } + } + }); + + if (mode == ADDRESS_OPERATION_MODE.EDIT) { + inputViewAddress.setHandleENS(false); + inputViewAddress.setAddressCallback(null); + inputViewAddress.setAddress(contactToEdit.getWalletAddress()); + inputViewName.setText(contactToEdit.getName()); + inputViewAddress.setEditable(false); + inputViewName.requestFocus(); + } else { + String walletAddress = getIntent().getStringExtra(C.EXTRA_CONTACT_WALLET_ADDRESS); + if (walletAddress != null) { + inputViewAddress.setAddress(walletAddress); + } + } + } + + private void onAddressCheck(Optional userInfo) { + if (userInfo.isPresent()) { + // show error + inputViewAddress.setError(getString(R.string.error_contact_address_exists, userInfo.get().getName())); + } else { + viewModel.checkName(inputViewName.getText().toString()); + } + } + + private void onNameCheck(Optional userInfo) { + if (userInfo.isPresent()) { + // show error + inputViewName.setError(getString(R.string.error_contact_name_taken)); + } else { + viewModel.addContact(); + } + } + + private void onSave(Boolean isSaved) { + if (isSaved) { + setResult(RESULT_OK); + finish(); + } + + } + + private void onUpdateContact(boolean success) { + if (success) { + setResult(RESULT_OK); + } else { + setResult(RESULT_CANCELED); + } + finish(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mode == ADDRESS_OPERATION_MODE.EDIT) { + getMenuInflater().inflate(R.menu.menu_close, menu); + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_cancel) { + super.onBackPressed(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case C.BARCODE_READER_REQUEST_CODE: { + if (resultCode == RESULT_OK && data != null) { + String qrData = data.getStringExtra(C.EXTRA_QR_CODE); + Timber.d("onActivityResult: data: "+data+"\nqr: "+qrData); + if (Utils.isAddressValid(qrData)) { + inputViewAddress.setAddress(qrData); + } + } + } + } + } + + private void onError(Throwable error) { + Timber.e(error, "onError: "); + displayToast(error.getMessage()); + } + + @Override + public void addressReady(String address, String ensName) { + Timber.d("Address ready: address: %s, ensName: %s", address, ensName); + + } + + @Override + public void resolvedAddress(String address, String ensName) { + AddressReadyCallback.super.resolvedAddress(address, ensName); + Timber.d("resolvedAddress: %s, %s", address, ensName); + // if the input address content is unchanged and this resolution is for that content + if (inputViewAddress.getInputText().equalsIgnoreCase(ensName) || inputViewAddress.getInputText().equalsIgnoreCase(address)) { + inputViewAddress.setHandleENS(false); // disable ens resolver + inputViewAddress.setAddress(address); // set the resolved address + inputViewAddress.setHandleENS(true); // enable ens resolver + } + inputViewName.setText(ensName); // prefill name using ens name + ensResolutionCache.put(address, ensName); + } + + @Override + public void addressValid(boolean valid) { + AddressReadyCallback.super.addressValid(valid); + Timber.d("address Valid: %s", valid); + } + +} diff --git a/app/src/main/java/com/alphawallet/app/ui/AddressBookActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddressBookActivity.java new file mode 100644 index 0000000000..e1149393ec --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/AddressBookActivity.java @@ -0,0 +1,314 @@ +package com.alphawallet.app.ui; + +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.C; +import com.alphawallet.app.R; +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.router.AddEditAddressRouter; +import com.alphawallet.app.router.HomeRouter; +import com.alphawallet.app.ui.widget.adapter.AddressBookAdapter; +import com.alphawallet.app.ui.widget.adapter.TokenListAdapter; +import com.alphawallet.app.ui.widget.decorator.RecyclerViewSwipeDecorator; +import com.alphawallet.app.viewmodel.AddressBookViewModel; +import com.alphawallet.app.widget.AWalletAlertDialog; +import com.google.android.material.snackbar.Snackbar; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import dagger.hilt.android.AndroidEntryPoint; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; +import timber.log.Timber; + +@AndroidEntryPoint +public class AddressBookActivity extends BaseActivity +{ + private static final String TAG = "AddressBookActivity"; + + AddressBookViewModel viewModel; + + private RecyclerView contactList; + private Button saveButton; + private TokenListAdapter adapter; + private EditText search; + private LinearLayout searchTokens; + private LinearLayout emptyAddressBook; + private Button addContactButton; + + private Wallet wallet; + private String realmId; + + private Handler delayHandler = new Handler(Looper.getMainLooper()); + + private final ArrayList contacts = new ArrayList<>(); + + /** + * Flag to check whether activity is started for result or not. + */ + private Boolean setResult = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_address_book); + toolbar(); + setResult = getCallingActivity() != null; + setTitle(setResult ? getString(R.string.title_select_recipient) : getString(R.string.title_address_book)); + initViews(); + + + } + + private void initViews() + { + viewModel = new ViewModelProvider(this) + .get(AddressBookViewModel.class); + + viewModel.contactsLD.observe(this, this::updateContactList); + viewModel.filteredContacts.observe(this, this::onContactsFiltered); + +// wallet = new Wallet(viewModel.getTokensService().getCurrentAddress()); + contactList = findViewById(R.id.contact_list); + saveButton = findViewById(R.id.btn_apply); + search = findViewById(R.id.edit_search); + searchTokens = findViewById(R.id.layout_search_tokens); + emptyAddressBook = findViewById(R.id.layout_empty_address_book); + addContactButton = findViewById(R.id.btn_add_contact); + + contactList.setLayoutManager(new LinearLayoutManager(this)); + + saveButton.setOnClickListener(v -> { + new HomeRouter().open(this, true); + }); + + addContactButton.setOnClickListener( (v) -> { + startAddAddressActivity(); + }); + + contactList.requestFocus(); +// search.addTextChangedListener(textWatcher); + setupSearch(); + + AddressBookAdapter adapter = new AddressBookAdapter(contacts, this::OnAddressSelected); + contactList.setAdapter(adapter); + + // Create and add a callback + ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + try + { + if (direction == ItemTouchHelper.RIGHT) + { + return; + } + final int position = viewHolder.getAdapterPosition(); + final AddressBookContact item = adapter.removeItem(position); + viewModel.removeContact(item); + Snackbar snackbar = Snackbar.make(viewHolder.itemView, "Item " + (direction == ItemTouchHelper.LEFT ? "deleted" : "archived") + ".", Snackbar.LENGTH_LONG); + snackbar.setAction(android.R.string.cancel, new View.OnClickListener() { + + @Override + public void onClick(View view) { + try + { + adapter.addItem(item, position); + viewModel.addContact(item); + } + catch(Exception e) + { + Timber.e(e); + } + } + }); + snackbar.show(); + } + catch(Exception e) + { + Timber.e(e); + } + } + + // You must use @RecyclerViewSwipeDecorator inside the onChildDraw method + @Override + public void onChildDraw (Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive){ + new RecyclerViewSwipeDecorator.Builder(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + .addSwipeLeftBackgroundColor(ContextCompat.getColor(AddressBookActivity.this, R.color.danger)) + .addSwipeLeftActionIcon(R.drawable.ic_delete_contact) + .addSwipeLeftLabel(getString(R.string.delete)) + .setSwipeLeftLabelColor(Color.WHITE) + .create() + .decorate(); + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + }; + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback); + if (!setResult) itemTouchHelper.attachToRecyclerView(contactList); + + } + + private void updateContactList(List contacts) + { + if (contacts.size() == 0) { + searchTokens.setVisibility(View.INVISIBLE); + contactList.setVisibility(View.INVISIBLE); + emptyAddressBook.setVisibility(View.VISIBLE); + } else { + searchTokens.setVisibility(View.VISIBLE); + contactList.setVisibility(View.VISIBLE); + emptyAddressBook.setVisibility(View.INVISIBLE); + } + this.contacts.clear(); + this.contacts.addAll(contacts); + for (AddressBookContact contact : contacts) + { + Timber.d("Contact : " + contact.getWalletAddress() + ", " + contact.getName()); + } + contactList.getAdapter().notifyDataSetChanged(); + } + + private void OnAddressSelected(int position, AddressBookContact addressBookContact) { + Timber.d("OnAddressSelected: pos: %s", position); + + if (setResult) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(C.EXTRA_CONTACT, addressBookContact); + setResult(RESULT_OK, resultIntent); + finish(); + } else { + // edit address + new AddEditAddressRouter().open(this, C.UPDATE_ADDRESS_REQUEST_CODE, addressBookContact); + } + } + + private void setupSearch() { + search.setImeOptions(EditorInfo.IME_ACTION_DONE); + PublishSubject subject = PublishSubject.create(); + subject.debounce(300, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(viewModel::filterByName, (throwable -> Timber.e(throwable, "searchException"))) + .isDisposed(); + + search.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + subject.onNext(s.toString()); + } + }); + } + + private void onContactsFiltered(List contacts) { + this.contacts.clear(); + this.contacts.addAll(contacts); + for (AddressBookContact contact : contacts) + { + Timber.d("onContactsFiltered: Contact : " + contact.getWalletAddress() + ", " + contact.getName()); + } + contactList.getAdapter().notifyDataSetChanged(); + } + +// private void showSnackBar(String text) +// { +// Snackbar.make(findViewById(R.id.button_sign_in), text, Snackbar.LENGTH_LONG).show(); +// } + + @Override + protected void onResume() { + super.onResume(); + viewModel.loadContacts(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (!setResult) getMenuInflater().inflate(R.menu.menu_add, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add: { + startAddAddressActivity(); + } + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + switch (requestCode) { + case C.ADD_ADDRESS_REQUEST_CODE: { + if (resultCode == RESULT_OK) { + // show tick dialog + AWalletAlertDialog dialog = new AWalletAlertDialog(this); + dialog.setIcon(AWalletAlertDialog.SUCCESS); + dialog.setMessage(getString(R.string.msg_address_added_succes)); + dialog.setTextStyle(AWalletAlertDialog.TEXT_STYLE.CENTERED); + dialog.show(); + } + } + break; + + case C.UPDATE_ADDRESS_REQUEST_CODE: { + if (resultCode == RESULT_OK) { + // show tick dialog + AWalletAlertDialog dialog = new AWalletAlertDialog(this); + dialog.setIcon(AWalletAlertDialog.SUCCESS); + dialog.setMessage(getString(R.string.msg_address_update_succes)); + dialog.setTextStyle(AWalletAlertDialog.TEXT_STYLE.CENTERED); + dialog.show(); + viewModel.loadContacts(); + } + } + break; + } + super.onActivityResult(requestCode, resultCode, data); + } + + private void startAddAddressActivity() { + new AddEditAddressRouter().open(this, C.ADD_ADDRESS_REQUEST_CODE); + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java index dbece60b0f..95804d6040 100644 --- a/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/Erc20DetailActivity.java @@ -229,7 +229,7 @@ private void setUpRecentTransactionsView() if (activityHistoryList != null) return; activityHistoryList = findViewById(R.id.history_list); ActivityAdapter adapter = new ActivityAdapter(viewModel.getTokensService(), viewModel.getTransactionsInteract(), - viewModel.getAssetDefinitionService()); + viewModel.getAssetDefinitionService(), viewModel.getAddressBookInteract()); adapter.setDefaultWallet(wallet); diff --git a/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java b/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java index a161916e02..32fdd013f8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/NewSettingsFragment.java @@ -40,6 +40,7 @@ import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.router.AddressBookRouter; import com.alphawallet.app.util.LocaleUtils; import com.alphawallet.app.util.UpdateUtils; import com.alphawallet.app.viewmodel.NewSettingsViewModel; @@ -72,6 +73,7 @@ public class NewSettingsFragment extends BaseFragment { private SettingsItemView walletConnectSetting; private SettingsItemView showSeedPhrase; private SettingsItemView nameThisWallet; + private SettingsItemView addressBook; private LinearLayout layoutBackup; private View warningSeparator; @@ -186,6 +188,13 @@ private void initializeSettings(View view) { .withListener(this::onNameThisWallet) .build(); + addressBook = + new SettingsItemView.Builder(getContext()) + .withIcon(R.drawable.ic_address_book) + .withTitle(R.string.title_address_book) + .withListener(this::onAddressBookSettingClicked) + .build(); + walletConnectSetting = new SettingsItemView.Builder(getContext()) .withIcon(R.drawable.ic_wallet_connect) @@ -248,6 +257,8 @@ private void addSettingsToLayout() { walletSettingsLayout.addView(nameThisWallet, walletIndex++); + walletSettingsLayout.addView(addressBook, walletIndex++); + walletSettingsLayout.addView(walletConnectSetting, walletIndex++); systemSettingsLayout.addView(notificationsSetting, systemIndex++); @@ -486,6 +497,15 @@ private void onNameThisWallet() requireActivity().startActivity(intent); } + private void onAddressBookSettingClicked() { + new AddressBookRouter().open(getContext()); + } + + private void openAddressBookActivity() { + Intent intent = new Intent(getActivity(), AddressBookActivity.class); + getActivity().startActivity(intent); + } + private void onNotificationsSettingClicked() { viewModel.setNotificationState(notificationsSetting.getToggleState()); } diff --git a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java index bcd1ba6671..57f6f8c71b 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java @@ -1,7 +1,6 @@ package com.alphawallet.app.ui; import static com.alphawallet.app.C.Key.WALLET; -import static com.alphawallet.app.repository.EthereumNetworkBase.hasGasOverride; import static com.alphawallet.app.widget.AWalletAlertDialog.ERROR; import static com.alphawallet.app.widget.AWalletAlertDialog.WARNING; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; @@ -30,6 +29,7 @@ import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionData; +import com.alphawallet.app.entity.AddressBookContact; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkBase; @@ -287,6 +287,15 @@ else if (qrCode.startsWith("wc:")) )); break; } + } else if (requestCode == C.ADDRESS_BOOK_CONTACT_REQUEST_CODE) { + if (data != null) { + // set selected contact returned by AddressBook + AddressBookContact addressBookContact = data.getParcelableExtra(C.EXTRA_CONTACT); + if (addressBookContact != null) { + addressInput.onContactSelected(addressBookContact); + } + } +// addressInput.onContactSelected(); } else { super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenActivityFragment.java b/app/src/main/java/com/alphawallet/app/ui/TokenActivityFragment.java index 32011891c2..945e4b69b3 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenActivityFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenActivityFragment.java @@ -66,7 +66,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat private void setUpRecentTransactionsView() { ActivityAdapter adapter = new ActivityAdapter(viewModel.getTokensService(), viewModel.getTransactionsInteract(), - viewModel.getAssetDefinitionService()); + viewModel.getAssetDefinitionService(), viewModel.getAddressBookInteract()); adapter.setDefaultWallet(wallet); history.setupAdapter(adapter); history.startActivityListeners(viewModel.getRealmInstance(wallet), wallet, diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java index a9deca3260..891993d03e 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java @@ -161,7 +161,7 @@ private void setTokenListener() private void setEventListener(Wallet wallet) { ActivityAdapter adapter = new ActivityAdapter(viewModel.getTokensService(), viewModel.getTransactionsInteract(), - viewModel.getAssetDefinitionService()); + viewModel.getAssetDefinitionService(), viewModel.getAddressBookInteract()); adapter.setDefaultWallet(wallet); diff --git a/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java index 383fd98036..ae0a03fa72 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TransactionDetailActivity.java @@ -21,6 +21,7 @@ import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionData; +import com.alphawallet.app.entity.AddressBookContact; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkRepository; @@ -72,6 +73,9 @@ public class TransactionDetailActivity extends BaseActivity implements StandardF private ActionSheetDialog confirmationDialog; private String tokenAddress; + private CopyTextView toValue; + private CopyTextView fromValue; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -83,12 +87,17 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { viewModel.onTransaction().observe(this, this::onTransaction); viewModel.transactionFinalised().observe(this, this::txWritten); viewModel.transactionError().observe(this, this::txError); + viewModel.senderAddressContact().observe(this, this::onSenderContactFound); + viewModel.receiverAddressContact().observe(this, this::onReceiverContactFound); String txHash = getIntent().getStringExtra(C.EXTRA_TXHASH); long chainId = getIntent().getLongExtra(C.EXTRA_CHAIN_ID, MAINNET_ID); wallet = getIntent().getParcelableExtra(WALLET); tokenAddress = getIntent().getStringExtra(C.EXTRA_ADDRESS); viewModel.fetchTransaction(wallet, txHash, chainId); + + toValue = findViewById(R.id.to); + fromValue = findViewById(R.id.from); } private void onTransaction(Transaction tx) @@ -119,12 +128,16 @@ private void onTransaction(Transaction tx) setupVisibilities(); amount = findViewById(R.id.amount); - CopyTextView toValue = findViewById(R.id.to); - CopyTextView fromValue = findViewById(R.id.from); + toValue = findViewById(R.id.to); + fromValue = findViewById(R.id.from); CopyTextView txHashView = findViewById(R.id.txn_hash); fromValue.setText(transaction.from != null ? transaction.from : ""); toValue.setText(transaction.to != null ? transaction.to : ""); + + viewModel.resolveSenderAddress(fromValue.getText()); + viewModel.resolveReceiverAddress(toValue.getText()); + txHashView.setText(transaction.hash != null ? transaction.hash : ""); ((TextView) findViewById(R.id.txn_time)).setText(Utils.localiseUnixDate(getApplicationContext(), transaction.timeStamp)); @@ -367,6 +380,19 @@ else if (requestCode >= SignTransactionDialog.REQUEST_CODE_CONFIRM_DEVICE_CREDEN confirmationDialog.completeSignRequest(resultCode == RESULT_OK); } } + else if (requestCode == C.ADD_ADDRESS_REQUEST_CODE) { + if (resultCode == RESULT_OK) { + viewModel.loadAddressBook(); + viewModel.resolveSenderAddress(fromValue.getText()); + viewModel.resolveReceiverAddress(toValue.getText()); + // show tick dialog + AWalletAlertDialog dialog = new AWalletAlertDialog(this); + dialog.setIcon(AWalletAlertDialog.SUCCESS); + dialog.setMessage(getString(R.string.msg_address_added_succes)); + dialog.setTextStyle(AWalletAlertDialog.TEXT_STYLE.CENTERED); + dialog.show(); + } + } else { super.onActivityResult(requestCode, resultCode, data); @@ -428,4 +454,14 @@ private void txError(Throwable throwable) dialog.show(); confirmationDialog.dismiss(); } + + private void onSenderContactFound(AddressBookContact addressBookContact) { + CopyTextView fromValue = findViewById(R.id.from); + fromValue.setAddressName(addressBookContact.getName()); + } + + private void onReceiverContactFound(AddressBookContact addressBookContact) { + CopyTextView toValue = findViewById(R.id.to); + toValue.setAddressName(addressBookContact.getName()); + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/OnAddressBookItemCLickListener.java b/app/src/main/java/com/alphawallet/app/ui/widget/OnAddressBookItemCLickListener.java new file mode 100644 index 0000000000..f48998f5d5 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/OnAddressBookItemCLickListener.java @@ -0,0 +1,7 @@ +package com.alphawallet.app.ui.widget; + +import com.alphawallet.app.entity.AddressBookContact; + +public interface OnAddressBookItemCLickListener { + void OnAddressSelected(int position, AddressBookContact addressBookContact); +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ActivityAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ActivityAdapter.java index f4feead330..1bec7f6cf7 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ActivityAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ActivityAdapter.java @@ -24,6 +24,7 @@ import com.alphawallet.app.entity.TransactionMeta; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.interact.ActivityDataInteract; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; @@ -92,6 +93,7 @@ public void onMoved(int fromPosition, int toPosition) { private final FetchTransactionsInteract fetchTransactionsInteract; private final ActivityDataInteract dataInteract; private final AssetDefinitionService assetService; + private final AddressBookInteract addressBookInteract; private long fetchData = 0; private final Handler handler = new Handler(); private int itemLimit = 0; @@ -99,19 +101,21 @@ public void onMoved(int fromPosition, int toPosition) { private boolean pendingReset = false; public ActivityAdapter(TokensService service, FetchTransactionsInteract fetchTransactionsInteract, - AssetDefinitionService svs, ActivityDataInteract dataInteract) { + AssetDefinitionService svs, ActivityDataInteract dataInteract, AddressBookInteract addressBookInteract) { this.fetchTransactionsInteract = fetchTransactionsInteract; this.dataInteract = dataInteract; this.assetService = svs; tokensService = service; + this.addressBookInteract = addressBookInteract; } - public ActivityAdapter(TokensService service, FetchTransactionsInteract fetchTransactionsInteract, AssetDefinitionService svs) + public ActivityAdapter(TokensService service, FetchTransactionsInteract fetchTransactionsInteract, AssetDefinitionService svs, AddressBookInteract addressBookInteract) { this.fetchTransactionsInteract = fetchTransactionsInteract; tokensService = service; this.dataInteract = null; this.assetService = svs; + this.addressBookInteract = addressBookInteract; } @Override @@ -119,7 +123,7 @@ public BinderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case TransactionHolder.VIEW_TYPE: return new TransactionHolder(parent, tokensService, fetchTransactionsInteract, - assetService); + assetService, addressBookInteract); case EventHolder.VIEW_TYPE: return new EventHolder(parent, tokensService, fetchTransactionsInteract, assetService, this); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/AddressBookAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/AddressBookAdapter.java new file mode 100644 index 0000000000..2502522061 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/AddressBookAdapter.java @@ -0,0 +1,101 @@ +package com.alphawallet.app.ui.widget.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alphawallet.app.R; +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.ui.widget.OnAddressBookItemCLickListener; + +import java.util.List; + +import timber.log.Timber; + +public class AddressBookAdapter extends RecyclerView.Adapter { + private List data; + private OnAddressBookItemCLickListener listener; + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView icon; + TextView name, address; + View root; + + ViewHolder(@NonNull View itemView) { + super(itemView); + icon = itemView.findViewById(R.id.icon); + name = itemView.findViewById(R.id.name); + address = itemView.findViewById(R.id.address); + root = itemView.findViewById(R.id.layout_list_item); + } + } + + public AddressBookAdapter(List data, OnAddressBookItemCLickListener listener) { + this.data = data; + this.listener = listener; + } + + public void addItem(AddressBookContact item, int position) { + try { + data.add(position, item); + notifyItemInserted(position); + } catch(Exception e) { + Timber.e(e); + } + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_contact, viewGroup, false); + + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { + AddressBookContact addressBookContact = data.get(i); + viewHolder.name.setText(addressBookContact.getName()); + viewHolder.address.setText(addressBookContact.getWalletAddress()); + + if (!addressBookContact.getEthName().isEmpty()) { + viewHolder.address.setText(addressBookContact.getEthName() + " | " + addressBookContact.getWalletAddress()); + } else { + viewHolder.address.setText(addressBookContact.getWalletAddress()); + } + viewHolder.root.setOnClickListener( (v ->{ + listener.OnAddressSelected(i, data.get(i)); + })); + } + + @Override + public int getItemCount() { + return data.size(); + } + + public AddressBookContact removeItem(int position) { + AddressBookContact item = null; + try + { + item = data.get(position); + data.remove(position); + notifyItemRemoved(position); + } + catch(Exception e) + { + Timber.e(e); + } + return item; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/decorator/RecyclerViewSwipeDecorator.java b/app/src/main/java/com/alphawallet/app/ui/widget/decorator/RecyclerViewSwipeDecorator.java new file mode 100644 index 0000000000..f6b4c60151 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/ui/widget/decorator/RecyclerViewSwipeDecorator.java @@ -0,0 +1,599 @@ +package com.alphawallet.app.ui.widget.decorator; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.Log; +import android.util.TypedValue; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +/** + * A simple utility class to add a background and/or an icon while swiping a RecyclerView item left or right. + */ + +public class RecyclerViewSwipeDecorator { + private Canvas canvas; + private RecyclerView recyclerView; + private RecyclerView.ViewHolder viewHolder; + private float dX; + private float dY; + private int actionState; + private boolean isCurrentlyActive; + + private int swipeLeftBackgroundColor; + private int swipeLeftActionIconId; + private Integer swipeLeftActionIconTint; + + private int swipeRightBackgroundColor; + private int swipeRightActionIconId; + private Integer swipeRightActionIconTint; + + private int iconHorizontalMargin; + + private String mSwipeLeftText; + private float mSwipeLeftTextSize = 14; + private int mSwipeLeftTextUnit = TypedValue.COMPLEX_UNIT_SP; + private int mSwipeLeftTextColor = Color.DKGRAY; + private Typeface mSwipeLeftTypeface = Typeface.SANS_SERIF; + + private String mSwipeRightText; + private float mSwipeRightTextSize = 14; + private int mSwipeRightTextUnit = TypedValue.COMPLEX_UNIT_SP; + private int mSwipeRightTextColor = Color.DKGRAY; + private Typeface mSwipeRightTypeface = Typeface.SANS_SERIF; + + private RecyclerViewSwipeDecorator() { + swipeLeftBackgroundColor = 0; + swipeRightBackgroundColor = 0; + swipeLeftActionIconId = 0; + swipeRightActionIconId = 0; + swipeLeftActionIconTint = null; + swipeRightActionIconTint = null; + } + + /** + * Create a @RecyclerViewSwipeDecorator + * @param context A valid Context object for the RecyclerView + * @param canvas The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either ACTION_STATE_DRAG or ACTION_STATE_SWIPE. + * @param isCurrentlyActive True if this view is currently being controlled by the user or false it is simply animating back to its original state + * + * @deprecated in RecyclerViewSwipeDecorator 1.2.2 + */ + @Deprecated + public RecyclerViewSwipeDecorator(Context context, Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + this(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + + /** + * Create a @RecyclerViewSwipeDecorator + * @param canvas The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either ACTION_STATE_DRAG or ACTION_STATE_SWIPE. + * @param isCurrentlyActive True if this view is currently being controlled by the user or false it is simply animating back to its original state + */ + public RecyclerViewSwipeDecorator(Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + this(); + this.canvas = canvas; + this.recyclerView = recyclerView; + this.viewHolder = viewHolder; + this.dX = dX; + this.dY = dY; + this.actionState = actionState; + this.isCurrentlyActive = isCurrentlyActive; + this.iconHorizontalMargin = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, recyclerView.getContext().getResources().getDisplayMetrics()); + } + + /** + * Set the background color for either (left/right) swipe directions + * @param backgroundColor The resource id of the background color to be set + */ + public void setBackgroundColor(int backgroundColor) { + this.swipeLeftBackgroundColor = backgroundColor; + this.swipeRightBackgroundColor = backgroundColor; + } + + /** + * Set the action icon for either (left/right) swipe directions + * @param actionIconId The resource id of the icon to be set + */ + public void setActionIconId(int actionIconId) { + this.swipeLeftActionIconId = actionIconId; + this.swipeRightActionIconId = actionIconId; + } + + /** + * Set the tint color for either (left/right) action icons + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + */ + public void setActionIconTint(int color) { + this.setSwipeLeftActionIconTint(color); + this.setSwipeRightActionIconTint(color); + } + + /** + * Set the background color for left swipe direction + * @param swipeLeftBackgroundColor The resource id of the background color to be set + */ + public void setSwipeLeftBackgroundColor(int swipeLeftBackgroundColor) { + this.swipeLeftBackgroundColor = swipeLeftBackgroundColor; + } + + /** + * Set the action icon for left swipe direction + * @param swipeLeftActionIconId The resource id of the icon to be set + */ + public void setSwipeLeftActionIconId(int swipeLeftActionIconId) { + this.swipeLeftActionIconId = swipeLeftActionIconId; + } + + /** + * Set the tint color for action icon drawn while swiping left + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + */ + public void setSwipeLeftActionIconTint(int color) { + swipeLeftActionIconTint = color; + } + + /** + * Set the background color for right swipe direction + * @param swipeRightBackgroundColor The resource id of the background color to be set + */ + public void setSwipeRightBackgroundColor(int swipeRightBackgroundColor) { + this.swipeRightBackgroundColor = swipeRightBackgroundColor; + } + + /** + * Set the action icon for right swipe direction + * @param swipeRightActionIconId The resource id of the icon to be set + */ + public void setSwipeRightActionIconId(int swipeRightActionIconId) { + this.swipeRightActionIconId = swipeRightActionIconId; + } + + /** + * Set the tint color for action icon drawn while swiping right + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + */ + public void setSwipeRightActionIconTint(int color) { + swipeRightActionIconTint = color; + } + + /** + * Set the label shown when swiping right + * @param label a String + */ + public void setSwipeRightLabel(String label) { + mSwipeRightText = label; + } + + /** + * Set the size of the text shown when swiping right + * @param unit TypedValue (default is COMPLEX_UNIT_SP) + * @param size the size value + */ + public void setSwipeRightTextSize(int unit, float size) { + mSwipeRightTextUnit = unit; + mSwipeRightTextSize = size; + } + + /** + * Set the color of the text shown when swiping right + * @param color the color to be set + */ + public void setSwipeRightTextColor(int color) { + mSwipeRightTextColor = color; + } + + /** + * Set the Typeface of the text shown when swiping right + * @param typeface the Typeface to be set + */ + public void setSwipeRightTypeface(Typeface typeface) { + mSwipeRightTypeface = typeface; + } + + /** + * Set the horizontal margin of the icon in DPs (default is 16dp) + * @param iconHorizontalMargin the margin in pixels + * + * @deprecated in RecyclerViewSwipeDecorator 1.2, use {@link #setIconHorizontalMargin(int, int)} instead. + */ + @Deprecated + public void setIconHorizontalMargin(int iconHorizontalMargin) { + setIconHorizontalMargin(TypedValue.COMPLEX_UNIT_DIP, iconHorizontalMargin); + } + + /** + * Set the horizontal margin of the icon in the given unit (default is 16dp) + * @param unit TypedValue + * @param iconHorizontalMargin the margin in the given unit + */ + public void setIconHorizontalMargin(int unit, int iconHorizontalMargin) { + this.iconHorizontalMargin = (int)TypedValue.applyDimension(unit, iconHorizontalMargin, recyclerView.getContext().getResources().getDisplayMetrics()); + } + + /** + * Set the label shown when swiping left + * @param label a String + */ + public void setSwipeLeftLabel(String label) { + mSwipeLeftText = label; + } + + /** + * Set the size of the text shown when swiping left + * @param unit TypedValue (default is COMPLEX_UNIT_SP) + * @param size the size value + */ + public void setSwipeLeftTextSize(int unit, float size) { + mSwipeLeftTextUnit = unit; + mSwipeLeftTextSize = size; + } + + /** + * Set the color of the text shown when swiping left + * @param color the color to be set + */ + public void setSwipeLeftTextColor(int color) { + mSwipeLeftTextColor = color; + } + + /** + * Set the Typeface of the text shown when swiping left + * @param typeface the Typeface to be set + */ + public void setSwipeLeftTypeface(Typeface typeface) { + mSwipeLeftTypeface = typeface; + } + + /** + * Decorate the RecyclerView item with the chosen backgrounds and icons + */ + public void decorate() { + try { + if ( actionState != ItemTouchHelper.ACTION_STATE_SWIPE ) return; + + if ( dX > 0 ) { + // Swiping Right + canvas.clipRect(viewHolder.itemView.getLeft(), viewHolder.itemView.getTop(), viewHolder.itemView.getLeft() + (int) dX, viewHolder.itemView.getBottom()); + if ( swipeRightBackgroundColor != 0 ) { + final ColorDrawable background = new ColorDrawable(swipeRightBackgroundColor); + background.setBounds(viewHolder.itemView.getLeft(), viewHolder.itemView.getTop(), viewHolder.itemView.getLeft() + (int) dX, viewHolder.itemView.getBottom()); + background.draw(canvas); + } + + int iconSize = 0; + if ( swipeRightActionIconId != 0 && dX > iconHorizontalMargin ) { + Drawable icon = ContextCompat.getDrawable(recyclerView.getContext(), swipeRightActionIconId); + if ( icon != null ) { + iconSize = icon.getIntrinsicHeight(); + int halfIcon = iconSize / 2; + int top = viewHolder.itemView.getTop() + ((viewHolder.itemView.getBottom() - viewHolder.itemView.getTop()) / 2 - halfIcon); + icon.setBounds(viewHolder.itemView.getLeft() + iconHorizontalMargin, top, viewHolder.itemView.getLeft() + iconHorizontalMargin + icon.getIntrinsicWidth(), top + icon.getIntrinsicHeight()); + if (swipeRightActionIconTint != null) + icon.setColorFilter(swipeRightActionIconTint, PorterDuff.Mode.SRC_IN); + icon.draw(canvas); + } + } + + if ( mSwipeRightText != null && mSwipeRightText.length() > 0 && dX > iconHorizontalMargin + iconSize) { + TextPaint textPaint = new TextPaint(); + textPaint.setAntiAlias(true); + textPaint.setTextSize(TypedValue.applyDimension(mSwipeRightTextUnit, mSwipeRightTextSize, recyclerView.getContext().getResources().getDisplayMetrics())); + textPaint.setColor(mSwipeRightTextColor); + textPaint.setTypeface(mSwipeRightTypeface); + + int textTop = (int) (viewHolder.itemView.getTop() + ((viewHolder.itemView.getBottom() - viewHolder.itemView.getTop()) / 2.0) + textPaint.getTextSize()/2); + canvas.drawText(mSwipeRightText, viewHolder.itemView.getLeft() + iconHorizontalMargin + iconSize + (iconSize > 0 ? iconHorizontalMargin/2 : 0), textTop,textPaint); + } + + } else if ( dX < 0 ) { + // Swiping Left + canvas.clipRect(viewHolder.itemView.getRight() + (int) dX, viewHolder.itemView.getTop(), viewHolder.itemView.getRight(), viewHolder.itemView.getBottom()); + if ( swipeLeftBackgroundColor != 0 ) { + final ColorDrawable background = new ColorDrawable(swipeLeftBackgroundColor); + background.setBounds(viewHolder.itemView.getRight() + (int) dX, viewHolder.itemView.getTop(), viewHolder.itemView.getRight(), viewHolder.itemView.getBottom()); + background.draw(canvas); + } + + int iconSize = 0; + int imgLeft = viewHolder.itemView.getRight(); + if ( swipeLeftActionIconId != 0 && dX < - iconHorizontalMargin ) { + Drawable icon = ContextCompat.getDrawable(recyclerView.getContext(), swipeLeftActionIconId); + if ( icon != null ) { + iconSize = icon.getIntrinsicHeight(); + int halfIcon = iconSize / 2; + int top = viewHolder.itemView.getTop() + ((viewHolder.itemView.getBottom() - viewHolder.itemView.getTop()) / 2 - halfIcon); + imgLeft = viewHolder.itemView.getRight() - iconHorizontalMargin - halfIcon * 2; + icon.setBounds(imgLeft, top, viewHolder.itemView.getRight() - iconHorizontalMargin, top + icon.getIntrinsicHeight()); + if (swipeLeftActionIconTint != null) + icon.setColorFilter(swipeLeftActionIconTint, PorterDuff.Mode.SRC_IN); + icon.draw(canvas); + } + } + + if ( mSwipeLeftText != null && mSwipeLeftText.length() > 0 && dX < - iconHorizontalMargin - iconSize ) { + TextPaint textPaint = new TextPaint(); + textPaint.setAntiAlias(true); + textPaint.setTextSize(TypedValue.applyDimension(mSwipeLeftTextUnit, mSwipeLeftTextSize, recyclerView.getContext().getResources().getDisplayMetrics())); + textPaint.setColor(mSwipeLeftTextColor); + textPaint.setTypeface(mSwipeLeftTypeface); + + float width = textPaint.measureText(mSwipeLeftText); + int textTop = (int) (viewHolder.itemView.getTop() + ((viewHolder.itemView.getBottom() - viewHolder.itemView.getTop()) / 2.0) + textPaint.getTextSize() / 2); + canvas.drawText(mSwipeLeftText, imgLeft - width - ( imgLeft == viewHolder.itemView.getRight() ? iconHorizontalMargin : iconHorizontalMargin/2 ), textTop, textPaint); + } + } + } catch(Exception e) { + Log.e(this.getClass().getName(), e.getMessage()); + } + } + + /** + * A Builder for the RecyclerViewSwipeDecorator class + */ + public static class Builder { + private RecyclerViewSwipeDecorator mDecorator; + + /** + * Create a builder for a RecyclerViewsSwipeDecorator + * @param context A valid Context object for the RecyclerView + * @param canvas The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either ACTION_STATE_DRAG or ACTION_STATE_SWIPE. + * @param isCurrentlyActive True if this view is currently being controlled by the user or false it is simply animating back to its original state + * + * @deprecated in RecyclerViewSwipeDecorator 1.2.2 + */ + @Deprecated + public Builder(Context context, Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + this(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + + /** + * Create a builder for a RecyclerViewsSwipeDecorator + * @param canvas The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either ACTION_STATE_DRAG or ACTION_STATE_SWIPE. + * @param isCurrentlyActive True if this view is currently being controlled by the user or false it is simply animating back to its original state + */ + public Builder(Canvas canvas, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + mDecorator = new RecyclerViewSwipeDecorator( + canvas, + recyclerView, + viewHolder, + dX, + dY, + actionState, + isCurrentlyActive + ); + } + + /** + * Add a background color to both swiping directions + * @param color A single color value in the form 0xAARRGGBB + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addBackgroundColor(int color) { + mDecorator.setBackgroundColor(color); + return this; + } + + /** + * Add an action icon to both swiping directions + * @param drawableId The resource id of the icon to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addActionIcon(int drawableId) { + mDecorator.setActionIconId(drawableId); + return this; + } + + /** + * Set the tint color for either (left/right) action icons + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setActionIconTint(int color) { + mDecorator.setActionIconTint(color); + return this; + } + + /** + * Add a background color while swiping right + * @param color A single color value in the form 0xAARRGGBB + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeRightBackgroundColor(int color) { + mDecorator.setSwipeRightBackgroundColor(color); + return this; + } + + /** + * Add an action icon while swiping right + * @param drawableId The resource id of the icon to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeRightActionIcon(int drawableId) { + mDecorator.setSwipeRightActionIconId(drawableId); + return this; + } + + /** + * Set the tint color for action icon shown while swiping right + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeRightActionIconTint(int color) { + mDecorator.setSwipeRightActionIconTint(color); + return this; + } + + /** + * Add a label to be shown while swiping right + * @param label The string to be shown as label + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeRightLabel(String label) { + mDecorator.setSwipeRightLabel(label); + return this; + } + + /** + * Set the color of the label to be shown while swiping right + * @param color the color to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeRightLabelColor(int color) { + mDecorator.setSwipeRightTextColor(color); + return this; + } + + /** + * Set the size of the label to be shown while swiping right + * @param unit the unit to convert from + * @param size the size to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeRightLabelTextSize(int unit, float size) { + mDecorator.setSwipeRightTextSize(unit, size); + return this; + } + + /** + * Set the Typeface of the label to be shown while swiping right + * @param typeface the Typeface to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeRightLabelTypeface(Typeface typeface) { + mDecorator.setSwipeRightTypeface(typeface); + return this; + } + + /** + * Adds a background color while swiping left + * @param color A single color value in the form 0xAARRGGBB + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeLeftBackgroundColor(int color) { + mDecorator.setSwipeLeftBackgroundColor(color); + return this; + } + + /** + * Add an action icon while swiping left + * @param drawableId The resource id of the icon to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeLeftActionIcon(int drawableId) { + mDecorator.setSwipeLeftActionIconId(drawableId); + return this; + } + + /** + * Set the tint color for action icon shown while swiping left + * @param color a color in ARGB format (e.g. 0xFF0000FF for blue) + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeLeftActionIconTint(int color) { + mDecorator.setSwipeLeftActionIconTint(color); + return this; + } + + /** + * Add a label to be shown while swiping left + * @param label The string to be shown as label + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder addSwipeLeftLabel(String label) { + mDecorator.setSwipeLeftLabel(label); + return this; + } + + /** + * Set the color of the label to be shown while swiping left + * @param color the color to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeLeftLabelColor(int color) { + mDecorator.setSwipeLeftTextColor(color); + return this; + } + + /** + * Set the size of the label to be shown while swiping left + * @param unit the unit to convert from + * @param size the size to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeLeftLabelTextSize(int unit, float size) { + mDecorator.setSwipeLeftTextSize(unit, size); + return this; + } + + /** + * Set the Typeface of the label to be shown while swiping left + * @param typeface the Typeface to be set + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setSwipeLeftLabelTypeface(Typeface typeface) { + mDecorator.setSwipeLeftTypeface(typeface); + return this; + } + + /** + * Set the horizontal margin of the icon in DPs (default is 16dp) + * @param pixels margin in pixels + * @return This instance of @RecyclerViewSwipeDecorator.Builder + * + * @deprecated in RecyclerViewSwipeDecorator 1.2, use {@link #setIconHorizontalMargin(int, int)} instead. + */ + @Deprecated + public Builder setIconHorizontalMargin(int pixels) { + mDecorator.setIconHorizontalMargin(pixels); + return this; + } + + /** + * Set the horizontal margin of the icon in the given unit (default is 16dp) + * @param unit TypedValue + * @param iconHorizontalMargin the margin in the given unit + * + * @return This instance of @RecyclerViewSwipeDecorator.Builder + */ + public Builder setIconHorizontalMargin(int unit, int iconHorizontalMargin) { + mDecorator.setIconHorizontalMargin(unit, iconHorizontalMargin); + return this; + } + + /** + * Create a RecyclerViewSwipeDecorator + * @return The created @RecyclerViewSwipeDecorator + */ + public RecyclerViewSwipeDecorator create() { + return mDecorator; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransactionHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransactionHolder.java index 4ce31f70f7..1fa0bfcbef 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransactionHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TransactionHolder.java @@ -16,6 +16,7 @@ import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionMeta; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; @@ -29,6 +30,8 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; +import timber.log.Timber; + public class TransactionHolder extends BinderViewHolder implements View.OnClickListener { public static final int VIEW_TYPE = 1003; @@ -47,12 +50,13 @@ public class TransactionHolder extends BinderViewHolder impleme private final LinearLayout transactionBackground; private final FetchTransactionsInteract transactionsInteract; private final AssetDefinitionService assetService; + private final AddressBookInteract addressBookInteract; private Transaction transaction; private String defaultAddress; private boolean fromTokenView; - public TransactionHolder(ViewGroup parent, TokensService service, FetchTransactionsInteract interact, AssetDefinitionService svs) + public TransactionHolder(ViewGroup parent, TokensService service, FetchTransactionsInteract interact, AssetDefinitionService svs, AddressBookInteract addressBookInteract) { super(R.layout.item_transaction, parent); date = findViewById(R.id.text_tx_time); @@ -66,6 +70,7 @@ public TransactionHolder(ViewGroup parent, TokensService service, FetchTransacti transactionsInteract = interact; assetService = svs; itemView.setOnClickListener(this); + this.addressBookInteract = addressBookInteract; } @Override @@ -117,6 +122,14 @@ private void setupTransactionDetail(Token token) { String detailStr = token.getTransactionDetail(getContext(), transaction, tokensService); address.setText(detailStr); + Timber.d(detailStr); + String walletAddress = detailStr.split(" ")[1]; + // set name from address book + addressBookInteract.searchContactAsync(walletAddress, (contact) -> { + if (contact != null) { + address.setText(contact.getName()); // wont work as address is formatted with ... in middle + } + }); } @Override diff --git a/app/src/main/java/com/alphawallet/app/util/ItemCallback.java b/app/src/main/java/com/alphawallet/app/util/ItemCallback.java new file mode 100644 index 0000000000..736db766ca --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/util/ItemCallback.java @@ -0,0 +1,6 @@ +package com.alphawallet.app.util; + +/** Simple callback functional interface for any object*/ +public interface ItemCallback { + public void call(T object); +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java index 677ae56937..464ae25f68 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/ActivityViewModel.java @@ -7,6 +7,7 @@ import com.alphawallet.app.entity.ActivityMeta; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.service.AssetDefinitionService; @@ -42,6 +43,7 @@ public class ActivityViewModel extends BaseViewModel private final TokensService tokensService; private final TransactionsService transactionsService; private final RealmManager realmManager; + private final AddressBookInteract addressBookInteract; @Nullable private Disposable queryUnknownTokensDisposable; @@ -58,13 +60,15 @@ public LiveData defaultWallet() { AssetDefinitionService assetDefinitionService, TokensService tokensService, TransactionsService transactionsService, - RealmManager realmManager) { + RealmManager realmManager, + AddressBookInteract addressBookInteract) { this.genericWalletInteract = genericWalletInteract; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.tokensService = tokensService; this.transactionsService = transactionsService; this.realmManager = realmManager; + this.addressBookInteract = addressBookInteract; } public void prepare() @@ -137,4 +141,9 @@ public AssetDefinitionService getAssetDefinitionService() { return assetDefinitionService; } + + public AddressBookInteract getAddressBookInteract() + { + return addressBookInteract; + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/AddEditAddressViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/AddEditAddressViewModel.java new file mode 100644 index 0000000000..e2d7de3ceb --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/AddEditAddressViewModel.java @@ -0,0 +1,147 @@ +package com.alphawallet.app.viewmodel; + +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.interact.AddressBookInteract; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import java8.util.Optional; +import timber.log.Timber; + +@HiltViewModel +public class AddEditAddressViewModel extends BaseViewModel { + private static final String TAG = "AddAddressViewModel"; + + private final AddressBookInteract addressBookInteract; + + public MutableLiveData> address = new MutableLiveData<>(); + public MutableLiveData addressError = new MutableLiveData<>(); + public MutableLiveData> contactName = new MutableLiveData<>(); + public MutableLiveData contactNameError = new MutableLiveData<>(); + public MutableLiveData saveContact = new MutableLiveData<>(); + public MutableLiveData updateContact = new MutableLiveData<>(); + public MutableLiveData error = new MutableLiveData<>(); + + private final List contactsList = new ArrayList<>(); + private AddressBookContact addressBookContactToSave = new AddressBookContact("", "", ""); + + @Inject + public AddEditAddressViewModel(AddressBookInteract addressBookInteract) { + this.addressBookInteract = addressBookInteract; + loadContacts(); + } + + // load contacts in list + public void loadContacts() { + addressBookInteract.getAllContacts() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( (list) -> { + Timber.d("loadContacts: list: %s", list); + contactsList.clear(); + contactsList.addAll(list); + }, (throwable -> Timber.e(throwable, "loadContacts: error: "))) + .isDisposed(); + } + + public void onClickSave(String walletAddress, String name, String ensName) { + addressBookContactToSave.setWalletAddress(walletAddress); + addressBookContactToSave.setEthName(ensName); + addressBookContactToSave.setName(name); + checkAddress(walletAddress); + } + + public void updateContact(AddressBookContact oldContact, AddressBookContact newContact) { + if (oldContact.getName().equalsIgnoreCase(newContact.getName())) + return; + // search by address + // if found, update the name + // else something went wrong. + Timber.d("New name: %s", newContact.getName()); + Optional matchedContactOpt = findByAddress(oldContact.getWalletAddress()); + if (matchedContactOpt.isPresent()) { + Optional matchedByName = findByName(newContact.getName()); + if (matchedByName.isPresent()) { + contactName.postValue(matchedByName); + return; + } + addressBookInteract.updateContact(oldContact, newContact) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( () -> updateContact.postValue(true), (throwable -> error.postValue(new RuntimeException("Failed to update contact")))) + .isDisposed(); + } else { + // error + error.postValue(new RuntimeException("No match found")); + updateContact.postValue(false); + } + } + + /** + * @param walletAddress wallet address to match with existing contacts. + * Finds address and posts result to addressError Live Data + */ + public void checkAddress(String walletAddress) { + addressBookContactToSave.setWalletAddress(walletAddress); // required when called from editorAction done + Single.fromCallable( () -> findByAddress(walletAddress)).subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(address::postValue, (throwable -> Timber.e(throwable, "checkAddress: "))) + .isDisposed(); + } + + public void checkName(String name) { + addressBookContactToSave.setName(name); // required when called from editorAction done + Single.fromCallable( () -> findByName(name)).subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( (contactName::postValue) , (throwable -> Timber.e(throwable, "checkName: "))) + .isDisposed(); + } + + private Optional findByAddress(String walletAddress) { + AddressBookContact matchedContact = null; + for (AddressBookContact addressBookContact : contactsList) { + if (addressBookContact.getWalletAddress().equalsIgnoreCase(walletAddress)) { + matchedContact = addressBookContact; + break; + } + } + if (matchedContact == null) { + return Optional.empty(); + } else { + return Optional.of(matchedContact); + } + } + + private Optional findByName(String name) { + AddressBookContact matchedContact = null; + for (AddressBookContact addressBookContact : contactsList) { + if (addressBookContact.getName().equalsIgnoreCase(name)) { + matchedContact = addressBookContact; + break; + } + } + if (matchedContact == null) { + return Optional.empty(); + } else { + return Optional.of(matchedContact); + } + } + + public void addContact() { + + addressBookInteract.addContact(addressBookContactToSave) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( () -> saveContact.postValue(true), (throwable -> saveContact.postValue(false)) ) + .isDisposed(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/AddressBookViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/AddressBookViewModel.java new file mode 100644 index 0000000000..4196842043 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/viewmodel/AddressBookViewModel.java @@ -0,0 +1,100 @@ +package com.alphawallet.app.viewmodel; + +import androidx.lifecycle.MutableLiveData; + +import com.alphawallet.app.entity.NetworkInfo; +import com.alphawallet.app.entity.AddressBookContact; +import com.alphawallet.app.interact.AddressBookInteract; +import com.alphawallet.app.interact.GenericWalletInteract; +import com.alphawallet.app.service.RealmManager; +import com.alphawallet.app.service.TokensService; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +@HiltViewModel +public class AddressBookViewModel extends BaseViewModel { + private static final String TAG = "AddressBookViewModel"; + + private final RealmManager realmManager; + private final TokensService tokensService; + private final AddressBookInteract addressBookInteract; + private final GenericWalletInteract genericWalletInteract; + + public MutableLiveData> contactsLD = new MutableLiveData<>(); + public MutableLiveData> filteredContacts = new MutableLiveData<>(); + private final MutableLiveData defaultNetwork = new MutableLiveData<>(); + + private List contactsList; + + @Inject + AddressBookViewModel(TokensService tokensService, + RealmManager realmManager, + AddressBookInteract addressBookInteract, + GenericWalletInteract genericWalletInteract + ) { + this.tokensService = tokensService; + this.realmManager = realmManager; + this.addressBookInteract = addressBookInteract; + this.genericWalletInteract = genericWalletInteract; + loadContacts(); + } + + public void loadContacts() { + addressBookInteract.getAllContacts() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::postContacts, this::onError) + .isDisposed(); + } + + public void addContact(AddressBookContact addressBookContact) { + addressBookInteract.addContact(addressBookContact) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onComplete, this::onError) + .isDisposed(); + } + + public void removeContact(AddressBookContact addressBookContact) { + addressBookInteract.removeContact(addressBookContact) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onComplete, this::onError) + .isDisposed(); + } + + private void onComplete() { + loadContacts(); + } + + private void postContacts(List contacts) { + this.contactsList = contacts; + contactsLD.postValue(contacts); + } + + @Override + protected void onError(Throwable throwable) { + super.onError(throwable); + Timber.d("onError: %s", throwable.getMessage()); + } + + public void filterByName(String nameText) { + String nameToSearch = nameText.toLowerCase(); + ArrayList filtered = new ArrayList<>(); + for (AddressBookContact u : contactsList) { + if (u.getName().toLowerCase().startsWith(nameToSearch)) { + filtered.add(u); + } + } + // post + filteredContacts.postValue(filtered); + } +} diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java index 0a38dcd149..4300784d14 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java @@ -13,6 +13,7 @@ import com.alphawallet.app.entity.OnRampContract; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.repository.OnRampRepository; import com.alphawallet.app.repository.OnRampRepositoryType; @@ -41,19 +42,22 @@ public class Erc20DetailViewModel extends BaseViewModel { private final AssetDefinitionService assetDefinitionService; private final TokensService tokensService; private final OnRampRepositoryType onRampRepository; + private final AddressBookInteract addressBookInteract; @Inject public Erc20DetailViewModel(MyAddressRouter myAddressRouter, FetchTransactionsInteract fetchTransactionsInteract, AssetDefinitionService assetDefinitionService, TokensService tokensService, - OnRampRepositoryType onRampRepository) + OnRampRepositoryType onRampRepository, + AddressBookInteract addressBookInteract) { this.myAddressRouter = myAddressRouter; this.fetchTransactionsInteract = fetchTransactionsInteract; this.assetDefinitionService = assetDefinitionService; this.tokensService = tokensService; this.onRampRepository = onRampRepository; + this.addressBookInteract = addressBookInteract; } public LiveData sig() @@ -91,6 +95,10 @@ public AssetDefinitionService getAssetDefinitionService() return this.assetDefinitionService; } + public AddressBookInteract getAddressBookInteract() { + return addressBookInteract; + } + public void showSendToken(Activity act, Wallet wallet, Token token) { if (token != null) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenActivityViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenActivityViewModel.java index 52f3087f1d..9d6ae202eb 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenActivityViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenActivityViewModel.java @@ -4,6 +4,7 @@ import com.alphawallet.app.entity.ActivityMeta; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; @@ -20,15 +21,18 @@ public class TokenActivityViewModel extends BaseViewModel { private final AssetDefinitionService assetDefinitionService; private final TokensService tokensService; private final FetchTransactionsInteract fetchTransactionsInteract; + private final AddressBookInteract addressBookInteract; @Inject public TokenActivityViewModel(AssetDefinitionService assetDefinitionService, FetchTransactionsInteract fetchTransactionsInteract, - TokensService tokensService) + TokensService tokensService, + AddressBookInteract addressBookInteract) { this.assetDefinitionService = assetDefinitionService; this.fetchTransactionsInteract = fetchTransactionsInteract; this.tokensService = tokensService; + this.addressBookInteract = addressBookInteract; } public TokensService getTokensService() @@ -46,6 +50,10 @@ public AssetDefinitionService getAssetDefinitionService() return this.assetDefinitionService; } + public AddressBookInteract getAddressBookInteract() { + return this.addressBookInteract; + } + public Realm getRealmInstance(Wallet wallet) { return tokensService.getRealmInstance(wallet); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java index 1cda35d059..2856c63ce1 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java @@ -21,6 +21,7 @@ import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.interact.GenericWalletInteract; @@ -95,6 +96,7 @@ public class TokenFunctionViewModel extends BaseViewModel private final GenericWalletInteract genericWalletInteract; private final OpenSeaService openseaService; private final FetchTransactionsInteract fetchTransactionsInteract; + private final AddressBookInteract addressBookInteract; private final AnalyticsServiceType analyticsService; private Wallet wallet; @@ -121,6 +123,7 @@ public class TokenFunctionViewModel extends BaseViewModel GenericWalletInteract genericWalletInteract, OpenSeaService openseaService, FetchTransactionsInteract fetchTransactionsInteract, + AddressBookInteract addressBookInteract, AnalyticsServiceType analyticsService) { this.assetDefinitionService = assetDefinitionService; @@ -132,6 +135,7 @@ public class TokenFunctionViewModel extends BaseViewModel this.genericWalletInteract = genericWalletInteract; this.openseaService = openseaService; this.fetchTransactionsInteract = fetchTransactionsInteract; + this.addressBookInteract = addressBookInteract; this.analyticsService = analyticsService; } @@ -139,6 +143,10 @@ public AssetDefinitionService getAssetDefinitionService() { return assetDefinitionService; } + public AddressBookInteract getAddressBookInteract() { + return addressBookInteract; + } + public LiveData insufficientFunds() { return insufficientFunds; } public LiveData invalidAddress() { return invalidAddress; } public LiveData sig() { return sig; } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java index 555e824fda..801486d616 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionDetailViewModel.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import com.alphawallet.app.C; import com.alphawallet.app.R; @@ -17,9 +18,11 @@ import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionData; +import com.alphawallet.app.entity.AddressBookContact; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokenscript.EventUtils; +import com.alphawallet.app.interact.AddressBookInteract; import com.alphawallet.app.interact.CreateTransactionInteract; import com.alphawallet.app.interact.FetchTransactionsInteract; import com.alphawallet.app.interact.FindDefaultNetworkInteract; @@ -41,6 +44,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.util.List; import java.util.concurrent.TimeUnit; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -49,6 +53,7 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; +import timber.log.Timber; import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; @@ -62,6 +67,7 @@ public class TransactionDetailViewModel extends BaseViewModel { private final TokenRepositoryType tokenRepository; private final FetchTransactionsInteract fetchTransactionsInteract; private final CreateTransactionInteract createTransactionInteract; + private final AddressBookInteract addressBookInteract; private final KeyService keyService; private final TokensService tokenService; @@ -73,13 +79,20 @@ public class TransactionDetailViewModel extends BaseViewModel { private final MutableLiveData transaction = new MutableLiveData<>(); private final MutableLiveData transactionFinalised = new MutableLiveData<>(); private final MutableLiveData transactionError = new MutableLiveData<>(); + private final MutableLiveData senderAddressContact = new MutableLiveData<>(); + private final MutableLiveData receiverAddressContact = new MutableLiveData<>(); + private final MutableLiveData> addressBook = new MutableLiveData<>(); public LiveData latestBlock() { return latestBlock; } public LiveData latestTx() { return latestTx; } public LiveData onTransaction() { return transaction; } + public LiveData senderAddressContact() { return senderAddressContact; } + public LiveData receiverAddressContact() { return receiverAddressContact; } private String walletAddress; + private Observer> senderAddressContactObserver; + private Observer> receiverAddressContactObserver; @Nullable private Disposable pendingUpdateDisposable; @@ -97,6 +110,7 @@ public LiveData latestBlock() { KeyService keyService, GasService gasService, CreateTransactionInteract createTransactionInteract, + AddressBookInteract addressBookInteract, AnalyticsServiceType analyticsService) { this.networkInteract = findDefaultNetworkInteract; @@ -107,7 +121,10 @@ public LiveData latestBlock() { this.keyService = keyService; this.gasService = gasService; this.createTransactionInteract = createTransactionInteract; + this.addressBookInteract = addressBookInteract; this.analyticsService = analyticsService; + + loadAddressBook(); } public MutableLiveData transactionFinalised() @@ -319,4 +336,58 @@ public void actionSheetConfirm(String mode) analyticsService.track(C.AN_CALL_ACTIONSHEET, analyticsProperties); } + + public void loadAddressBook() { + Timber.d("loadAddressBook: "); + addressBookInteract.getAllContacts() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onLoadAddressBook, (throwable -> Timber.e(throwable, "Failed to load address book"))) + .isDisposed(); + } + + private void onLoadAddressBook(List addressBook) { + Timber.d("onLoadAddressBook: "); + this.addressBook.postValue(addressBook); + } + + public void resolveSenderAddress(String walletAddress) { + // observer to react on loading address book + senderAddressContactObserver = new Observer>() { + @Override + public void onChanged(List addressBookContacts) { + for (AddressBookContact addressBookContact : addressBookContacts) { + if (addressBookContact.getWalletAddress().equalsIgnoreCase(walletAddress)) { + senderAddressContact.postValue(addressBookContact); + break; + } + } + } + }; + this.addressBook.observeForever(senderAddressContactObserver); + + } + + public void resolveReceiverAddress(String walletAddress) { + // observer to react on loading address book + receiverAddressContactObserver = new Observer>() { + @Override + public void onChanged(List addressBookContacts) { + for (AddressBookContact addressBookContact : addressBookContacts) { + if (addressBookContact.getWalletAddress().equalsIgnoreCase(walletAddress)) { + receiverAddressContact.postValue(addressBookContact); + break; + } + } + } + }; + this.addressBook.observeForever(receiverAddressContactObserver); + } + + @Override + protected void onCleared() { + addressBook.removeObserver(senderAddressContactObserver); + addressBook.removeObserver(receiverAddressContactObserver); + super.onCleared(); + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java b/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java index 3697e7cabb..713c8d2dbf 100644 --- a/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java +++ b/app/src/main/java/com/alphawallet/app/widget/CopyTextView.java @@ -12,7 +12,9 @@ import android.widget.TextView; import android.widget.Toast; +import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.router.AddEditAddressRouter; import org.web3j.crypto.WalletUtils; @@ -24,6 +26,7 @@ public class CopyTextView extends LinearLayout { private ImageView copy; private TextView text; private LinearLayout layout; + private ImageView addressBook; private int textResId; private int textColor; @@ -33,6 +36,7 @@ public class CopyTextView extends LinearLayout { private boolean removePadding; private String rawAddress; private float marginRight; + private boolean showAddressBook = false; public CopyTextView(Context context, AttributeSet attrs) { super(context, attrs); @@ -60,6 +64,7 @@ private void getAttrs(Context context, AttributeSet attrs) { boldFont = a.getBoolean(R.styleable.CopyTextView_bold, false); removePadding = a.getBoolean(R.styleable.CopyTextView_removePadding, false); marginRight = a.getDimension(R.styleable.CopyTextView_marginRight, 0.0f); + showAddressBook = a.getBoolean(R.styleable.CopyTextView_showAddressBook, false); } finally { a.recycle(); } @@ -72,6 +77,7 @@ private void bindViews() { text.setText(textResId); text.setTextColor(textColor); text.setGravity(gravity); + addressBook = findViewById(R.id.img_address_book); LayoutParams layoutParams = (LayoutParams) text.getLayoutParams(); layoutParams.rightMargin = (int) marginRight; @@ -89,6 +95,18 @@ private void bindViews() { layout.setOnClickListener(v -> copyToClipboard()); copy.setOnClickListener(v -> copyToClipboard()); + + if (showAddressBook) { + copy.setVisibility(GONE); + addressBook.setVisibility(VISIBLE); + addressBook.setOnClickListener( v -> { + new AddEditAddressRouter().open( + getContext(), + C.ADD_ADDRESS_REQUEST_CODE, + text.getText().toString() + ); + }); + } } public String getText() { @@ -116,4 +134,10 @@ private void copyToClipboard() if(showToast) Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); } + + public void setAddressName(String name) { + text.setText(name); + addressBook.setVisibility(GONE); + copy.setVisibility(VISIBLE); + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/InputAddress.java b/app/src/main/java/com/alphawallet/app/widget/InputAddress.java index 8d258297eb..19b07ad67c 100644 --- a/app/src/main/java/com/alphawallet/app/widget/InputAddress.java +++ b/app/src/main/java/com/alphawallet/app/widget/InputAddress.java @@ -14,20 +14,20 @@ import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.AutoCompleteTextView; import android.widget.ImageButton; -import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.ENSCallback; +import com.alphawallet.app.entity.AddressBookContact; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.router.AddressBookRouter; import com.alphawallet.app.ui.QRScanning.QRScanner; import com.alphawallet.app.ui.widget.adapter.AutoCompleteAddressAdapter; import com.alphawallet.app.ui.widget.entity.AddressReadyCallback; @@ -47,6 +47,7 @@ */ public class InputAddress extends RelativeLayout implements ItemClickListener, ENSCallback, TextWatcher { + private final View layout; private final AutoCompleteTextView editText; private final TextView labelText; private final TextView pasteItem; @@ -69,11 +70,13 @@ public class InputAddress extends RelativeLayout implements ItemClickListener, E private long chainOverride; private final Pattern findAddress = Pattern.compile("^(\\s?)+(0x)([0-9a-fA-F]{40})(\\s?)+\\z"); private final float standardTextSize; + private boolean showAddressBook = false; + private TextView addressBook; public InputAddress(Context context, AttributeSet attrs) { super(context, attrs); - inflate(context, R.layout.item_input_address, this); + layout = inflate(context, R.layout.item_input_address, this); getAttrs(context, attrs); this.context = context; @@ -140,6 +143,12 @@ private void getAttrs(Context context, AttributeSet attrs) findViewById(R.id.layout_header).setVisibility(showHeader ? View.VISIBLE : View.GONE); TextView headerText = findViewById(R.id.text_header); headerText.setText(headerTextId); + showAddressBook = a.getBoolean(R.styleable.InputView_showAddressBook, false); + addressBook = findViewById(R.id.text_address_book); + addressBook.setVisibility(showAddressBook ? View.VISIBLE : View.GONE); + addressBook.setOnClickListener(v -> { + new AddressBookRouter().openForContactSelection(getContext(), C.ADDRESS_BOOK_CONTACT_REQUEST_CODE); + }); } finally { @@ -518,7 +527,32 @@ public void afterTextChanged(Editable s) setStatus(null); if (ensHandler != null && !TextUtils.isEmpty(getInputText())) { + Timber.d("ensHandler.checkAddress: "); ensHandler.checkAddress(); } } + + public void onContactSelected(AddressBookContact addressBookContact) { + if (!addressBookContact.getWalletAddress().isEmpty()) { + fullAddress = addressBookContact.getWalletAddress(); + if (!addressBookContact.getEthName().isEmpty()) { + setAddress(addressBookContact.getEthName() + " | " + addressBookContact.getWalletAddress()); + } else { + setAddress(addressBookContact.getWalletAddress()); + } + } + } + + /** Use to enable/disable ens resolver*/ + public void setHandleENS(boolean handleENS) { + this.handleENS = handleENS; + } + + public void setEditable(boolean enabled) { + layout.setClickable(enabled); + editText.setEnabled(enabled); + scanQrIcon.setVisibility(enabled ? VISIBLE : GONE); + pasteItem.setVisibility(enabled ? VISIBLE : GONE); + addressBook.setVisibility( (enabled && showAddressBook) ? VISIBLE : GONE); + } } diff --git a/app/src/main/java/com/alphawallet/app/widget/InputView.java b/app/src/main/java/com/alphawallet/app/widget/InputView.java index e97901fa73..1bcfea5acd 100644 --- a/app/src/main/java/com/alphawallet/app/widget/InputView.java +++ b/app/src/main/java/com/alphawallet/app/widget/InputView.java @@ -184,6 +184,7 @@ public void setError(int resId) { errorText.setText(resId); errorText.setVisibility(View.VISIBLE); } + setBoxColour(BoxStatus.ERROR); } public void setError(CharSequence message) { @@ -196,6 +197,7 @@ public void setError(CharSequence message) { errorText.setText(message); errorText.setVisibility(View.VISIBLE); } + setBoxColour(BoxStatus.ERROR); } public void setStatus(CharSequence statusTxt) @@ -236,4 +238,17 @@ private void setBoxColour(BoxStatus status) break; } } + + /** + * Enables highlighting input box on selection & error. + */ + public void enableFocusListener() { + editText.setOnFocusChangeListener( (v, hasFocus) -> { + if (hasFocus) { + setBoxColour(BoxStatus.SELECTED); + } else { + setBoxColour(BoxStatus.UNSELECTED); + } + }); + } } diff --git a/app/src/main/res/drawable-anydpi/ic_address_book.xml b/app/src/main/res/drawable-anydpi/ic_address_book.xml new file mode 100644 index 0000000000..826e843576 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_address_book.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable-anydpi/illustration_empty_2.xml b/app/src/main/res/drawable-anydpi/illustration_empty_2.xml new file mode 100644 index 0000000000..c4d7145df6 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/illustration_empty_2.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add_to_address_book.xml b/app/src/main/res/drawable/ic_add_to_address_book.xml new file mode 100644 index 0000000000..bd610cea55 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_to_address_book.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete_contact.xml b/app/src/main/res/drawable/ic_delete_contact.xml new file mode 100644 index 0000000000..b10dee49bd --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_contact.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_add_address.xml b/app/src/main/res/layout/activity_add_address.xml new file mode 100644 index 0000000000..0f65c87c75 --- /dev/null +++ b/app/src/main/res/layout/activity_add_address.xml @@ -0,0 +1,57 @@ + + + + + + + + + +