├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── logo.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── logo.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── logo.png
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── cards.png
│ │ │ │ ├── ic_user.png
│ │ │ │ └── ic_exchange.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── cards.png
│ │ │ │ ├── ic_user.png
│ │ │ │ └── ic_exchange.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── logo.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── logo.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── cards.png
│ │ │ │ ├── ic_user.png
│ │ │ │ └── ic_exchange.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── cards.png
│ │ │ │ ├── ic_user.png
│ │ │ │ └── ic_exchange.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── cards.png
│ │ │ │ ├── ic_user.png
│ │ │ │ └── ic_exchange.png
│ │ │ ├── values
│ │ │ │ ├── integers.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── xml
│ │ │ │ └── file_paths.xml
│ │ │ ├── drawable
│ │ │ │ ├── avatar_placeholder.xml
│ │ │ │ ├── list_item_background.xml
│ │ │ │ ├── ic_done.xml
│ │ │ │ ├── ic_chevron_right.xml
│ │ │ │ ├── ic_back_right.xml
│ │ │ │ ├── ic_back_left.xml
│ │ │ │ ├── ic_close.xml
│ │ │ │ ├── ic_edit.xml
│ │ │ │ ├── ic_search.xml
│ │ │ │ ├── avatar_edit.xml
│ │ │ │ └── ic_avatar.xml
│ │ │ ├── transition
│ │ │ │ ├── slide_end.xml
│ │ │ │ └── slide_start.xml
│ │ │ ├── layout
│ │ │ │ ├── card_detail_dialog.xml
│ │ │ │ ├── toolbar.xml
│ │ │ │ ├── item_card_field.xml
│ │ │ │ ├── card_list_loader.xml
│ │ │ │ ├── activity_crop.xml
│ │ │ │ ├── toolbar_search.xml
│ │ │ │ ├── activity_user.xml
│ │ │ │ ├── view_toolbar.xml
│ │ │ │ ├── item_card.xml
│ │ │ │ ├── activity_welcome.xml
│ │ │ │ ├── activity_home.xml
│ │ │ │ ├── view_card_info.xml
│ │ │ │ └── activity_exchange.xml
│ │ │ └── values-pt
│ │ │ │ └── strings.xml
│ │ ├── assets
│ │ │ └── fonts
│ │ │ │ ├── Lato-Bold.ttf
│ │ │ │ └── Lato-Regular.ttf
│ │ ├── java
│ │ │ └── io
│ │ │ │ └── bloco
│ │ │ │ └── cardcase
│ │ │ │ ├── presentation
│ │ │ │ ├── exchange
│ │ │ │ │ ├── CardWrapper.java
│ │ │ │ │ ├── ExchangeContract.java
│ │ │ │ │ ├── NearbyManager.java
│ │ │ │ │ ├── CardSerializer.java
│ │ │ │ │ ├── ExchangePresenter.java
│ │ │ │ │ └── ExchangeActivity.java
│ │ │ │ ├── home
│ │ │ │ │ ├── SimpleTextWatcher.java
│ │ │ │ │ ├── HomeContract.java
│ │ │ │ │ ├── CardDetailDialog.java
│ │ │ │ │ ├── HomePresenter.java
│ │ │ │ │ └── HomeActivity.java
│ │ │ │ ├── common
│ │ │ │ │ ├── ImageLoader.java
│ │ │ │ │ ├── DateTimeFormat.java
│ │ │ │ │ ├── ErrorDisplayer.java
│ │ │ │ │ ├── CardViewHolder.java
│ │ │ │ │ ├── CircleTransform.java
│ │ │ │ │ ├── Toolbar.java
│ │ │ │ │ ├── SearchToolbar.java
│ │ │ │ │ ├── CardAdapter.java
│ │ │ │ │ ├── Bootstrap.java
│ │ │ │ │ ├── FileHelper.java
│ │ │ │ │ └── CardInfoView.java
│ │ │ │ ├── user
│ │ │ │ │ ├── UserContract.java
│ │ │ │ │ ├── UserPresenter.java
│ │ │ │ │ ├── CropAvatarActivity.java
│ │ │ │ │ ├── AvatarPicker.java
│ │ │ │ │ └── UserActivity.java
│ │ │ │ ├── BaseActivity.java
│ │ │ │ └── welcome
│ │ │ │ │ └── WelcomeActivity.java
│ │ │ │ ├── common
│ │ │ │ ├── di
│ │ │ │ │ ├── PerActivity.java
│ │ │ │ │ ├── ActivityComponent.java
│ │ │ │ │ ├── ActivityModule.java
│ │ │ │ │ ├── ApplicationModule.java
│ │ │ │ │ └── ApplicationComponent.java
│ │ │ │ ├── analytics
│ │ │ │ │ ├── AnalyticsTracker.java
│ │ │ │ │ ├── AnswersTracker.java
│ │ │ │ │ ├── AnalyticsService.java
│ │ │ │ │ └── GoogleAnalyticsTracker.java
│ │ │ │ └── Preconditions.java
│ │ │ │ ├── domain
│ │ │ │ ├── GetUserCard.java
│ │ │ │ ├── SaveUserCard.java
│ │ │ │ ├── GetReceivedCards.java
│ │ │ │ └── SaveReceivedCards.java
│ │ │ │ ├── data
│ │ │ │ ├── Database.java
│ │ │ │ ├── DatabaseHelper.java
│ │ │ │ └── models
│ │ │ │ │ └── Card.java
│ │ │ │ └── AndroidApplication.java
│ │ └── AndroidManifest.xml
│ ├── debug
│ │ └── assets
│ │ │ └── avatars
│ │ │ ├── avatar1.jpg
│ │ │ ├── avatar2.jpg
│ │ │ ├── avatar3.jpg
│ │ │ ├── avatar4.jpg
│ │ │ ├── avatar5.jpg
│ │ │ ├── avatar6.jpg
│ │ │ ├── avatar7.jpg
│ │ │ ├── avatar8.jpg
│ │ │ ├── avatar9.jpg
│ │ │ └── avatar10.jpg
│ ├── test
│ │ └── java
│ │ │ └── io
│ │ │ └── bloco
│ │ │ └── cardcase
│ │ │ └── domain
│ │ │ ├── SaveUserCardTest.java
│ │ │ ├── GetUserCardTest.java
│ │ │ ├── SaveReceivedCardsTest.java
│ │ │ ├── GetReceivedCardsTest.java
│ │ │ └── presenters
│ │ │ ├── ExchangePresenterTest.java
│ │ │ └── HomePresenterTest.java
│ └── androidTest
│ │ └── java
│ │ └── io
│ │ └── bloco
│ │ └── cardcase
│ │ ├── helpers
│ │ ├── AssertCurrentActivity.java
│ │ └── CardFactory.java
│ │ ├── presentation
│ │ ├── CardSerializerTest.java
│ │ ├── UserActivityOnOnboardingTest.java
│ │ ├── HomeActivityTest.java
│ │ └── UserActivityOnEditTest.java
│ │ └── data
│ │ └── DatabaseTest.java
├── release
│ └── output.json
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── .circleci
└── config.yml
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/mipmap-hdpi/logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/mipmap-mdpi/logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/mipmap-xhdpi/logo.png
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar1.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar2.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar3.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar4.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar5.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar6.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar7.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar8.jpg
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar9.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Lato-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/assets/fonts/Lato-Bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-hdpi/cards.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-mdpi/cards.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/mipmap-xxhdpi/logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/mipmap-xxxhdpi/logo.png
--------------------------------------------------------------------------------
/app/src/debug/assets/avatars/avatar10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/debug/assets/avatars/avatar10.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/fonts/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/assets/fonts/Lato-Regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-hdpi/ic_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-mdpi/ic_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xhdpi/cards.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xhdpi/ic_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxhdpi/cards.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxxhdpi/cards.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxhdpi/ic_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxxhdpi/ic_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_exchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-hdpi/ic_exchange.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_exchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-mdpi/ic_exchange.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_exchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xhdpi/ic_exchange.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_exchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxhdpi/ic_exchange.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_exchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocoio/cardcase/HEAD/app/src/main/res/drawable-xxxhdpi/ic_exchange.png
--------------------------------------------------------------------------------
/app/src/main/res/values/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 300
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/CardWrapper.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import io.bloco.cardcase.data.models.Card;
4 |
5 | class CardWrapper {
6 | Card card;
7 | String avatarData;
8 | }
--------------------------------------------------------------------------------
/app/release/output.json:
--------------------------------------------------------------------------------
1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":25,"versionName":"1.1.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
--------------------------------------------------------------------------------
/app/src/main/res/drawable/avatar_placeholder.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 08 11:21:20 BST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/transition/slide_end.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/transition/slide_start.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/di/PerActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.di;
2 |
3 | import java.lang.annotation.Retention;
4 | import javax.inject.Scope;
5 |
6 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
7 |
8 | @Scope @Retention(RUNTIME) public @interface PerActivity {
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/list_item_background.xml:
--------------------------------------------------------------------------------
1 |
4 | -
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/card_detail_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_done.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_chevron_right.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back_right.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back_left.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/analytics/AnalyticsTracker.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.analytics;
2 |
3 | import android.content.Context;
4 | import androidx.annotation.Nullable;
5 | import java.util.Map;
6 |
7 | @SuppressWarnings("WeakerAccess")
8 | public interface AnalyticsTracker {
9 | void init(Context context);
10 |
11 | void trackEvent(String event, @Nullable Map eventParams);
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_card_field.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/card_list_loader.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/avatar_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/home/SimpleTextWatcher.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.home;
2 |
3 | import android.text.Editable;
4 | import android.text.TextWatcher;
5 |
6 | public abstract class SimpleTextWatcher implements TextWatcher {
7 |
8 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
9 | }
10 |
11 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
12 | onTextChanged(s.toString(
13 |
14 | ));
15 | }
16 |
17 | @Override public void afterTextChanged(Editable s) {
18 | }
19 |
20 | protected abstract void onTextChanged(String newText);
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/domain/GetUserCard.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | @Singleton public class GetUserCard {
9 |
10 | private final Database database;
11 |
12 | public interface Callback {
13 | void onGetUserCard(Card userCard);
14 | }
15 |
16 | @Inject public GetUserCard(Database database) {
17 | this.database = database;
18 | }
19 |
20 | public void get(Callback callback) {
21 | Card userCard = this.database.getUserCard();
22 | callback.onGetUserCard(userCard);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/domain/SaveUserCard.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | @Singleton public class SaveUserCard {
9 |
10 | private final Database database;
11 |
12 | public interface Callback {
13 | void onSaveUserCard(Card savedCard);
14 | }
15 |
16 | @Inject public SaveUserCard(Database database) {
17 | this.database = database;
18 | }
19 |
20 | public void save(Card userCard, Callback callback) {
21 | userCard.setIsUser(true);
22 | database.saveCard(userCard);
23 | callback.onSaveUserCard(userCard);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/domain/GetReceivedCards.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import java.util.List;
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | @Singleton public class GetReceivedCards {
10 |
11 | private final Database database;
12 |
13 | public interface Callback {
14 | void onGetReceivedCards(List receivedCards);
15 | }
16 |
17 | @Inject public GetReceivedCards(Database database) {
18 | this.database = database;
19 | }
20 |
21 | public void get(Callback callback) {
22 | List receivedCards = database.getReceivedCards();
23 | callback.onGetReceivedCards(receivedCards);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #607D8B
4 | #00BFA5
5 |
6 | @color/primary
7 | #FAFAFA
8 | #000000
9 |
10 | #333333
11 | #999999
12 | #999999
13 | #FFFFFF
14 | #80FFFFFF
15 |
16 | #D8D8D8
17 |
18 | #DDDDDD
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_avatar.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle
16 | .gradle
17 | gradlew.bat
18 | build
19 | reports
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | # Android Studio captures folder
34 | captures/
35 |
36 | # Finder
37 | .DS_Store
38 |
39 | # IntelliJ IDEA
40 | .idea
41 | *.iml
42 | *.ipl
43 | *.iws
44 | classes/
45 | idea-classes/
46 | coverage-error.log
47 |
48 | # Android private files
49 | fabric.properties
50 | keystore.jks
51 | google-services.json
52 | google_analytics_tracker.xml
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/ExchangeContract.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import androidx.annotation.StringRes;
4 |
5 | import java.util.List;
6 |
7 | import io.bloco.cardcase.data.models.Card;
8 |
9 | public class ExchangeContract {
10 | public interface View {
11 |
12 | void setupCards(List receivedCards);
13 |
14 | void notifyCardAdded();
15 |
16 | void showCards();
17 |
18 | void showDone();
19 |
20 | void showError(@StringRes int messageRes);
21 |
22 | void openInvite();
23 |
24 | void close();
25 |
26 | void closeWithConfirmation();
27 | }
28 |
29 | public interface Presenter {
30 | void start(View view);
31 |
32 | void stop();
33 |
34 | void clickedInvite();
35 |
36 | void clickedClose();
37 |
38 | void clickedDone();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/home/HomeContract.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.home;
2 |
3 | import java.util.List;
4 |
5 | import io.bloco.cardcase.data.models.Card;
6 |
7 | public class HomeContract {
8 | public interface View {
9 | void showEmpty();
10 |
11 | void showCards(List cards);
12 |
13 | void showEmptySearchResult();
14 |
15 | void hideEmptySearchResult();
16 |
17 | void openOnboarding();
18 |
19 | void openUser();
20 |
21 | void openExchange();
22 |
23 | void openSearch();
24 |
25 | void closeSearch();
26 | }
27 |
28 | public interface Presenter {
29 | void start(View view);
30 |
31 | void clickedSearch();
32 |
33 | void clickedCloseSearch();
34 |
35 | void searchEntered(String query);
36 |
37 | void clickedUser();
38 |
39 | void clickedExchange();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/analytics/AnswersTracker.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.analytics;
2 |
3 | import android.content.Context;
4 | import androidx.annotation.Nullable;
5 | import com.crashlytics.android.answers.Answers;
6 | import com.crashlytics.android.answers.CustomEvent;
7 | import java.util.Map;
8 |
9 | public class AnswersTracker implements AnalyticsTracker {
10 |
11 | @Override public void init(Context context) {
12 | }
13 |
14 | @Override public void trackEvent(String eventName, @Nullable Map eventParams) {
15 | CustomEvent event = new CustomEvent(eventName);
16 |
17 | if (eventParams != null) {
18 | for (Map.Entry param : eventParams.entrySet()) {
19 | event.putCustomAttribute(param.getKey(), param.getValue());
20 | }
21 | }
22 |
23 | Answers.getInstance().logCustom(event);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/domain/SaveReceivedCards.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import java.util.List;
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | @Singleton public class SaveReceivedCards {
10 |
11 | private final Database database;
12 |
13 | public interface Callback {
14 | void onSavedReceivedCards(List savedCards);
15 | }
16 |
17 | @Inject public SaveReceivedCards(Database database) {
18 | this.database = database;
19 | }
20 |
21 | public void save(List receivedCards, Callback callback) {
22 | for (Card receivedCard : receivedCards) {
23 | receivedCard.setIsUser(false);
24 | receivedCard.setCreatedAt(null);
25 | }
26 | database.saveCards(receivedCards);
27 | callback.onSavedReceivedCards(receivedCards);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/SaveUserCardTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.mockito.Mock;
8 | import org.mockito.MockitoAnnotations;
9 |
10 | import static org.mockito.Matchers.eq;
11 | import static org.mockito.Mockito.verify;
12 |
13 | public class SaveUserCardTest {
14 |
15 | private SaveUserCard saveUserCard;
16 |
17 | @Mock private Database database;
18 | @Mock private SaveUserCard.Callback callback;
19 |
20 | @Before public void setUp() {
21 | MockitoAnnotations.initMocks(this);
22 | saveUserCard = new SaveUserCard(database);
23 | }
24 |
25 | @Test public void testGet() {
26 | Card card = new Card();
27 | saveUserCard.save(card, callback);
28 |
29 | verify(database).saveCard(eq(card));
30 | verify(callback).onSaveUserCard(eq(card));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/analytics/AnalyticsService.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.analytics;
2 |
3 | import android.content.Context;
4 |
5 | import java.util.Arrays;
6 | import java.util.List;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Singleton;
10 |
11 | @Singleton
12 | public class AnalyticsService {
13 |
14 | private boolean active;
15 | private List trackers;
16 |
17 | @Inject
18 | public AnalyticsService() {
19 | active = false;
20 | }
21 |
22 | public void init(Context context, AnalyticsTracker... trackers) {
23 | active = true;
24 | this.trackers = Arrays.asList(trackers);
25 | for (AnalyticsTracker tracker : trackers) {
26 | tracker.init(context);
27 | }
28 | }
29 |
30 | public void trackEvent(String event) {
31 | if (active) {
32 | for (AnalyticsTracker tracker : trackers) {
33 | tracker.trackEvent(event, null);
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/ImageLoader.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.Context;
4 | import android.graphics.BitmapFactory;
5 | import android.widget.ImageView;
6 | import com.squareup.picasso.Picasso;
7 | import java.io.File;
8 | import javax.inject.Inject;
9 | import javax.inject.Singleton;
10 | import timber.log.Timber;
11 |
12 | @Singleton public class ImageLoader {
13 |
14 | private final Context context;
15 |
16 | @Inject public ImageLoader(Context context) {
17 | this.context = context;
18 | }
19 |
20 | public void loadAvatar(ImageView imageView, String avatarPath) {
21 | if (BitmapFactory.decodeFile(avatarPath) == null) {
22 | Timber.w("Invalid avatar file");
23 | }
24 | Picasso.with(context)
25 | .load(new File(avatarPath))
26 | .fit()
27 | .centerCrop()
28 | .transform(new CircleTransform())
29 | .into(imageView);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | android.enableJetifier=true
20 | android.useAndroidX=true
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/user/UserContract.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.user;
2 |
3 | import io.bloco.cardcase.data.models.Card;
4 |
5 | public class UserContract {
6 | public interface View {
7 | void showUser(Card userCard);
8 |
9 | void showBack();
10 |
11 | void hideBack();
12 |
13 | void showCancel();
14 |
15 | void hideCancel();
16 |
17 | void showEditButton();
18 |
19 | void hideEditButton();
20 |
21 | void showDoneButton();
22 |
23 | void hideDoneButton();
24 |
25 | void enableEditMode();
26 |
27 | void disabledEditMode();
28 |
29 | void openHome();
30 |
31 | void close();
32 | }
33 |
34 | public interface Presenter {
35 | void start(View view, boolean onboarding);
36 |
37 | void clickedBack();
38 |
39 | void clickedCancel();
40 |
41 | void clickedEdit();
42 |
43 | void clickedDone(Card updatedCard);
44 |
45 | void onCardChanged(Card updatedCard);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/di/ActivityComponent.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.di;
2 |
3 | import android.app.Activity;
4 |
5 | import dagger.Component;
6 | import io.bloco.cardcase.presentation.exchange.ExchangeActivity;
7 | import io.bloco.cardcase.presentation.home.HomeActivity;
8 | import io.bloco.cardcase.presentation.user.CropAvatarActivity;
9 | import io.bloco.cardcase.presentation.user.UserActivity;
10 | import io.bloco.cardcase.presentation.welcome.WelcomeActivity;
11 |
12 | @PerActivity
13 | @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class)
14 | public interface ActivityComponent {
15 |
16 | void inject(CropAvatarActivity cropAvatarActivity);
17 |
18 | void inject(HomeActivity activity);
19 |
20 | void inject(UserActivity activity);
21 |
22 | void inject(ExchangeActivity activity);
23 |
24 | void inject(WelcomeActivity activity);
25 |
26 | //Exposed to sub-graphs.
27 | @SuppressWarnings("unused")
28 | Activity activity();
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/GetUserCardTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.mockito.Mock;
8 | import org.mockito.MockitoAnnotations;
9 |
10 | import static org.mockito.Matchers.eq;
11 | import static org.mockito.Mockito.verify;
12 | import static org.mockito.Mockito.when;
13 |
14 | public class GetUserCardTest {
15 |
16 | private GetUserCard getUserCard;
17 |
18 | @Mock private Database database;
19 | @Mock private GetUserCard.Callback callback;
20 |
21 | @Before public void setUp() {
22 | MockitoAnnotations.initMocks(this);
23 | getUserCard = new GetUserCard(database);
24 | }
25 |
26 | @Test public void testGet() {
27 | Card card = new Card();
28 | when(database.getUserCard()).thenReturn(card);
29 |
30 | getUserCard.get(callback);
31 |
32 | verify(callback).onGetUserCard(eq(card));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/Preconditions.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | // Based on http://google-collections.googlecode.com/svn-history/r78/trunk/javadoc/com/google/common/base/Preconditions.html
6 |
7 | public class Preconditions {
8 |
9 | @SuppressWarnings("unused")
10 | public static void checkArgument(boolean expression, @Nullable Object errorMessage) {
11 | if (!expression) {
12 | throw new IllegalArgumentException(String.valueOf(errorMessage));
13 | }
14 | }
15 |
16 | public static void checkState(boolean expression, @Nullable Object errorMessage) {
17 | if (!expression) {
18 | throw new IllegalStateException(String.valueOf(errorMessage));
19 | }
20 | }
21 |
22 | @SuppressWarnings("UnusedReturnValue")
23 | public static T checkNotNull(T reference, @Nullable Object errorMessage) {
24 | if (reference == null) {
25 | throw new NullPointerException(String.valueOf(errorMessage));
26 | }
27 | return reference;
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/SaveReceivedCardsTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.mockito.Mock;
10 | import org.mockito.MockitoAnnotations;
11 |
12 | import static org.mockito.Matchers.eq;
13 | import static org.mockito.Mockito.verify;
14 |
15 | public class SaveReceivedCardsTest {
16 |
17 | private SaveReceivedCards saveReceivedCards;
18 |
19 | @Mock private Database database;
20 | @Mock private SaveReceivedCards.Callback callback;
21 |
22 | @Before public void setUp() {
23 | MockitoAnnotations.initMocks(this);
24 | saveReceivedCards = new SaveReceivedCards(database);
25 | }
26 |
27 | @Test public void testGet() {
28 | List cards = Arrays.asList(new Card(), new Card());
29 | saveReceivedCards.save(cards, callback);
30 |
31 | verify(database).saveCards(eq(cards));
32 | verify(callback).onSavedReceivedCards(eq(cards));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/GetReceivedCardsTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain;
2 |
3 | import io.bloco.cardcase.data.Database;
4 | import io.bloco.cardcase.data.models.Card;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.mockito.Mock;
10 | import org.mockito.MockitoAnnotations;
11 |
12 | import static org.mockito.Matchers.eq;
13 | import static org.mockito.Mockito.verify;
14 | import static org.mockito.Mockito.when;
15 |
16 | public class GetReceivedCardsTest {
17 |
18 | private GetReceivedCards getReceivedCards;
19 |
20 | @Mock private Database database;
21 | @Mock private GetReceivedCards.Callback callback;
22 |
23 | @Before public void setUp() {
24 | MockitoAnnotations.initMocks(this);
25 | getReceivedCards = new GetReceivedCards(database);
26 | }
27 |
28 | @Test public void testGet() {
29 | List cards = Arrays.asList(new Card(), new Card());
30 | when(database.getReceivedCards()).thenReturn(cards);
31 |
32 | getReceivedCards.get(callback);
33 |
34 | verify(callback).onGetReceivedCards(eq(cards));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_crop.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
15 |
16 |
23 |
24 |
25 |
26 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/helpers/AssertCurrentActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.helpers;
2 |
3 | import android.app.Activity;
4 | import androidx.test.espresso.core.internal.deps.guava.collect.Iterables;
5 | import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
6 | import androidx.test.runner.lifecycle.Stage;
7 | import java.util.Collection;
8 |
9 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
10 | import static org.junit.Assert.assertEquals;
11 |
12 | public class AssertCurrentActivity {
13 | public static void assertCurrentActivity(Class extends Activity> activityClass) {
14 | assertEquals(activityClass.getName(), getCurrentActivity().getComponentName().getClassName());
15 | }
16 |
17 | private static Activity getCurrentActivity() {
18 | getInstrumentation().waitForIdleSync();
19 | final Activity[] activity = new Activity[1];
20 | getInstrumentation().runOnMainSync(() -> {
21 | Collection activities =
22 | ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
23 | activity[0] = Iterables.getOnlyElement(activities);
24 | });
25 | return activity[0];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/di/ActivityModule.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.di;
2 |
3 | import android.app.Activity;
4 |
5 | import dagger.Module;
6 | import dagger.Provides;
7 | import io.bloco.cardcase.presentation.exchange.ExchangeContract;
8 | import io.bloco.cardcase.presentation.exchange.ExchangePresenter;
9 | import io.bloco.cardcase.presentation.home.HomeContract;
10 | import io.bloco.cardcase.presentation.home.HomePresenter;
11 | import io.bloco.cardcase.presentation.user.UserContract;
12 | import io.bloco.cardcase.presentation.user.UserPresenter;
13 |
14 | @Module
15 | public class ActivityModule {
16 | private final Activity activity;
17 |
18 | public ActivityModule(Activity activity) {
19 | this.activity = activity;
20 | }
21 |
22 | @Provides
23 | @PerActivity
24 | Activity activity() {
25 | return this.activity;
26 | }
27 |
28 | @Provides
29 | @PerActivity
30 | public HomeContract.Presenter provideHomePresenter(HomePresenter presenter) {
31 | return presenter;
32 | }
33 |
34 | @Provides
35 | @PerActivity
36 | public UserContract.Presenter provideUserPresenter(UserPresenter presenter) {
37 | return presenter;
38 | }
39 |
40 | @Provides
41 | @PerActivity
42 | public ExchangeContract.Presenter provideExchangePresenter(ExchangePresenter presenter) {
43 | return presenter;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/DateTimeFormat.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.res.Resources;
4 | import android.text.format.DateUtils;
5 | import io.bloco.cardcase.R;
6 | import java.util.Date;
7 | import javax.inject.Inject;
8 | import javax.inject.Singleton;
9 |
10 | @Singleton
11 | class DateTimeFormat {
12 |
13 | private static final int SECOND_MILLIS = 1000;
14 | private static final int MINUTE_MILLIS = 60 * SECOND_MILLIS;
15 | private static final int HOUR_MILLIS = 60 * MINUTE_MILLIS;
16 |
17 | private final Resources mResources;
18 |
19 | @Inject public DateTimeFormat(Resources resources) {
20 | mResources = resources;
21 | }
22 |
23 | public String getRelativeTimeSpanString(Date timestamp) {
24 | long time = timestamp.getTime();
25 | long now = System.currentTimeMillis();
26 | long diff = now - time;
27 |
28 | if (diff < MINUTE_MILLIS) {
29 | return mResources.getString(R.string.time_just_now);
30 | } else if (diff < 50 * MINUTE_MILLIS) {
31 | int minutes = Math.round(diff / (float) MINUTE_MILLIS);
32 | return mResources.getString(R.string.time_minutes, minutes);
33 | } else if (diff < 24 * HOUR_MILLIS) {
34 | int hours = Math.round(diff / (float) HOUR_MILLIS);
35 | return mResources.getString(R.string.time_hours, hours);
36 | } else {
37 | return DateUtils.getRelativeTimeSpanString(time).toString();
38 | }
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
19 |
20 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/home/CardDetailDialog.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.home;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.view.Window;
6 | import android.view.WindowManager;
7 |
8 | import butterknife.BindView;
9 | import butterknife.ButterKnife;
10 | import io.bloco.cardcase.R;
11 | import io.bloco.cardcase.common.di.PerActivity;
12 | import io.bloco.cardcase.data.models.Card;
13 | import io.bloco.cardcase.presentation.common.CardInfoView;
14 |
15 | import javax.inject.Inject;
16 |
17 | @PerActivity
18 | public class CardDetailDialog {
19 |
20 | private final Dialog dialog;
21 |
22 | @BindView(R.id.card_dialog_info)
23 | CardInfoView cardInfoView;
24 |
25 | // TODO: Inject only the activity context?
26 | @Inject
27 | public CardDetailDialog(Activity activity) {
28 | this.dialog = new Dialog(activity);
29 | this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
30 | this.dialog.setContentView(R.layout.card_detail_dialog);
31 | ButterKnife.bind(this, dialog);
32 | }
33 |
34 | public void show(Card card) {
35 | fillCardInfoInDialog(card);
36 | dialog.show();
37 | Window window = dialog.getWindow();
38 | if (window != null)
39 | window.setLayout(
40 | WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
41 | }
42 |
43 | private void fillCardInfoInDialog(Card card) {
44 | cardInfoView.setCard(card);
45 | cardInfoView.showTime();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
14 |
15 |
19 |
20 |
21 |
22 |
29 |
30 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
32 |
33 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/ErrorDisplayer.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.res.Resources;
6 | import androidx.annotation.StringRes;
7 | import com.google.android.material.snackbar.Snackbar;
8 | import androidx.core.content.ContextCompat;
9 | import android.view.View;
10 | import android.widget.TextView;
11 | import javax.inject.Inject;
12 | import javax.inject.Singleton;
13 |
14 | @SuppressWarnings("unused")
15 | @Singleton public class ErrorDisplayer {
16 |
17 | private final Context context;
18 | private final Resources resources;
19 |
20 | @Inject public ErrorDisplayer(Context context, Resources resources) {
21 | this.context = context;
22 | this.resources = resources;
23 | }
24 |
25 | public void show(Activity activity, @StringRes int errorRes) {
26 | show(activity.findViewById(android.R.id.content), resources.getString(errorRes));
27 | }
28 |
29 | public void show(View view, @StringRes int errorRes) {
30 | show(view, resources.getString(errorRes));
31 | }
32 |
33 | private void show(View view, String errorMessage) {
34 | Snackbar snackbar = Snackbar.make(view, errorMessage, Snackbar.LENGTH_LONG);
35 | setTextColor(snackbar);
36 | snackbar.show();
37 | }
38 |
39 | private void setTextColor(Snackbar snackbar) {
40 | View view = snackbar.getView();
41 | TextView textView = view.findViewById(com.google.android.material.R.id.snackbar_text);
42 | textView.setTextColor(ContextCompat.getColor(context, android.R.color.white));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/di/ApplicationModule.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.di;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import com.google.gson.Gson;
6 | import com.j256.ormlite.dao.RuntimeExceptionDao;
7 | import com.j256.ormlite.support.ConnectionSource;
8 | import dagger.Module;
9 | import dagger.Provides;
10 | import io.bloco.cardcase.AndroidApplication;
11 | import io.bloco.cardcase.data.DatabaseHelper;
12 | import io.bloco.cardcase.data.models.Card;
13 | import java.sql.SQLException;
14 | import javax.inject.Singleton;
15 |
16 | @Module public class ApplicationModule {
17 | private final AndroidApplication application;
18 |
19 | public ApplicationModule(AndroidApplication application) {
20 | this.application = application;
21 | }
22 |
23 | @Provides @Singleton public Context provideApplicationContext() {
24 | return application;
25 | }
26 |
27 | @Provides @Singleton public Resources provideResources(Context context) {
28 | return context.getResources();
29 | }
30 |
31 | @Provides @Singleton public Gson provideGson() {
32 | return new Gson();
33 | }
34 |
35 | @Provides @Singleton public RuntimeExceptionDao provideCardDao() {
36 | DatabaseHelper databaseHelper =
37 | new DatabaseHelper(application.getApplicationContext(), application.getMode());
38 | ConnectionSource connectionSource = databaseHelper.getConnectionSource();
39 | try {
40 | return RuntimeExceptionDao.createDao(connectionSource, Card.class);
41 | } catch (SQLException exception) {
42 | throw new RuntimeException(exception);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 4dp
3 | 8dp
4 | 16dp
5 | 32dp
6 | 48dp
7 |
8 | 14sp
9 | 16sp
10 | 18sp
11 | 22sp
12 |
13 | 4dp
14 |
15 | 40dp
16 | 56dp
17 | 40dp
18 | @dimen/margin_small
19 | 56dp
20 | -48dp
21 |
22 | 56dp
23 | @dimen/margin_default
24 |
25 | @dimen/margin_double
26 | 128dp
27 | @dimen/margin_default
28 | 24sp
29 | 48dp
30 | 40dp
31 |
32 | 64dp
33 | @dimen/text_large
34 | 1px
35 | 56dp
36 |
37 | 88dp
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/analytics/GoogleAnalyticsTracker.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.analytics;
2 |
3 | import android.content.Context;
4 | import androidx.annotation.Nullable;
5 | import com.google.android.gms.analytics.GoogleAnalytics;
6 | import com.google.android.gms.analytics.HitBuilders;
7 | import com.google.android.gms.analytics.Tracker;
8 | import io.bloco.cardcase.R;
9 | import java.util.Map;
10 |
11 | public class GoogleAnalyticsTracker implements AnalyticsTracker {
12 | private static final String CATEGORY_KEY = "category";
13 | private static final String LABEL_KEY = "label";
14 | private static final String VALUE_KEY = "value";
15 |
16 | private static final String DEFAULT_CATEGORY = "All";
17 |
18 | private Tracker tracker;
19 |
20 | @Override public void init(Context context) {
21 | GoogleAnalytics analytics = GoogleAnalytics.getInstance(context);
22 | tracker = analytics.newTracker(R.xml.google_analytics_tracker);
23 | }
24 |
25 | @Override public void trackEvent(String eventName, @Nullable Map eventParams) {
26 | HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder();
27 | eventBuilder.setAction(eventName);
28 |
29 | if (eventParams != null && eventParams.containsKey(CATEGORY_KEY)) {
30 | eventBuilder.setCategory(eventParams.get(CATEGORY_KEY));
31 | } else {
32 | eventBuilder.setCategory(DEFAULT_CATEGORY);
33 | }
34 |
35 | if (eventParams != null && eventParams.containsKey(LABEL_KEY)) {
36 | eventBuilder.setLabel(eventParams.get(LABEL_KEY));
37 | }
38 |
39 | if (eventParams != null && eventParams.containsKey(VALUE_KEY)) {
40 | eventBuilder.setLabel(eventParams.get(VALUE_KEY));
41 | }
42 |
43 | tracker.send(eventBuilder.build());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import androidx.appcompat.app.AppCompatActivity;
8 | import androidx.core.app.ActivityCompat;
9 | import androidx.core.app.ActivityOptionsCompat;
10 |
11 | import io.bloco.cardcase.AndroidApplication;
12 | import io.bloco.cardcase.R;
13 | import io.bloco.cardcase.common.di.ActivityModule;
14 | import io.bloco.cardcase.common.di.ApplicationComponent;
15 | import io.bloco.cardcase.presentation.common.Toolbar;
16 | import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper;
17 |
18 | public abstract class BaseActivity extends AppCompatActivity {
19 |
20 | protected Toolbar toolbar;
21 |
22 | @Override
23 | protected void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | }
26 |
27 | @Override
28 | protected void attachBaseContext(Context newBase) {
29 | super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
30 | }
31 |
32 | protected ApplicationComponent getApplicationComponent() {
33 | return ((AndroidApplication) getApplication()).getApplicationComponent();
34 | }
35 |
36 | protected ActivityModule getActivityModule() {
37 | return new ActivityModule(this);
38 | }
39 |
40 | protected void startActivityWithAnimation(Intent intent) {
41 | @SuppressWarnings("unchecked")
42 | Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle();
43 | ActivityCompat.startActivity(this, intent, options);
44 | }
45 |
46 | protected void bindToolbar() {
47 | toolbar = findViewById(R.id.toolbar);
48 | }
49 |
50 | protected void finishWithAnimation() {
51 | ActivityCompat.finishAfterTransition(this);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/welcome/WelcomeActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.welcome;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import javax.inject.Inject;
8 |
9 | import butterknife.ButterKnife;
10 | import butterknife.OnClick;
11 | import io.bloco.cardcase.R;
12 | import io.bloco.cardcase.common.analytics.AnalyticsService;
13 | import io.bloco.cardcase.common.di.ActivityComponent;
14 | import io.bloco.cardcase.common.di.DaggerActivityComponent;
15 | import io.bloco.cardcase.presentation.BaseActivity;
16 | import io.bloco.cardcase.presentation.user.UserActivity;
17 |
18 | @SuppressWarnings("unused")
19 | public class WelcomeActivity extends BaseActivity {
20 |
21 | @Inject
22 | AnalyticsService analyticsService;
23 |
24 | public static class Factory {
25 | public static Intent getIntent(Context context) {
26 | return new Intent(context, WelcomeActivity.class);
27 | }
28 | }
29 |
30 | @Override
31 | protected void onCreate(Bundle savedInstanceState) {
32 | super.onCreate(savedInstanceState);
33 | setContentView(R.layout.activity_welcome);
34 | initializeInjectors();
35 | ButterKnife.bind(this);
36 | analyticsService.trackEvent("Welcome Screen");
37 | }
38 |
39 | @OnClick(R.id.welcome_start)
40 | void onClickStart() {
41 | Intent intent = UserActivity.Factory.getOnboardingIntent(this);
42 | startActivity(intent);
43 | finishWithAnimation();
44 | }
45 |
46 | private void initializeInjectors() {
47 | ActivityComponent component = DaggerActivityComponent.builder()
48 | .applicationComponent(getApplicationComponent())
49 | .activityModule(getActivityModule())
50 | .build();
51 | component.inject(this);
52 |
53 | ButterKnife.bind(this);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/NearbyManager.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import android.app.Activity;
4 |
5 | import com.google.android.gms.common.api.ApiException;
6 | import com.google.android.gms.common.api.Status;
7 | import com.google.android.gms.nearby.Nearby;
8 | import com.google.android.gms.nearby.messages.Message;
9 | import com.google.android.gms.nearby.messages.MessageListener;
10 |
11 | import javax.inject.Inject;
12 |
13 | import io.bloco.cardcase.common.di.PerActivity;
14 |
15 | @PerActivity
16 | public class NearbyManager extends MessageListener {
17 |
18 | private Message message;
19 | private Listener listener;
20 | private final Activity activity;
21 |
22 | public interface Listener {
23 |
24 | void onMessageReceived(byte[] messageBytes);
25 |
26 | void onError(Status status);
27 | }
28 |
29 | @Inject
30 | public NearbyManager(Activity activity) {
31 | this.activity = activity;
32 | }
33 |
34 | @Override
35 | public void onFound(Message message) {
36 | listener.onMessageReceived(message.getContent());
37 | }
38 | //onLost not needed in our case
39 |
40 | public void start(byte[] messageBytes, Listener listener) {
41 | this.message = new Message(messageBytes);
42 | this.listener = listener;
43 | Nearby.getMessagesClient(activity).publish(message)
44 | .addOnFailureListener(e -> {
45 | ApiException apiException = (ApiException) e;
46 | listener.onError(new Status(apiException.getStatusCode()));
47 | });
48 | Nearby.getMessagesClient(activity).subscribe(this);
49 | }
50 |
51 | public void stop() {
52 | // Clean up when the user leaves the activity.
53 | Nearby.getMessagesClient(activity).unpublish(message);
54 | Nearby.getMessagesClient(activity).unsubscribe(this);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Butter Knife
2 | -keep class butterknife.** { *; }
3 | -dontwarn butterknife.internal.**
4 | -keep class **$$ViewBinder { *; }
5 |
6 | -keepclasseswithmembernames class * {
7 | @butterknife.* ;
8 | }
9 |
10 | -keepclasseswithmembernames class * {
11 | @butterknife.* ;
12 | }
13 |
14 | # OrmLite
15 | -keep class com.j256.**
16 | -keepclassmembers class com.j256.** { *; }
17 | -keep enum com.j256.**
18 | -keepclassmembers enum com.j256.** { *; }
19 | -keep interface com.j256.**
20 | -keepclassmembers interface com.j256.** { *; }
21 | -keepattributes Signature
22 | -keepattributes *Annotation*
23 | -keepclassmembers class * { public (android.content.Context); }
24 | -keep class io.bloco.cardcase.data.models.**
25 | -keepclassmembers class io.bloco.cardcase.data.models.** { *; }
26 |
27 | # Picasso
28 | -dontwarn com.squareup.okhttp.**
29 |
30 | # Fabric
31 | -keep class com.crashlytics.** { *; }
32 | -keep class com.crashlytics.android.**
33 | -keepattributes SourceFile,LineNumberTable
34 |
35 | ## Gson
36 | # Gson uses generic type information stored in a class file when working with fields. Proguard
37 | # removes such information by default, so configure it to keep all of it.
38 | -keepattributes Signature
39 |
40 | # Gson specific classes
41 | -keep class sun.misc.Unsafe { *; }
42 | #-keep class com.google.gson.stream.** { *; }
43 |
44 | # Application classes that will be serialized/deserialized over Gson
45 | -keep class io.bloco.cardcase.data.models.** { *; }
46 |
47 | # SnakeYAML
48 | -keep class org.yaml.snakeyaml.** { public protected private *; }
49 | -keep class org.yaml.snakeyaml.** { public protected private *; }
50 | -dontwarn org.yaml.snakeyaml.**
51 |
52 | # Joda Time
53 | -dontwarn org.joda.convert.**
54 | -dontwarn org.joda.time.**
55 | -keep class org.joda.time.** { *; }
56 | -keep interface org.joda.time.** { *; }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/CardSerializer.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import com.google.gson.Gson;
4 |
5 | import java.io.File;
6 |
7 | import javax.inject.Inject;
8 | import javax.inject.Singleton;
9 |
10 | import io.bloco.cardcase.data.models.Card;
11 | import io.bloco.cardcase.presentation.common.FileHelper;
12 | import timber.log.Timber;
13 |
14 | @Singleton
15 | public class CardSerializer {
16 |
17 | private final Gson gson;
18 | private final FileHelper fileHelper;
19 |
20 | @Inject
21 | public CardSerializer(Gson gson, FileHelper fileHelper) {
22 | this.gson = gson;
23 | this.fileHelper = fileHelper;
24 | }
25 |
26 | public byte[] serialize(Card card) {
27 | CardWrapper cardWrapper = newCardWrapper(card);
28 | return gson.toJson(cardWrapper).getBytes();
29 | }
30 |
31 | public Card deserialize(byte[] data) {
32 | String cardSerialized = new String(data);
33 | Timber.i("Card received: %s", cardSerialized);
34 | CardWrapper cardWrapper = gson.fromJson(cardSerialized, CardWrapper.class);
35 | return unwrapCard(cardWrapper);
36 | }
37 |
38 | private CardWrapper newCardWrapper(Card card) {
39 | CardWrapper cardWrapper = new CardWrapper();
40 | cardWrapper.card = card;
41 | if (card.getAvatarPath() != null) {
42 | cardWrapper.avatarData = fileHelper.getBytesFromFile(card.getAvatar());
43 | }
44 | return cardWrapper;
45 | }
46 |
47 | private Card unwrapCard(CardWrapper cardWrapper) {
48 | Card card = cardWrapper.card;
49 | if (card == null) {
50 | return null;
51 | }
52 |
53 | if (card.getAvatarPath() != null) {
54 | File avatarFile = fileHelper.createFinalImageFile();
55 | fileHelper.saveBytesToFile(cardWrapper.avatarData, avatarFile);
56 | card.setAvatar(avatarFile);
57 | }
58 |
59 | return card;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/CardViewHolder.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.view.View;
4 | import android.widget.ImageView;
5 | import android.widget.TextView;
6 |
7 | import androidx.recyclerview.widget.RecyclerView;
8 |
9 | import javax.inject.Inject;
10 |
11 | import butterknife.BindView;
12 | import butterknife.ButterKnife;
13 | import io.bloco.cardcase.AndroidApplication;
14 | import io.bloco.cardcase.R;
15 | import io.bloco.cardcase.data.models.Card;
16 | import io.bloco.cardcase.presentation.home.CardDetailDialog;
17 |
18 | @SuppressWarnings("unused")
19 | public class CardViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
20 |
21 | @Inject
22 | DateTimeFormat dateTimeFormat;
23 | @Inject
24 | ImageLoader imageLoader;
25 |
26 | @BindView(R.id.card_avatar)
27 | ImageView avatar;
28 | @BindView(R.id.card_name)
29 | TextView name;
30 | @BindView(R.id.card_time)
31 | TextView time;
32 |
33 | private final CardDetailDialog cardDetailDialog;
34 | private Card card;
35 |
36 | public CardViewHolder(View view, CardDetailDialog cardDetailDialog) {
37 | super(view);
38 | view.setOnClickListener(this);
39 | this.cardDetailDialog = cardDetailDialog;
40 |
41 | ((AndroidApplication) view.getContext().getApplicationContext()).getApplicationComponent()
42 | .inject(this);
43 | ButterKnife.bind(this, view);
44 | }
45 |
46 | public void bind(Card card) {
47 | this.card = card;
48 | name.setText(card.getName());
49 |
50 | CharSequence timeStr = dateTimeFormat.getRelativeTimeSpanString(card.getUpdatedAt());
51 | time.setText(timeStr);
52 |
53 | if (card.hasAvatar()) {
54 | imageLoader.loadAvatar(avatar, card.getAvatarPath());
55 | }
56 | }
57 |
58 | @Override
59 | public void onClick(View view) {
60 | cardDetailDialog.show(card);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
30 |
31 |
41 |
42 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/data/Database.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.data;
2 |
3 | import com.j256.ormlite.dao.RuntimeExceptionDao;
4 | import com.j256.ormlite.stmt.Where;
5 | import com.j256.ormlite.table.TableUtils;
6 | import io.bloco.cardcase.data.models.Card;
7 | import java.sql.SQLException;
8 | import java.util.Calendar;
9 | import java.util.Date;
10 | import java.util.List;
11 | import javax.inject.Inject;
12 | import javax.inject.Singleton;
13 |
14 | @Singleton public class Database {
15 |
16 | private final RuntimeExceptionDao cardDao;
17 |
18 | @Inject public Database(RuntimeExceptionDao cardDao) {
19 | this.cardDao = cardDao;
20 | }
21 |
22 | public Card getUserCard() {
23 | try {
24 | return getCardQuery().eq("isUser", true).queryForFirst();
25 | } catch (SQLException exception) {
26 | throw new RuntimeException(exception);
27 | }
28 | }
29 |
30 | public List getReceivedCards() {
31 | try {
32 | return getCardQuery().eq("isUser", false).query();
33 | } catch (SQLException exception) {
34 | throw new RuntimeException(exception);
35 | }
36 | }
37 |
38 | public void saveCard(final Card card) {
39 | card.setUpdatedAt(now());
40 | if (card.getCreatedAt() == null) {
41 | card.setCreatedAt(card.getUpdatedAt());
42 | }
43 | cardDao.createOrUpdate(card);
44 | }
45 |
46 | public void saveCards(List cards) {
47 | for (Card card : cards) {
48 | saveCard(card);
49 | }
50 | }
51 |
52 | public void clear() {
53 | try {
54 | TableUtils.clearTable(cardDao.getConnectionSource(), Card.class);
55 | } catch (SQLException exception) {
56 | throw new RuntimeException(exception);
57 | }
58 | }
59 |
60 | private Where getCardQuery() {
61 | return cardDao.queryBuilder().orderBy("updatedAt", false).where();
62 | }
63 |
64 | private Date now() {
65 | return Calendar.getInstance().getTime();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/common/di/ApplicationComponent.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.common.di;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import com.google.gson.Gson;
6 | import com.j256.ormlite.dao.RuntimeExceptionDao;
7 | import dagger.Component;
8 | import io.bloco.cardcase.common.analytics.AnalyticsService;
9 | import io.bloco.cardcase.data.Database;
10 | import io.bloco.cardcase.data.models.Card;
11 | import io.bloco.cardcase.domain.GetReceivedCards;
12 | import io.bloco.cardcase.domain.GetUserCard;
13 | import io.bloco.cardcase.domain.SaveReceivedCards;
14 | import io.bloco.cardcase.domain.SaveUserCard;
15 | import io.bloco.cardcase.presentation.common.Bootstrap;
16 | import io.bloco.cardcase.presentation.common.CardInfoView;
17 | import io.bloco.cardcase.presentation.common.CardViewHolder;
18 | import io.bloco.cardcase.presentation.common.ErrorDisplayer;
19 | import io.bloco.cardcase.presentation.common.ImageLoader;
20 | import io.bloco.cardcase.presentation.exchange.CardSerializer;
21 | import io.bloco.cardcase.presentation.user.AvatarPicker;
22 | import javax.inject.Singleton;
23 |
24 | @SuppressWarnings("unused")
25 | @Singleton @Component(modules = ApplicationModule.class) public interface ApplicationComponent {
26 |
27 | void inject(CardViewHolder cardViewHolder);
28 |
29 | void inject(CardInfoView cardInfoView);
30 |
31 | //Exposed to sub-graphs.
32 | Context context();
33 |
34 | Resources resources();
35 |
36 | Gson gson();
37 |
38 | Database database();
39 |
40 | AvatarPicker avatarPicker();
41 |
42 | GetUserCard getUserCard();
43 |
44 | GetReceivedCards getReceivedCards();
45 |
46 | SaveUserCard saveUserCard();
47 |
48 | SaveReceivedCards saveReceivedCards();
49 |
50 | ImageLoader imageLoader();
51 |
52 | CardSerializer cardSerializer();
53 |
54 | AnalyticsService analyticsService();
55 |
56 | Bootstrap bootstrap();
57 |
58 | ErrorDisplayer errorDisplayer();
59 |
60 | RuntimeExceptionDao cardDao();
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/CircleTransform.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | /*
4 | * Copyright 2014 Julian Shen
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | import android.graphics.Bitmap;
20 | import android.graphics.BitmapShader;
21 | import android.graphics.Canvas;
22 | import android.graphics.Paint;
23 | import com.squareup.picasso.Transformation;
24 |
25 | /**
26 | * Created by julian on 13/6/21.
27 | */
28 | class CircleTransform implements Transformation {
29 | @Override public Bitmap transform(Bitmap source) {
30 | int size = Math.min(source.getWidth(), source.getHeight());
31 |
32 | int x = (source.getWidth() - size) / 2;
33 | int y = (source.getHeight() - size) / 2;
34 |
35 | Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
36 | if (squaredBitmap != source) {
37 | source.recycle();
38 | }
39 |
40 | Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
41 |
42 | Canvas canvas = new Canvas(bitmap);
43 | Paint paint = new Paint();
44 | BitmapShader shader =
45 | new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
46 | paint.setShader(shader);
47 | paint.setAntiAlias(true);
48 |
49 | float r = size / 2f;
50 | canvas.drawCircle(r, r, r, paint);
51 |
52 | squaredBitmap.recycle();
53 | return bitmap;
54 | }
55 |
56 | @Override public String key() {
57 | return "circle";
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | commands:
4 | cardcase_setup:
5 | description: "Get the app ready to test"
6 | steps:
7 | - checkout
8 | - run:
9 | name: Setup secret files
10 | command: |
11 | echo ${LOCAL_PROPERTIES} > ~/code/local.properties
12 | echo ${FABRIC_PROPERTIES} > ~/code/app/fabric.properties
13 | echo ${GOOGLE_ANALYTICS_TRACKER_XML} > ~/code/app/src/main/res/xml/google_analytics_tracker.xml
14 | echo ${GOOGLE_SERVICES_JSON} > ~/code/app/google-services.json
15 | - restore_cache:
16 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
17 | - run:
18 | name: Download Dependencies
19 | command: ./gradlew androidDependencies
20 | - save_cache:
21 | paths:
22 | - ~/.gradle
23 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
24 |
25 | executors:
26 | cardcase_executor:
27 | working_directory: ~/code
28 | docker:
29 | - image: circleci/android:api-28
30 | environment:
31 | JVM_OPTS: -Xmx3200m
32 |
33 | jobs:
34 | build:
35 | executor: cardcase_executor
36 | steps:
37 | - cardcase_setup
38 | - run:
39 | name: Build
40 | command: ./gradlew assemble
41 | lint:
42 | executor: cardcase_executor
43 | steps:
44 | - cardcase_setup
45 | - run:
46 | name: Run Lint
47 | command: ./gradlew lint
48 | - store_artifacts:
49 | path: app/build/reports
50 | destination: reports
51 | test:
52 | executor: cardcase_executor
53 | steps:
54 | - cardcase_setup
55 | - run:
56 | name: Run Tests
57 | command: ./gradlew test
58 | - store_test_results:
59 | path: app/build/test-results
60 | - store_artifacts:
61 | path: app/build/reports
62 | destination: reports
63 |
64 | workflows:
65 | version: 2.1
66 | build_lint_test:
67 | jobs:
68 | - build
69 | - lint:
70 | requires:
71 | - build
72 | - test:
73 | requires:
74 | - build
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_welcome.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
23 |
24 |
30 |
31 |
36 |
37 |
38 |
39 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/Toolbar.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.Context;
4 | import androidx.annotation.DrawableRes;
5 | import androidx.annotation.Nullable;
6 | import androidx.annotation.StringRes;
7 | import android.util.AttributeSet;
8 | import android.view.View;
9 | import android.widget.ImageButton;
10 | import android.widget.TextView;
11 |
12 | import butterknife.BindView;
13 | import butterknife.ButterKnife;
14 | import io.bloco.cardcase.R;
15 |
16 | public class Toolbar extends androidx.appcompat.widget.Toolbar {
17 |
18 | @BindView(R.id.toolbar_title) TextView title;
19 | @BindView(R.id.toolbar_icon_start) ImageButton iconStart;
20 | @BindView(R.id.toolbar_icon_end) ImageButton iconEnd;
21 |
22 | public Toolbar(Context context, @Nullable AttributeSet attrs) {
23 | super(context, attrs);
24 | inflate(context, R.layout.view_toolbar, this);
25 | ButterKnife.bind(this);
26 | }
27 |
28 | @Override public void setTitle(@StringRes int titleRes) {
29 | String title = getResources().getString(titleRes);
30 | setTitle(title);
31 | }
32 |
33 | @Override public void setTitle(CharSequence titleStr) {
34 | title.setText(titleStr);
35 | }
36 |
37 | public void setStartButton(@DrawableRes int drawableRes, @StringRes int stringRes,
38 | OnClickListener listener) {
39 | String description = getResources().getString(stringRes);
40 | iconStart.setContentDescription(description);
41 | iconStart.setImageResource(drawableRes);
42 | iconStart.setOnClickListener(listener);
43 | iconStart.setVisibility(View.VISIBLE);
44 | }
45 |
46 | public void removeStartButton() {
47 | iconStart.setVisibility(View.GONE);
48 | }
49 |
50 | public void setEndButton(@DrawableRes int drawableRes, @StringRes int stringRes,
51 | OnClickListener listener) {
52 | String description = getResources().getString(stringRes);
53 | iconEnd.setContentDescription(description);
54 | iconEnd.setImageResource(drawableRes);
55 | iconEnd.setOnClickListener(listener);
56 | iconEnd.setVisibility(View.VISIBLE);
57 | }
58 |
59 | public void removeEndButton() {
60 | iconEnd.setVisibility(View.GONE);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/helpers/CardFactory.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.helpers;
2 |
3 | import androidx.test.rule.ActivityTestRule;
4 | import com.j256.ormlite.dao.RuntimeExceptionDao;
5 | import com.j256.ormlite.table.TableUtils;
6 | import io.bloco.cardcase.AndroidApplication;
7 | import io.bloco.cardcase.data.models.Card;
8 | import io.bloco.faker.Faker;
9 | import java.sql.SQLException;
10 | import java.util.UUID;
11 |
12 | @SuppressWarnings("ALL")
13 | public class CardFactory {
14 |
15 | private final Faker faker;
16 | private RuntimeExceptionDao dao;
17 |
18 | private CardFactory() {
19 | faker = new Faker();
20 | }
21 |
22 | public CardFactory(ActivityTestRule activityTestRule) {
23 | this(((AndroidApplication) activityTestRule.getActivity().getApplication())
24 | .getApplicationComponent().cardDao());
25 | }
26 |
27 | public CardFactory(RuntimeExceptionDao dao) {
28 | this();
29 | this.dao = dao;
30 | }
31 |
32 | private Card build() {
33 | Card card = new Card();
34 | card.setId(UUID.randomUUID().toString());
35 | card.setName(faker.name.name());
36 | card.setEmail(faker.internet.email(card.getName()));
37 | card.setPhone(faker.phoneNumber.phoneNumber());
38 | card.setCreatedAt(faker.time.backward(365));
39 | card.setUpdatedAt(card.getCreatedAt());
40 | return card;
41 | }
42 |
43 | public Card buildUserCard() {
44 | Card card = build();
45 | card.setIsUser(true);
46 | return card;
47 | }
48 |
49 | private Card buildReceivedCard() {
50 | Card card = build();
51 | card.setIsUser(false);
52 | return card;
53 | }
54 |
55 | public Card create() {
56 | return create(build());
57 | }
58 |
59 | public Card createUserCard() {
60 | return create(buildUserCard());
61 | }
62 |
63 | public Card createReceivedCard() {
64 | return create(buildReceivedCard());
65 | }
66 |
67 | private Card create(final Card card) {
68 | dao.createOrUpdate(card);
69 | return card;
70 | }
71 |
72 | public void clear() {
73 | try {
74 | TableUtils.clearTable(dao.getConnectionSource(), Card.class);
75 | } catch (SQLException exception) {
76 | throw new RuntimeException(exception);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/SearchToolbar.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import android.util.AttributeSet;
8 | import android.view.inputmethod.InputMethodManager;
9 | import android.widget.EditText;
10 |
11 | import butterknife.BindView;
12 | import butterknife.ButterKnife;
13 | import butterknife.OnClick;
14 | import io.bloco.cardcase.R;
15 | import io.bloco.cardcase.presentation.home.SimpleTextWatcher;
16 |
17 | public class SearchToolbar extends androidx.appcompat.widget.Toolbar {
18 |
19 | @BindView(R.id.toolbar_search_field)
20 | EditText field;
21 |
22 | private SearchListener listener;
23 |
24 | public SearchToolbar(Context context, @Nullable AttributeSet attrs) {
25 | super(context, attrs);
26 | }
27 |
28 | @Override
29 | protected void onFinishInflate() {
30 | super.onFinishInflate();
31 | ButterKnife.bind(this);
32 |
33 | field.addTextChangedListener(new SimpleTextWatcher() {
34 | @Override
35 | public void onTextChanged(String newText) {
36 | if (listener != null) {
37 | listener.onSearchQuery(newText);
38 | }
39 | }
40 | });
41 | }
42 |
43 | @OnClick(R.id.toolbar_search_close)
44 | public void onCloseClicked() {
45 | clear();
46 |
47 | if (listener != null) {
48 | listener.onSearchClosed();
49 | }
50 | }
51 |
52 | public void setListener(SearchListener listener) {
53 | this.listener = listener;
54 | }
55 |
56 | public void clear() {
57 | field.setText("");
58 | field.postDelayed(() -> {
59 | InputMethodManager keyboard = (InputMethodManager)
60 | getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
61 | if (keyboard != null)
62 | keyboard.hideSoftInputFromWindow(field.getWindowToken(), 0);
63 | }, 100);
64 | }
65 |
66 | public void focus() {
67 | field.requestFocus();
68 | field.postDelayed(() -> {
69 | InputMethodManager keyboard = (InputMethodManager)
70 | getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
71 | if (keyboard != null)
72 | keyboard.showSoftInput(field, 0);
73 | }, 100);
74 | }
75 |
76 | public interface SearchListener {
77 | void onSearchClosed();
78 |
79 | void onSearchQuery(String query);
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/presenters/ExchangePresenterTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain.presenters;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 | import org.mockito.ArgumentCaptor;
6 | import org.mockito.Captor;
7 | import org.mockito.InjectMocks;
8 | import org.mockito.Mock;
9 | import org.mockito.MockitoAnnotations;
10 |
11 | import java.util.List;
12 |
13 | import io.bloco.cardcase.common.analytics.AnalyticsService;
14 | import io.bloco.cardcase.data.models.Card;
15 | import io.bloco.cardcase.domain.GetUserCard;
16 | import io.bloco.cardcase.domain.SaveReceivedCards;
17 | import io.bloco.cardcase.presentation.exchange.CardSerializer;
18 | import io.bloco.cardcase.presentation.exchange.ExchangeContract;
19 | import io.bloco.cardcase.presentation.exchange.ExchangePresenter;
20 | import io.bloco.cardcase.presentation.exchange.NearbyManager;
21 |
22 | import static org.junit.Assert.assertTrue;
23 | import static org.mockito.ArgumentMatchers.any;
24 | import static org.mockito.Mockito.verify;
25 | import static org.mockito.Mockito.when;
26 |
27 | @SuppressWarnings("unused")
28 | public class ExchangePresenterTest {
29 | @InjectMocks
30 | private ExchangePresenter exchangePresenter;
31 |
32 | @Mock
33 | private NearbyManager nearbyManager;
34 | @Mock
35 | private CardSerializer cardSerializer;
36 | @Mock
37 | private GetUserCard getUserCard;
38 | @Mock
39 | private SaveReceivedCards saveReceivedCards;
40 | @Mock
41 | private AnalyticsService analyticsService;
42 | @Mock
43 | private ExchangeContract.View view;
44 | @Captor
45 | private ArgumentCaptor> receivedCardsCaptor;
46 |
47 | @Before
48 | public void setup() {
49 | MockitoAnnotations.initMocks(this);
50 | exchangePresenter.start(view);
51 | }
52 |
53 | @Test
54 | @SuppressWarnings("unchecked")
55 | public void onMessageReceived() {
56 | ArgumentCaptor> receivedCardsCaptor = ArgumentCaptor.forClass(List.class);
57 | verify(view).setupCards(receivedCardsCaptor.capture());
58 | List receivedCards = receivedCardsCaptor.getValue();
59 |
60 | Card card = new Card();
61 | card.setId("1");
62 | card.setName("Sergio");
63 | when(cardSerializer.deserialize(any())).thenReturn(card);
64 |
65 | exchangePresenter.onMessageReceived(new byte[]{});
66 | verify(view).showCards();
67 | assertTrue(receivedCards.contains(card));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/AndroidApplication.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase;
2 |
3 | import android.app.Application;
4 |
5 | import com.crashlytics.android.Crashlytics;
6 |
7 | import io.bloco.cardcase.common.analytics.AnswersTracker;
8 | import io.bloco.cardcase.common.analytics.GoogleAnalyticsTracker;
9 | import io.bloco.cardcase.common.di.ApplicationComponent;
10 | import io.bloco.cardcase.common.di.ApplicationModule;
11 | import io.bloco.cardcase.common.di.DaggerApplicationComponent;
12 | import io.fabric.sdk.android.Fabric;
13 | import timber.log.Timber;
14 | import uk.co.chrisjenx.calligraphy.CalligraphyConfig;
15 |
16 | public class AndroidApplication extends Application {
17 |
18 | private Mode mode;
19 | private ApplicationComponent applicationComponent;
20 |
21 | @Override
22 | public void onCreate() {
23 | super.onCreate();
24 | checkTestMode();
25 | this.initializeInjector();
26 |
27 | if (BuildConfig.DEBUG) {
28 | Timber.plant(new Timber.DebugTree());
29 | }
30 |
31 | // Analytics
32 | if (!BuildConfig.DEBUG) {
33 | Fabric.with(this, new Crashlytics());
34 | applicationComponent.analyticsService()
35 | .init(this, new GoogleAnalyticsTracker(), new AnswersTracker());
36 | }
37 |
38 | CalligraphyConfig.initDefault(
39 | new CalligraphyConfig.Builder().setDefaultFontPath("fonts/Lato-Regular.ttf")
40 | .setFontAttrId(R.attr.fontPath)
41 | .build());
42 |
43 | // Uncomment this to fill database with fake cards
44 | // if (BuildConfig.DEBUG) {
45 | // getApplicationComponent().bootstrap().clearAndBootstrap();
46 | // }
47 | }
48 |
49 | public ApplicationComponent getApplicationComponent() {
50 | return this.applicationComponent;
51 | }
52 |
53 | public Mode getMode() {
54 | return mode;
55 | }
56 |
57 | private void initializeInjector() {
58 | this.applicationComponent =
59 | DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build();
60 | }
61 |
62 | // Test loading a random test class, to check if we're in test mode
63 | private void checkTestMode() {
64 | try {
65 | getClassLoader().loadClass("io.bloco.cardcase.presentation.HomeActivityTest");
66 | mode = Mode.TEST;
67 | } catch (final Exception e) {
68 | mode = Mode.NORMAL;
69 | }
70 | }
71 |
72 | public enum Mode {
73 | NORMAL, TEST
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Card Case
2 |
3 | ⚠️ Warning: this project is no longer being maintained. ⚠️
4 |
5 | [](https://circleci.com/gh/blocoio/cardcase)
6 |
7 | _Exchange your business card with people nearby_ ([Website](https://www.bloco.io/card-case))
8 |
9 | Leave your paper business cards at home. With Card Case you can create your digital business card, and share it with people nearby in seconds.
10 |
11 | Using Google’s Nearby technology, the Card Case app exchanges your business cards with people around you. You don’t need to create any account or send emails. And it works instantly.
12 |
13 |
14 |
16 |
17 |
18 |
20 |
21 | ## Features
22 |
23 | - Share your name, photo, email, phone number… any information you want
24 | - Exchange cards with one or multiple people near you, at the same time
25 | - Only exchange your business card when you want to
26 | - Keep all the cards you received for later
27 | - Search your received cards
28 |
29 | ## Development
30 |
31 | Before building the application, you will need to:
32 |
33 | - Create a `local.properties` file with `google_nearby_api_key`
34 | - Create a `app/fabric.properties` file with `apiKey` and `apiSecret`
35 | - Add your `google_analytics_tracker.xml` to the `app/src/main/res/xml` folder
36 | - Add your `google-services.json` file to the `app` folder
37 |
38 | ## License
39 |
40 | Copyright 2016 Block Studio, Lda
41 |
42 | Licensed under the Apache License, Version 2.0 (the "License");
43 | you may not use this file except in compliance with the License.
44 | You may obtain a copy of the License at
45 |
46 | http://www.apache.org/licenses/LICENSE-2.0
47 |
48 | Unless required by applicable law or agreed to in writing, software
49 | distributed under the License is distributed on an "AS IS" BASIS,
50 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51 | See the License for the specific language governing permissions and
52 | limitations under the License.
53 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/presentation/CardSerializerTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation;
2 |
3 |
4 | import android.content.Context;
5 |
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 | import androidx.test.platform.app.InstrumentationRegistry;
8 |
9 | import com.google.gson.Gson;
10 |
11 | import org.junit.After;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 |
16 | import java.io.File;
17 | import java.io.FileOutputStream;
18 | import java.util.Random;
19 |
20 | import io.bloco.cardcase.data.models.Card;
21 | import io.bloco.cardcase.presentation.common.FileHelper;
22 | import io.bloco.cardcase.presentation.exchange.CardSerializer;
23 |
24 | import static org.hamcrest.CoreMatchers.equalTo;
25 | import static org.hamcrest.core.Is.is;
26 | import static org.junit.Assert.assertThat;
27 | import static org.junit.Assert.assertTrue;
28 |
29 | @RunWith(AndroidJUnit4.class)
30 | public class CardSerializerTest {
31 |
32 | private static final long FILE_LENGTH = 1000;
33 |
34 | private CardSerializer cardSerializer;
35 | private File avatarFile;
36 |
37 | @Before
38 | public void setUp() throws Exception {
39 | cardSerializer = new CardSerializer(new Gson(), new FileHelper(getContext()));
40 |
41 | avatarFile = File.createTempFile("temp", ".jpg", getContext().getCacheDir());
42 | FileOutputStream fos = new FileOutputStream(avatarFile);
43 | for (int i = 0; i < FILE_LENGTH; i++) {
44 | fos.write(new Random().nextInt());
45 | }
46 | fos.close();
47 | }
48 |
49 | @Test
50 | public void test() {
51 | Card card = new Card();
52 | card.setId("card-id");
53 | card.setAvatar(avatarFile);
54 |
55 | byte[] data = cardSerializer.serialize(card);
56 | //noinspection ResultOfMethodCallIgnored
57 | avatarFile.delete();
58 | Card outputCard = cardSerializer.deserialize(data);
59 |
60 | assertThat(outputCard.getId(), is(equalTo(card.getId())));
61 | assertTrue(outputCard.getAvatar().exists());
62 | assertThat(outputCard.getAvatar().length(), is(equalTo(FILE_LENGTH)));
63 | }
64 |
65 | @After
66 | public void tearDown() {
67 | if (avatarFile != null && avatarFile.exists()) {
68 | //noinspection ResultOfMethodCallIgnored
69 | avatarFile.delete();
70 | avatarFile = null;
71 | }
72 | }
73 |
74 | private Context getContext() {
75 | return InstrumentationRegistry.getInstrumentation().getTargetContext();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/data/DatabaseTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.data;
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4;
4 | import androidx.test.platform.app.InstrumentationRegistry;
5 |
6 | import com.j256.ormlite.dao.RuntimeExceptionDao;
7 |
8 | import org.junit.After;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.junit.runner.RunWith;
12 |
13 | import java.util.Arrays;
14 | import java.util.List;
15 |
16 | import io.bloco.cardcase.AndroidApplication;
17 | import io.bloco.cardcase.common.di.ApplicationModule;
18 | import io.bloco.cardcase.data.models.Card;
19 | import io.bloco.cardcase.helpers.CardFactory;
20 |
21 | import static org.hamcrest.core.IsCollectionContaining.hasItems;
22 | import static org.junit.Assert.assertEquals;
23 | import static org.junit.Assert.assertNull;
24 | import static org.junit.Assert.assertThat;
25 |
26 | @RunWith(AndroidJUnit4.class)
27 | public class DatabaseTest {
28 |
29 | private RuntimeExceptionDao cardDao;
30 | private Database database;
31 | private CardFactory cardFactory;
32 |
33 | @Before
34 | public void setUp() {
35 | cardDao = new ApplicationModule(getApplication()).provideCardDao();
36 | database = new Database(cardDao);
37 | cardFactory = new CardFactory(cardDao);
38 | }
39 |
40 | @Test
41 | public void testSaveCard() throws Exception {
42 | Card card = cardFactory.buildUserCard();
43 | database.saveCard(card);
44 |
45 | assertEquals(cardDao.countOf(), 1);
46 | assertEquals(cardDao.queryBuilder().queryForFirst(), card);
47 | }
48 | @Test
49 | public void testSaveCards() {
50 | Card card1 = cardFactory.buildUserCard();
51 | Card card2 = cardFactory.buildUserCard();
52 | List cards = Arrays.asList(card1, card2);
53 | database.saveCards(cards);
54 |
55 | assertEquals(cardDao.countOf(), 2);
56 | assertThat(cardDao.queryForAll(), hasItems(card1, card2));
57 | }
58 | @Test
59 | public void testGetUserCard() {
60 | Card card = cardFactory.createUserCard();
61 | assertEquals(database.getUserCard().getId(), card.getId());
62 | }
63 | @Test
64 | public void testGetUserCardEmpty() {
65 | assertNull(database.getUserCard());
66 | }
67 | @Test
68 | public void testGetReceivedCard() {
69 | cardFactory.createUserCard();
70 | cardFactory.createReceivedCard();
71 | cardFactory.createReceivedCard();
72 |
73 | assertEquals(database.getReceivedCards().size(), 2);
74 | }
75 |
76 | @After
77 | public void tearDown() {
78 | cardFactory.clear();
79 | }
80 |
81 | private AndroidApplication getApplication() {
82 | return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/CardAdapter.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.view.LayoutInflater;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.recyclerview.widget.RecyclerView;
9 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | import javax.inject.Inject;
14 |
15 | import io.bloco.cardcase.R;
16 | import io.bloco.cardcase.common.di.PerActivity;
17 | import io.bloco.cardcase.data.models.Card;
18 | import io.bloco.cardcase.presentation.home.CardDetailDialog;
19 |
20 | @PerActivity
21 | public class CardAdapter extends RecyclerView.Adapter {
22 |
23 | private static class ViewType {
24 | private static final int NORMAL = 0;
25 | private static final int FOOTER = 1;
26 | }
27 |
28 | private final CardDetailDialog cardDetailDialog;
29 | private List cards;
30 | private boolean showLoader;
31 |
32 | @Inject
33 | public CardAdapter(CardDetailDialog cardDetailDialog) {
34 | this.cards = new ArrayList<>();
35 | this.cardDetailDialog = cardDetailDialog;
36 | this.showLoader = false;
37 | }
38 |
39 | public void showLoader() {
40 | this.showLoader = true;
41 | }
42 |
43 | @Override
44 | public int getItemCount() {
45 | return cards.size() + (showLoader ? 1 : 0);
46 | }
47 |
48 | @Override
49 | public int getItemViewType(int position) {
50 | if (position < cards.size()) {
51 | return ViewType.NORMAL;
52 | } else {
53 | return ViewType.FOOTER;
54 | }
55 | }
56 |
57 | @NonNull
58 | @Override
59 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
60 | View view;
61 | switch (viewType) {
62 | case ViewType.FOOTER:
63 | view = LayoutInflater.from(parent.getContext())
64 | .inflate(R.layout.card_list_loader, parent, false);
65 | return new FooterViewHolder(view);
66 |
67 | case ViewType.NORMAL:
68 | default:
69 | view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card, parent, false);
70 | return new CardViewHolder(view, cardDetailDialog);
71 | }
72 | }
73 |
74 | @Override
75 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
76 | if (holder instanceof CardViewHolder) {
77 | Card card = cards.get(position);
78 | ((CardViewHolder) holder).bind(card);
79 | }
80 | }
81 |
82 | public void setCards(List cards) {
83 | this.cards = cards;
84 | }
85 |
86 | private static class FooterViewHolder extends RecyclerView.ViewHolder {
87 |
88 | FooterViewHolder(View view) {
89 | super(view);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
20 |
21 |
24 |
25 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
49 |
50 |
54 |
55 |
58 |
59 |
64 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/Bootstrap.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.Context;
4 | import io.bloco.cardcase.data.Database;
5 | import io.bloco.cardcase.data.models.Card;
6 | import io.bloco.faker.Faker;
7 | import java.io.File;
8 | import java.io.FileOutputStream;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.io.OutputStream;
12 | import javax.inject.Inject;
13 |
14 | @SuppressWarnings("unused")
15 | public class Bootstrap {
16 |
17 | private static final int NUM_CARDS = 16;
18 | private static final int NUM_AVATARS = 10;
19 |
20 | private final Context context;
21 | private final Database database;
22 | private final FileHelper fileHelper;
23 | private final Faker faker;
24 | private int avatarIndex = 0;
25 |
26 | @Inject public Bootstrap(Context context, Database database, FileHelper fileHelper) {
27 | this.context = context;
28 | this.database = database;
29 | this.fileHelper = fileHelper;
30 | this.faker = new Faker();
31 | }
32 |
33 | public void clearAndBootstrap() {
34 | database.clear();
35 | bootstrap();
36 | }
37 |
38 | private void bootstrap() {
39 | Card userCard = buildFakeCard();
40 | userCard.setIsUser(true);
41 | database.saveCard(userCard);
42 |
43 | for (int i = 0; i < NUM_CARDS; i++) {
44 | Card card = buildFakeCard();
45 | database.saveCard(card);
46 | }
47 | }
48 |
49 | private Card buildFakeCard() {
50 | String avatarFilePath = "avatars/avatar"+(avatarIndex+1)+".jpg";
51 | String avatarPath = fileFromAssetPath(avatarFilePath).getAbsolutePath();
52 | avatarIndex = (avatarIndex + 1) % NUM_AVATARS;
53 |
54 | Card card = new Card();
55 | card.setName(faker.name.name());
56 | card.setEmail(faker.internet.safeEmail(card.getName().split(" ")[0]));
57 | card.setPhone(faker.phoneNumber.cellPhone());
58 | card.setAvatarPath(avatarPath);
59 | return card;
60 | }
61 |
62 | private File fileFromAssetPath(String assetPath) {
63 | InputStream inputStream;
64 | try {
65 | inputStream = context.getAssets().open(assetPath);
66 | } catch (IOException exception) {
67 | throw new RuntimeException(exception);
68 | }
69 | File file = fileHelper.createFinalImageFile();
70 | copyInputStreamToFile(inputStream, file);
71 | return file;
72 | }
73 |
74 | private void copyInputStreamToFile( InputStream in, File file ) {
75 | OutputStream out;
76 | try {
77 | out = new FileOutputStream(file);
78 | byte[] buf = new byte[1024];
79 | int len;
80 | while((len=in.read(buf))>0){
81 | out.write(buf,0,len);
82 | }
83 | out.close();
84 | in.close();
85 | } catch (Exception exception) {
86 | throw new RuntimeException(exception);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/test/java/io/bloco/cardcase/domain/presenters/HomePresenterTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.domain.presenters;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 | import org.mockito.InjectMocks;
6 | import org.mockito.Mock;
7 | import org.mockito.MockitoAnnotations;
8 |
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.Collections;
12 | import java.util.List;
13 |
14 | import io.bloco.cardcase.common.analytics.AnalyticsService;
15 | import io.bloco.cardcase.data.models.Card;
16 | import io.bloco.cardcase.domain.GetReceivedCards;
17 | import io.bloco.cardcase.domain.GetUserCard;
18 | import io.bloco.cardcase.presentation.home.HomeContract;
19 | import io.bloco.cardcase.presentation.home.HomePresenter;
20 |
21 | import static org.mockito.Mockito.verify;
22 |
23 | @SuppressWarnings("ALL")
24 | public class HomePresenterTest {
25 | @InjectMocks
26 | private HomePresenter homePresenter;
27 |
28 | @Mock
29 | private HomeContract.View view;
30 | @Mock
31 | private List receivedCards;
32 | @Mock
33 | private GetUserCard getUserCard;
34 | @Mock
35 | private GetReceivedCards getReceivedCards;
36 | @Mock
37 | private AnalyticsService analyticsService;
38 |
39 |
40 | @Before
41 | public void setup() {
42 | MockitoAnnotations.initMocks(this);
43 | homePresenter.start(view);
44 | }
45 |
46 | @Test
47 | public void testClickedSearch() {
48 | homePresenter.clickedSearch();
49 | verify(view).openSearch();
50 | }
51 |
52 | @Test
53 | public void searchClickedUser() {
54 | homePresenter.clickedUser();
55 | verify(view).openUser();
56 | }
57 |
58 | @Test
59 | public void searchEnteredName() {
60 | receivedCards = new ArrayList<>();
61 | Card card1 = new Card();
62 | card1.setName("sergio");
63 | card1.setId("1");
64 | receivedCards.add(card1);
65 | Card card2 = new Card();
66 | card2.setName("joao");
67 | card2.setId("2");
68 | receivedCards.add(card2);
69 | Card card3 = new Card();
70 | card3.setName("maria");
71 | card3.setId("3");
72 | receivedCards.add(card3);
73 |
74 | homePresenter.onGetReceivedCards(receivedCards);
75 | homePresenter.searchEntered(card2.getName());
76 | verify(view).showCards(Collections.singletonList(card2));
77 | }
78 |
79 | @Test
80 | public void onGetReceivedCards() {
81 | receivedCards = new ArrayList<>();
82 | Card card1 = new Card();
83 | card1.setName("sergio");
84 | card1.setId("1");
85 | receivedCards.add(card1);
86 | Card card2 = new Card();
87 | card2.setName("joao");
88 | card2.setId("2");
89 | receivedCards.add(card2);
90 | Card card3 = new Card();
91 | card3.setName("maria");
92 | card3.setId("3");
93 | receivedCards.add(card3);
94 |
95 | homePresenter.onGetReceivedCards(receivedCards);
96 | verify(view).showCards(Arrays.asList(card1, card2, card3));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/data/DatabaseHelper.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.data;
2 |
3 | import android.content.Context;
4 | import android.database.sqlite.SQLiteDatabase;
5 | import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
6 | import com.j256.ormlite.dao.RuntimeExceptionDao;
7 | import com.j256.ormlite.support.ConnectionSource;
8 | import com.j256.ormlite.table.TableUtils;
9 | import io.bloco.cardcase.AndroidApplication;
10 | import io.bloco.cardcase.data.models.Card;
11 | import java.sql.SQLException;
12 | import javax.inject.Inject;
13 | import javax.inject.Singleton;
14 |
15 | @SuppressWarnings("unused")
16 | @Singleton public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
17 |
18 | private static final String DATABASE_NAME = "database";
19 | private static final String TEST_DATABASE_NAME = "database_test";
20 | private static final int DATABASE_VERSION = 2;
21 |
22 | private final Class[] mTables = new Class[] { Card.class };
23 |
24 | @Inject public DatabaseHelper(Context context, AndroidApplication.Mode mode) {
25 | super(context, getDbName(mode), null, DATABASE_VERSION);
26 | }
27 |
28 | private static String getDbName(AndroidApplication.Mode mode) {
29 | return mode == AndroidApplication.Mode.NORMAL ? DATABASE_NAME : TEST_DATABASE_NAME;
30 | }
31 |
32 | @SuppressWarnings("unchecked")
33 | @Override public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
34 | try {
35 | for (Class tableClass : mTables) {
36 | TableUtils.createTableIfNotExists(connectionSource, tableClass);
37 | }
38 | } catch (SQLException e) {
39 | throw new RuntimeException(e);
40 | }
41 | }
42 |
43 | @Override
44 | public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion,
45 | int newVersion) {
46 | RuntimeExceptionDao cardsDao = getCardsDao(connectionSource);
47 |
48 | if (oldVersion < 2) {
49 | cardsDao.executeRaw("ALTER TABLE `cards` ADD COLUMN email VARCHAR;");
50 | cardsDao.executeRaw("ALTER TABLE `cards` ADD COLUMN phone VARCHAR;");
51 | }
52 | }
53 |
54 | @SuppressWarnings("unchecked")
55 | public void clear() {
56 | try {
57 | for (Class tableClass : mTables) {
58 | TableUtils.clearTable(getConnectionSource(), tableClass);
59 | }
60 | } catch (SQLException e) {
61 | throw new RuntimeException(e);
62 | }
63 | }
64 |
65 | @SuppressWarnings("unchecked")
66 | private void dropTables() throws SQLException {
67 | for (Class tableClass : mTables) {
68 | TableUtils.dropTable(connectionSource, tableClass, true);
69 | }
70 | }
71 |
72 | private RuntimeExceptionDao getCardsDao(ConnectionSource connectionSource) {
73 | try {
74 | return RuntimeExceptionDao.createDao(connectionSource, Card.class);
75 | } catch (SQLException exception) {
76 | throw new RuntimeException(exception);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_home.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
12 |
15 |
16 |
17 |
18 |
26 |
27 |
33 |
34 |
40 |
41 |
42 |
43 |
49 |
50 |
54 |
55 |
56 |
57 |
65 |
66 |
71 |
72 |
73 |
74 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Card Case
3 | Cards received
4 | My card
5 | Back
6 | Done
7 | Photo
8 | Name
9 | Email
10 | Phone number
11 | Exchange
12 | Close
13 | Edit
14 | Added %s
15 | Waiting
16 | OK
17 | Cancel
18 |
19 | Change avatar
20 | Where from?
21 | Crop photo
22 | Cropping photo
23 |
24 | now
25 | %dm
26 | %dh
27 |
28 | Create your card
29 | Welcome to Card Case
30 | The easiest way to exchange contacts with people nearby
31 |
32 |
33 | Here you will see the\n
34 | cards you receive.\n\n
35 | Mingle and exchange\n
36 | cards with others!
37 |
38 | Send email
39 |
40 |
41 |
42 | Sharing your card\n
43 | and searching for other cards
44 |
45 | Make sure others are also exchanging their card
46 |
47 | Invite them
49 | ]]>
50 |
51 | Cards received
52 | Are you sure you want to close and lose the cards you
53 | received?
54 |
55 | Discard and close
56 |
57 |
58 | Invite to Card Case
59 |
60 | Hey, check out Card Case, an app to easily exchange business cards with people nearby.
61 |
62 | Invite
63 |
64 |
65 | Error (sorry!)
66 | Could not connect. Please confirm internet is available and try
67 | again.
68 |
69 | Could not process image. Please try with a different one.
70 |
71 | Could not crop image. Please try again.
72 | Search cards
73 | No cards found
74 |
75 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/presentation/UserActivityOnOnboardingTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation;
2 |
3 | import android.content.Intent;
4 |
5 | import androidx.test.ext.junit.runners.AndroidJUnit4;
6 | import androidx.test.rule.ActivityTestRule;
7 |
8 | import org.junit.After;
9 | import org.junit.Before;
10 | import org.junit.Rule;
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 |
14 | import io.bloco.cardcase.R;
15 | import io.bloco.cardcase.helpers.CardFactory;
16 | import io.bloco.cardcase.presentation.user.UserActivity;
17 | import io.bloco.faker.Faker;
18 |
19 | import static androidx.test.espresso.Espresso.onView;
20 | import static androidx.test.espresso.action.ViewActions.clearText;
21 | import static androidx.test.espresso.action.ViewActions.click;
22 | import static androidx.test.espresso.action.ViewActions.typeText;
23 | import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
24 | import static androidx.test.espresso.assertion.ViewAssertions.matches;
25 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
26 | import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
27 | import static androidx.test.espresso.matcher.ViewMatchers.withId;
28 | import static androidx.test.espresso.matcher.ViewMatchers.withText;
29 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
30 | import static org.hamcrest.Matchers.not;
31 |
32 | @RunWith(AndroidJUnit4.class)
33 | public class UserActivityOnOnboardingTest {
34 |
35 | @Rule
36 | public final ActivityTestRule activityTestRule =
37 | new ActivityTestRule<>(UserActivity.class, true, false);
38 | private CardFactory cardFactory;
39 |
40 | @Before
41 | public void setUp() {
42 | Intent intent = UserActivity.Factory.getOnboardingIntent(getInstrumentation().getTargetContext());
43 | activityTestRule.launchActivity(intent);
44 | cardFactory = new CardFactory(activityTestRule);
45 | }
46 |
47 | @Test
48 | public void testBackIsNotVisible() {
49 | onView(withContentDescription(R.string.back)).check(doesNotExist());
50 | }
51 |
52 | @Test
53 | public void testDoneVisibility() throws Exception {
54 | onView(withId(R.id.user_done)).check(matches(not(isDisplayed())));
55 |
56 | String name = new Faker().name.name();
57 | onView(withId(R.id.card_name)).perform(typeText(name));
58 | onView(withId(R.id.user_done)).check(matches(isDisplayed()));
59 |
60 | onView(withId(R.id.card_name)).perform(clearText());
61 | Thread.sleep(250);
62 | onView(withId(R.id.user_done)).check(matches(not(isDisplayed())));
63 | }
64 |
65 | @Test
66 | public void testUserDataIsSaved() {
67 | String name = new Faker().name.name();
68 | onView(withId(R.id.card_name)).perform(typeText(name));
69 | // closeSoftKeyboard();
70 | onView(withId(R.id.user_done)).perform(click());
71 |
72 | onView(withContentDescription(R.string.user_card)).perform(click());
73 | onView(withContentDescription(R.string.user_card)).perform(click());
74 | onView(withText(name)).check(matches(isDisplayed()));
75 | }
76 |
77 | @After
78 | public void tearDown() {
79 | cardFactory.clear();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Card Case
3 | Cartões recebidos
4 | O meu cartão
5 | Voltar
6 | Terminar
7 | Foto
8 | Nome
9 | Email
10 | Telefone
11 | Trocar
12 | Fechar
13 | Editar
14 | Adicionado em %s
15 | Em espera
16 | OK
17 | Cancelar
18 |
19 | Mudar foto
20 | A partir de onde?
21 | Recortar foto
22 | A recortar foto
23 |
24 | agora
25 | %dm
26 | %dh
27 |
28 | Cria o teu cartão
29 | Bem-vindo ao Card Case
30 | A forma mais fácil de trocar contactos com pessoas perto de ti
31 |
32 |
33 |
34 | Aqui irás ver os\n
35 | cartões que recebes.\n\n
36 | Conversa e troca cartões\n
37 | com outras pessoas!
38 |
39 | Enviar email
40 |
41 |
42 |
43 | A partilhar o teu cartão\n
44 | e à procura de outros cartões
45 |
46 |
47 | Confirma que as outras pessoas também estão a partilhar os seus cartões
48 |
49 |
50 | Convida-os
52 | ]]>
53 |
54 | Cartões recebidos
55 |
56 | Tens a certeza que queres fechar e perder os cartões que recebeste?
57 |
58 | Descartar e fechar
59 |
60 |
61 | Convida para o Card Case
62 |
63 | Olá, experimenta o Card Case, uma app para partilhar contactos com pessoas perto de ti.
64 |
65 | Convidar
66 |
67 |
68 | Erro (desculpa!)
69 |
70 | Não foi possível estabelecer uma ligação.
71 | Por favor, confirma que tens Internet disponível e tenta novamente.
72 |
73 | Não foi possível processar a image. Por favor tente com uma
74 | imagem diferente.
75 |
76 | Não foi possível cortar imagem. Por favor tente novamente.
77 |
78 | Pesquisar cartões
79 | Nenhum cartão encontrado
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/user/UserPresenter.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.user;
2 |
3 | import java.util.UUID;
4 |
5 | import javax.inject.Inject;
6 |
7 | import io.bloco.cardcase.common.analytics.AnalyticsService;
8 | import io.bloco.cardcase.common.di.PerActivity;
9 | import io.bloco.cardcase.data.models.Card;
10 | import io.bloco.cardcase.domain.GetUserCard;
11 | import io.bloco.cardcase.domain.SaveUserCard;
12 |
13 | @PerActivity
14 | public class UserPresenter
15 | implements UserContract.Presenter, GetUserCard.Callback, SaveUserCard.Callback {
16 |
17 | private final GetUserCard getUserCard;
18 | private final SaveUserCard saveUserCard;
19 | private final AnalyticsService analyticsService;
20 | private UserContract.View view;
21 | private boolean onboarding;
22 | private Card userCard;
23 |
24 | @Inject
25 | public UserPresenter(GetUserCard getUserCard, SaveUserCard saveUserCard,
26 | AnalyticsService analyticsService) {
27 | this.getUserCard = getUserCard;
28 | this.saveUserCard = saveUserCard;
29 | this.analyticsService = analyticsService;
30 | }
31 |
32 | @Override
33 | public void start(UserContract.View view, boolean onboarding) {
34 | this.view = view;
35 | this.onboarding = onboarding;
36 |
37 | if (onboarding) {
38 | userCard = new Card();
39 | userCard.setId(UUID.randomUUID().toString());
40 | view.showUser(userCard);
41 | view.enableEditMode();
42 | analyticsService.trackEvent("Onboarding Screen");
43 | } else {
44 | getUserCard.get(this);
45 | view.showBack();
46 | view.showEditButton();
47 | analyticsService.trackEvent("User Screen");
48 | }
49 | }
50 |
51 | @Override
52 | public void clickedBack() {
53 | view.close();
54 | }
55 |
56 | @Override
57 | public void clickedCancel() {
58 | view.showUser(userCard);
59 | view.disabledEditMode();
60 | view.hideDoneButton();
61 | view.hideCancel();
62 | view.showBack();
63 | view.showEditButton();
64 | }
65 |
66 | @Override
67 | public void clickedEdit() {
68 | view.hideEditButton();
69 | view.hideBack();
70 | view.showCancel();
71 | view.enableEditMode();
72 | analyticsService.trackEvent("User Card Edit");
73 | }
74 |
75 | @Override
76 | public void clickedDone(Card updatedCard) {
77 | view.hideDoneButton();
78 | view.hideCancel();
79 | this.userCard = updatedCard;
80 | saveUserCard.save(userCard, this);
81 | analyticsService.trackEvent("User Card Save");
82 | }
83 |
84 | @Override
85 | public void onCardChanged(Card updatedCard) {
86 | if (updatedCard.isValid()) {
87 | view.showDoneButton();
88 | } else {
89 | view.hideDoneButton();
90 | }
91 | }
92 |
93 | @Override
94 | public void onGetUserCard(Card userCard) {
95 | this.userCard = userCard;
96 | view.showUser(this.userCard);
97 | }
98 |
99 | @Override
100 | public void onSaveUserCard(Card savedCard) {
101 | if (onboarding) {
102 | view.openHome();
103 | view.close();
104 | } else {
105 | userCard = savedCard;
106 | view.disabledEditMode();
107 | view.showBack();
108 | view.showEditButton();
109 | view.showUser(userCard);
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/presentation/HomeActivityTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation;
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4;
4 | import androidx.test.rule.ActivityTestRule;
5 |
6 | import org.junit.After;
7 | import org.junit.Before;
8 | import org.junit.Rule;
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 |
12 | import io.bloco.cardcase.R;
13 | import io.bloco.cardcase.data.models.Card;
14 | import io.bloco.cardcase.helpers.CardFactory;
15 | import io.bloco.cardcase.presentation.home.HomeActivity;
16 | import io.bloco.cardcase.presentation.welcome.WelcomeActivity;
17 |
18 | import static androidx.test.espresso.Espresso.onView;
19 | import static androidx.test.espresso.action.ViewActions.click;
20 | import static androidx.test.espresso.assertion.ViewAssertions.matches;
21 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
22 | import static androidx.test.espresso.matcher.ViewMatchers.withId;
23 | import static androidx.test.espresso.matcher.ViewMatchers.withText;
24 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
25 | import static io.bloco.cardcase.helpers.AssertCurrentActivity.assertCurrentActivity;
26 | import static org.hamcrest.core.StringStartsWith.startsWith;
27 |
28 | @RunWith(AndroidJUnit4.class)
29 | public class HomeActivityTest {
30 |
31 | @Rule
32 | public final ActivityTestRule activityTestRule =
33 | new ActivityTestRule<>(HomeActivity.class);
34 | private CardFactory cardFactory;
35 |
36 | @Before
37 | public void setupFactory() {
38 | cardFactory = new CardFactory(activityTestRule);
39 | }
40 |
41 | @Test
42 | public void testFirstOpen() {
43 | assertCurrentActivity(WelcomeActivity.class);
44 | }
45 |
46 | @Test
47 | public void testOpenWithUserCard() {
48 | createUserCard();
49 | launchApp();
50 | assertCurrentActivity(HomeActivity.class);
51 | }
52 |
53 | @Test
54 | public void testReceivedCards(){
55 | Card card1 = createReceivedCard();
56 | Card card2 = createReceivedCard();
57 |
58 | createUserCard();
59 | launchApp();
60 |
61 | onView(withText(card1.getName())).check(matches(isDisplayed()));
62 | onView(withText(card2.getName())).check(matches(isDisplayed()));
63 | }
64 |
65 | @Test
66 | public void testViewCardDetails() {
67 | Card card = createReceivedCard();
68 |
69 | createUserCard();
70 | launchApp();
71 |
72 | onView(withText(card.getName())).perform(click());
73 |
74 | onView(withId(R.id.card_avatar)).check(matches(isDisplayed()));
75 | onView(withId(R.id.card_name)).check(matches(withText(card.getName())));
76 | onView(withId(R.id.card_email)).check(matches(withText(card.getEmail())));
77 | onView(withId(R.id.card_phone)).check(matches(withText(card.getPhone())));
78 | onView(withId(R.id.card_time)).check(matches(withText(startsWith("Ad"))));
79 | }
80 |
81 | @After
82 | public void cleanUp() {
83 | cardFactory.clear();
84 | }
85 |
86 | private void launchApp() {
87 | activityTestRule.launchActivity(
88 | HomeActivity.Factory.getIntent(getInstrumentation().getTargetContext()));
89 | }
90 |
91 | @SuppressWarnings("UnusedReturnValue")
92 | private Card createUserCard() {
93 | return cardFactory.createUserCard();
94 | }
95 |
96 | private Card createReceivedCard() {
97 | return cardFactory.createReceivedCard();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_card_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
21 |
28 |
29 |
39 |
40 |
41 |
50 |
51 |
61 |
62 |
72 |
73 |
78 |
79 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/ExchangePresenter.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import com.google.android.gms.common.api.Status;
4 |
5 | import java.util.ArrayList;
6 | import java.util.Calendar;
7 | import java.util.List;
8 |
9 | import javax.inject.Inject;
10 |
11 | import io.bloco.cardcase.R;
12 | import io.bloco.cardcase.common.analytics.AnalyticsService;
13 | import io.bloco.cardcase.common.di.PerActivity;
14 | import io.bloco.cardcase.data.models.Card;
15 | import io.bloco.cardcase.domain.GetUserCard;
16 | import io.bloco.cardcase.domain.SaveReceivedCards;
17 | import timber.log.Timber;
18 |
19 | @PerActivity
20 | public class ExchangePresenter
21 | implements ExchangeContract.Presenter, NearbyManager.Listener, GetUserCard.Callback {
22 |
23 | private final NearbyManager nearbyManager;
24 | private final CardSerializer cardSerializer;
25 | private final GetUserCard getUserCard;
26 | private final SaveReceivedCards saveReceivedCards;
27 | private final AnalyticsService analyticsService;
28 | private ExchangeContract.View view;
29 | private List receivedCards;
30 | private boolean errorState;
31 |
32 | @Inject
33 | public ExchangePresenter(NearbyManager nearbyManager, CardSerializer cardSerializer,
34 | GetUserCard getUserCard, SaveReceivedCards saveReceivedCards,
35 | AnalyticsService analyticsService) {
36 | this.nearbyManager = nearbyManager;
37 | this.cardSerializer = cardSerializer;
38 | this.getUserCard = getUserCard;
39 | this.saveReceivedCards = saveReceivedCards;
40 | this.analyticsService = analyticsService;
41 | }
42 |
43 | @Override
44 | public void start(ExchangeContract.View view) {
45 | this.view = view;
46 | this.errorState = false;
47 | this.receivedCards = new ArrayList<>();
48 |
49 | view.setupCards(this.receivedCards);
50 | getUserCard.get(this);
51 |
52 | analyticsService.trackEvent("Exchange Screen");
53 | }
54 |
55 | @Override
56 | public void stop() {
57 | nearbyManager.stop();
58 | }
59 |
60 | @Override
61 | public void onGetUserCard(Card userCard) {
62 | nearbyManager.start(cardSerializer.serialize(userCard), this);
63 | }
64 |
65 | @Override
66 | public void clickedInvite() {
67 | analyticsService.trackEvent("Exchange Invite Open");
68 | view.openInvite();
69 | }
70 |
71 | @Override
72 | public void clickedClose() {
73 | if (receivedCards.isEmpty()) {
74 | view.close();
75 | } else {
76 | view.closeWithConfirmation();
77 | }
78 | }
79 |
80 | @Override
81 | public void clickedDone() {
82 | saveReceivedCards.save(receivedCards, savedCards -> view.close());
83 | }
84 |
85 | @Override
86 | public void onMessageReceived(byte[] messageBytes) {
87 | analyticsService.trackEvent("Exchange Card Received");
88 | Card card = cardSerializer.deserialize(messageBytes);
89 | if (card != null) {
90 | addNewCard(card);
91 | }
92 | }
93 |
94 | @Override
95 | public void onError(Status status) {
96 | Timber.e("Exchange error: %s", status);
97 | showConnectionError();
98 | }
99 |
100 | private void addNewCard(final Card card) {
101 | if (receivedCards.contains(card)) {
102 | return;
103 | }
104 |
105 | card.setCreatedAt(Calendar.getInstance().getTime());
106 | card.setUpdatedAt(card.getCreatedAt());
107 | card.setIsUser(false);
108 |
109 | receivedCards.add(card);
110 | view.notifyCardAdded();
111 |
112 | if (receivedCards.size() == 1) { // First card added
113 | view.showCards();
114 | view.showDone();
115 | }
116 | }
117 |
118 | private void showConnectionError() {
119 | if (errorState) {
120 | return;
121 | }
122 | errorState = true;
123 | view.showError(R.string.error_connection);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/home/HomePresenter.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.home;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import javax.inject.Inject;
7 |
8 | import io.bloco.cardcase.common.analytics.AnalyticsService;
9 | import io.bloco.cardcase.common.di.PerActivity;
10 | import io.bloco.cardcase.data.models.Card;
11 | import io.bloco.cardcase.domain.GetReceivedCards;
12 | import io.bloco.cardcase.domain.GetUserCard;
13 |
14 | @PerActivity
15 | public class HomePresenter
16 | implements HomeContract.Presenter, GetUserCard.Callback, GetReceivedCards.Callback {
17 |
18 | private final GetUserCard getUserCard;
19 | private final GetReceivedCards getReceivedCards;
20 | private final AnalyticsService analyticsService;
21 | private HomeContract.View view;
22 | private List receivedCards;
23 |
24 | @Inject
25 | public HomePresenter(GetUserCard getUserCard, GetReceivedCards getReceivedCards,
26 | AnalyticsService analyticsService) {
27 | this.getUserCard = getUserCard;
28 | this.getReceivedCards = getReceivedCards;
29 | this.analyticsService = analyticsService;
30 | }
31 |
32 | @Override
33 | public void start(HomeContract.View view) {
34 | this.view = view;
35 | getUserCard.get(HomePresenter.this);
36 |
37 | /*
38 | Card card1 = new Card();
39 | card1.setId("1");
40 | card1.setName("Caroline Smith");
41 | card1.setCreatedAt(Calendar.getInstance().getTime());
42 | card1.setUpdatedAt(Calendar.getInstance().getTime());
43 |
44 | Card card2 = new Card();
45 | card2.setId("2");
46 | card2.setName("Jamie Frazier");
47 | card2.setCreatedAt(new Date(Calendar.getInstance().getTimeInMillis() - 1000000));
48 | card2.setUpdatedAt(new Date(Calendar.getInstance().getTimeInMillis() - 1000000));
49 |
50 | Card card3 = new Card();
51 | card3.setId("3");
52 | card3.setName("Arnold Tucker");
53 | card3.setCreatedAt(new Date(Calendar.getInstance().getTimeInMillis() - 5000000));
54 | card3.setUpdatedAt(new Date(Calendar.getInstance().getTimeInMillis() - 5000000));
55 |
56 | onGetReceivedCards(Arrays.asList(card1, card2, card3));
57 | */
58 |
59 | analyticsService.trackEvent("Home Screen");
60 | }
61 |
62 | @Override
63 | public void clickedSearch() {
64 | view.openSearch();
65 | analyticsService.trackEvent("Home Open Search");
66 | }
67 |
68 | @Override
69 | public void clickedCloseSearch() {
70 | view.hideEmptySearchResult();
71 | view.closeSearch();
72 | showReceivedCards();
73 | }
74 |
75 | @Override
76 | public void searchEntered(String query) {
77 | List filteredCards = new ArrayList<>(receivedCards.size());
78 | for (Card card : receivedCards) {
79 | if (card.matchQuery(query)) {
80 | filteredCards.add(card);
81 | }
82 | }
83 |
84 | if (filteredCards.isEmpty()) {
85 | view.showEmptySearchResult();
86 | } else {
87 | view.hideEmptySearchResult();
88 | }
89 |
90 | view.showCards(filteredCards);
91 | }
92 |
93 | @Override
94 | public void clickedUser() {
95 | view.openUser();
96 | analyticsService.trackEvent("Home View Card");
97 | }
98 |
99 | @Override
100 | public void clickedExchange() {
101 | view.openExchange();
102 | }
103 |
104 | @Override
105 | public void onGetUserCard(Card userCard) {
106 | if (userCard == null) {
107 | view.openOnboarding();
108 | } else {
109 | getReceivedCards.get(this);
110 | }
111 | }
112 |
113 | @Override
114 | public void onGetReceivedCards(List receivedCards) {
115 | this.receivedCards = receivedCards;
116 | showReceivedCards();
117 | }
118 |
119 | private void showReceivedCards() {
120 | if (this.receivedCards.isEmpty()) {
121 | view.showEmpty();
122 | } else {
123 | view.showCards(this.receivedCards);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_exchange.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
15 |
16 |
17 |
18 |
20 |
21 |
26 |
27 |
33 |
34 |
39 |
40 |
46 |
47 |
51 |
52 |
66 |
67 |
68 |
69 |
75 |
76 |
82 |
83 |
87 |
88 |
89 |
90 |
91 |
92 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | maven { url 'https://maven.fabric.io/public' }
4 | }
5 |
6 | dependencies {
7 | classpath 'io.fabric.tools:gradle:1.30.0'
8 | }
9 | }
10 | apply plugin: 'com.android.application'
11 | apply plugin: 'io.fabric'
12 |
13 | repositories {
14 | jcenter()
15 | google()
16 | maven { url 'https://maven.fabric.io/public' }
17 | maven { url 'https://jitpack.io' }
18 | }
19 |
20 | android {
21 | compileSdkVersion 28
22 | buildToolsVersion '29.0.2'
23 |
24 | defaultConfig {
25 | applicationId "io.bloco.cardcase"
26 | minSdkVersion 21
27 | targetSdkVersion 28
28 | versionCode 26
29 | versionName "1.1.0"
30 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
31 | resConfigs 'en', 'pt'
32 |
33 | def filesAuthorityValue = applicationId + ".files"
34 | manifestPlaceholders = [filesAuthority: filesAuthorityValue]
35 | buildConfigField "String", "FILES_AUTORITY", "\"${filesAuthorityValue}\""
36 | resValue "string", "google_nearby_api_key",
37 | getLocalProperties().getProperty("google_nearby_api_key")
38 | }
39 |
40 | buildTypes {
41 | release {
42 | minifyEnabled true
43 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
44 | }
45 | }
46 |
47 | lintOptions {
48 | warningsAsErrors true
49 | disable 'DefaultLocale'
50 | }
51 |
52 | packagingOptions {
53 | exclude 'LICENSE'
54 | exclude 'LICENSE.txt'
55 | exclude 'NOTICE'
56 | exclude 'asm-license.txt'
57 | exclude 'META-INF/LICENSE.txt'
58 | exclude 'META-INF/services/javax.annotation.processing.Processor'
59 | }
60 |
61 | compileOptions {
62 | sourceCompatibility JavaVersion.VERSION_1_8
63 | targetCompatibility JavaVersion.VERSION_1_8
64 | }
65 | }
66 |
67 | dependencies {
68 | implementation 'androidx.appcompat:appcompat:1.1.0'
69 | implementation 'androidx.cardview:cardview:1.0.0'
70 | implementation 'com.google.android.material:material:1.0.0'
71 | implementation 'com.google.android.gms:play-services-nearby:17.0.0'
72 | implementation 'com.google.android.gms:play-services-appinvite:18.0.0'
73 | implementation 'com.google.android.gms:play-services-analytics:17.0.0'
74 | // Dagger
75 | implementation 'com.google.dagger:dagger:2.23'
76 | annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
77 | // Logging
78 | implementation 'com.jakewharton.timber:timber:4.7.1'
79 | // View injection
80 | implementation 'com.jakewharton:butterknife:10.2.0'
81 | annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
82 | // Database
83 | implementation 'com.j256.ormlite:ormlite-android:4.48'
84 | // Fonts
85 | implementation 'uk.co.chrisjenx:calligraphy:2.3.0'
86 | // Image loading
87 | implementation 'com.squareup.picasso:picasso:2.5.2'
88 | // Crop avatar
89 | implementation 'com.lyft:scissors:1.0.1'
90 | // JSON serialization
91 | implementation 'com.google.code.gson:gson:2.8.5'
92 | // Crash & usage monitoring
93 | implementation('com.crashlytics.sdk.android:crashlytics:2.10.1@aar') {
94 | transitive = true
95 | }
96 | // Generate fake data
97 | implementation 'com.github.blocoio:faker:1.2.7'
98 | // Testing
99 | testImplementation 'junit:junit:4.12'
100 | testImplementation 'org.mockito:mockito-core:2.10.0'
101 | androidTestImplementation 'junit:junit:4.12'
102 | androidTestImplementation 'androidx.annotation:annotation:1.1.0'
103 | androidTestImplementation 'androidx.test:runner:1.2.0'
104 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
105 | androidTestImplementation 'androidx.test:rules:1.2.0'
106 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
107 | androidTestImplementation('androidx.test.espresso:espresso-contrib:3.2.0') {
108 | exclude module: 'recyclerview-v7'
109 | exclude module: 'support-v4'
110 | }
111 | }
112 |
113 | apply plugin: 'com.google.gms.google-services'
114 |
115 | def getLocalProperties() {
116 | Properties properties = new Properties()
117 | properties.load(new File(rootDir.absolutePath + "/local.properties").newDataInputStream())
118 | return properties
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/data/models/Card.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.data.models;
2 |
3 | import com.j256.ormlite.field.DataType;
4 | import com.j256.ormlite.field.DatabaseField;
5 | import com.j256.ormlite.table.DatabaseTable;
6 |
7 | import java.io.File;
8 | import java.util.ArrayList;
9 | import java.util.Date;
10 | import java.util.List;
11 |
12 | @DatabaseTable(tableName = "cards")
13 | public class Card {
14 |
15 | @DatabaseField(id = true)
16 | private String id;
17 | @DatabaseField(canBeNull = false)
18 | private String name;
19 | @DatabaseField
20 | private String email;
21 | @DatabaseField
22 | private String phone;
23 | @DatabaseField
24 | private String avatarPath;
25 | @DatabaseField(dataType = DataType.SERIALIZABLE)
26 | private ArrayList fields;
27 | @DatabaseField
28 | private transient Date createdAt;
29 | @DatabaseField
30 | private transient Date updatedAt;
31 | @DatabaseField
32 | private transient boolean isUser;
33 |
34 | public Card() {
35 | fields = new ArrayList<>();
36 | }
37 |
38 | @Override
39 | public boolean equals(Object o) {
40 | if (this == o) {
41 | return true;
42 | }
43 | if (o == null || getClass() != o.getClass()) {
44 | return false;
45 | }
46 | Card card = (Card) o;
47 | return id.equals(card.id);
48 | }
49 |
50 | @Override
51 | public int hashCode() {
52 | return id.hashCode();
53 | }
54 |
55 | public Card copy() {
56 | Card card = new Card();
57 | card.id = id;
58 | card.name = name;
59 | card.email = email;
60 | card.phone = phone;
61 | card.avatarPath = avatarPath;
62 | card.fields = new ArrayList<>(getFields());
63 | card.createdAt = createdAt;
64 | card.updatedAt = updatedAt;
65 | card.isUser = isUser;
66 | return card;
67 | }
68 |
69 | public String getId() {
70 | return id;
71 | }
72 |
73 | public void setId(String id) {
74 | this.id = id;
75 | }
76 |
77 | public String getName() {
78 | return name;
79 | }
80 |
81 | public void setName(String name) {
82 | this.name = name;
83 | }
84 |
85 | public String getEmail() {
86 | return email;
87 | }
88 |
89 | public void setEmail(String email) {
90 | this.email = email;
91 | }
92 |
93 | public String getPhone() {
94 | return phone;
95 | }
96 |
97 | public void setPhone(String phone) {
98 | this.phone = phone;
99 | }
100 |
101 | public File getAvatar() {
102 | return new File(avatarPath);
103 | }
104 |
105 | public void setAvatar(File avatar) {
106 | this.avatarPath = avatar.getAbsolutePath();
107 | }
108 |
109 | public String getAvatarPath() {
110 | return avatarPath;
111 | }
112 |
113 | public void setAvatarPath(String avatarPath) {
114 | this.avatarPath = avatarPath;
115 | }
116 |
117 | public boolean hasAvatar() {
118 | return avatarPath != null;
119 | }
120 |
121 | public ArrayList getFields() {
122 | if (fields == null) {
123 | fields = new ArrayList<>();
124 | }
125 | return fields;
126 | }
127 |
128 | public void setFields(ArrayList fields) {
129 | this.fields = fields;
130 | }
131 |
132 | public Date getCreatedAt() {
133 | return createdAt;
134 | }
135 |
136 | public void setCreatedAt(Date createdAt) {
137 | this.createdAt = createdAt;
138 | }
139 |
140 | public Date getUpdatedAt() {
141 | return updatedAt;
142 | }
143 |
144 | public void setUpdatedAt(Date updatedAt) {
145 | this.updatedAt = updatedAt;
146 | }
147 |
148 | @SuppressWarnings("unused")
149 | public boolean isUser() {
150 | return isUser;
151 | }
152 |
153 | public void setIsUser(boolean isUser) {
154 | this.isUser = isUser;
155 | }
156 |
157 | public boolean isValid() {
158 | return !getName().isEmpty();
159 | }
160 |
161 | public boolean matchQuery(String query) {
162 | String queryNormalized = query.toLowerCase().trim();
163 |
164 | List fieldsToMatch = new ArrayList<>(fields);
165 | fieldsToMatch.add(name);
166 | fieldsToMatch.add(email);
167 | fieldsToMatch.add(phone);
168 |
169 | for (String field : fieldsToMatch) {
170 | if (field != null && field.toLowerCase().contains(queryNormalized)) {
171 | return true;
172 | }
173 | }
174 |
175 | return false;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/FileHelper.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.ContentResolver;
4 | import android.content.Context;
5 | import android.net.Uri;
6 | import android.os.ParcelFileDescriptor;
7 | import android.util.Base64;
8 | import io.bloco.cardcase.common.Preconditions;
9 | import java.io.BufferedOutputStream;
10 | import java.io.File;
11 | import java.io.FileDescriptor;
12 | import java.io.FileInputStream;
13 | import java.io.FileNotFoundException;
14 | import java.io.FileOutputStream;
15 | import java.io.IOException;
16 | import java.io.RandomAccessFile;
17 | import java.nio.channels.FileChannel;
18 | import java.text.SimpleDateFormat;
19 | import java.util.Date;
20 | import java.util.Locale;
21 | import javax.inject.Inject;
22 | import javax.inject.Singleton;
23 | import timber.log.Timber;
24 |
25 | @Singleton public class FileHelper {
26 |
27 | private static final String IMAGE_PREFIX = "card_avatar";
28 | private static final String TEMP_PREFIX = "temp";
29 | private static final String IMAGE_SEPARATOR = "_";
30 | private static final String IMAGE_SUFFIX = ".jpg";
31 |
32 | private final Context context;
33 | private final ContentResolver contentResolver;
34 |
35 | @Inject public FileHelper(Context context) {
36 | this.context = context;
37 | this.contentResolver = context.getContentResolver();
38 | }
39 |
40 | public File createTemporaryFile() {
41 | try {
42 | return File.createTempFile(TEMP_PREFIX, IMAGE_SUFFIX, getCacheFolder());
43 | } catch (IOException e) {
44 | throw new RuntimeException(e);
45 | }
46 | }
47 |
48 | public File createFinalImageFile() {
49 | String timeStamp =
50 | new SimpleDateFormat("yyyyMMdd" + IMAGE_SEPARATOR + "HHmmssSSSS", Locale.US).format(new Date());
51 | String imageFileName = IMAGE_PREFIX + IMAGE_SEPARATOR + timeStamp;
52 |
53 | File file = new File(getStorageFolder(), imageFileName + IMAGE_SUFFIX);
54 | try {
55 | Preconditions.checkState(file.createNewFile(), "Could not create new file");
56 | } catch (IOException e) {
57 | throw new RuntimeException(e);
58 | }
59 | return file;
60 | }
61 |
62 | public File saveUriToFile(Uri uri, File file) {
63 | ParcelFileDescriptor parcelFileDes;
64 |
65 | try {
66 | parcelFileDes = contentResolver.openFileDescriptor(uri, "r");
67 | } catch (FileNotFoundException e) {
68 | Timber.e(e, "saveUriToFile");
69 | return null;
70 | }
71 |
72 | Preconditions.checkNotNull(parcelFileDes, "Could not open file URI");
73 | FileDescriptor fileDes = parcelFileDes.getFileDescriptor();
74 |
75 | FileInputStream inStream = new FileInputStream(fileDes);
76 | copyInputStreamToFile(inStream, file);
77 | return file;
78 | }
79 |
80 | public void saveBytesToFile(String avatarData, File file) {
81 | byte[] byteData = Base64.decode(avatarData, Base64.DEFAULT);
82 | try {
83 | BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
84 | bos.write(byteData);
85 | bos.flush();
86 | bos.close();
87 | } catch (IOException exception) {
88 | throw new RuntimeException(exception);
89 | }
90 | }
91 |
92 | public void delete(File file) {
93 | if (file.exists() && !file.delete()) {
94 | Timber.w("Could not delete file: %s", file);
95 | }
96 | }
97 |
98 | public String getBytesFromFile(File file) {
99 | try {
100 | try (RandomAccessFile f = new RandomAccessFile(file, "r")) {
101 | // Read file and return data
102 | byte[] data = new byte[(int) f.length()];
103 | f.readFully(data);
104 | return Base64.encodeToString(data, Base64.DEFAULT);
105 | }
106 | } catch (IOException exception) {
107 | throw new RuntimeException(exception);
108 | }
109 | }
110 |
111 | // PRIVATE
112 |
113 | private File getStorageFolder() {
114 | return context.getFilesDir();
115 | }
116 |
117 | private File getCacheFolder() {
118 | return context.getCacheDir();
119 | }
120 |
121 | private void copyInputStreamToFile(FileInputStream inStream, File dst) {
122 | try {
123 | FileOutputStream outStream = new FileOutputStream(dst);
124 | FileChannel inChannel = inStream.getChannel();
125 | FileChannel outChannel = outStream.getChannel();
126 | inChannel.transferTo(0, inChannel.size(), outChannel);
127 | inStream.close();
128 | outStream.close();
129 | } catch (IOException exception) {
130 | throw new RuntimeException(exception);
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/user/CropAvatarActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.user;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.app.ProgressDialog;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.graphics.Bitmap;
9 | import android.graphics.BitmapFactory;
10 | import android.os.AsyncTask;
11 | import android.os.Bundle;
12 |
13 | import com.lyft.android.scissors.CropView;
14 |
15 | import java.io.File;
16 | import java.util.concurrent.ExecutionException;
17 |
18 | import javax.inject.Inject;
19 |
20 | import butterknife.BindView;
21 | import butterknife.ButterKnife;
22 | import butterknife.OnClick;
23 | import io.bloco.cardcase.R;
24 | import io.bloco.cardcase.common.Preconditions;
25 | import io.bloco.cardcase.common.di.ActivityComponent;
26 | import io.bloco.cardcase.common.di.DaggerActivityComponent;
27 | import io.bloco.cardcase.presentation.BaseActivity;
28 |
29 | public class CropAvatarActivity extends BaseActivity {
30 |
31 | public static final int RESULT_ERROR = 999;
32 |
33 | @Inject
34 | AvatarPicker avatarPicker;
35 |
36 | @BindView(R.id.crop_avatar_view)
37 | CropView cropView;
38 |
39 | private ProgressDialog waitDialog;
40 | private CropView.Extensions cropExtensions;
41 |
42 | public static class Factory {
43 | public static Intent getIntent(Context context, String filePath) {
44 | Intent intent = new Intent(context, CropAvatarActivity.class);
45 | intent.putExtra(AvatarPicker.BundleArgs.FILE_PATH, filePath);
46 | return intent;
47 | }
48 | }
49 |
50 | @Override
51 | protected void onCreate(Bundle savedInstanceState) {
52 | super.onCreate(savedInstanceState);
53 | setContentView(R.layout.activity_crop);
54 |
55 | initializeInjectors();
56 |
57 | bindToolbar();
58 | toolbar.setTitle(R.string.crop_avatar);
59 | toolbar.setStartButton(R.drawable.ic_close, R.string.close, v -> finish());
60 |
61 | Intent intent = getIntent();
62 | String filePath = intent.getStringExtra(AvatarPicker.BundleArgs.FILE_PATH);
63 | Preconditions.checkNotNull(filePath, "File path not provided");
64 |
65 | Bitmap bitmap = BitmapFactory.decodeFile(filePath);
66 | cropView.setImageBitmap(bitmap);
67 | cropExtensions = cropView.extensions();
68 | }
69 |
70 | private void initializeInjectors() {
71 | ActivityComponent component = DaggerActivityComponent.builder()
72 | .applicationComponent(getApplicationComponent())
73 | .activityModule(getActivityModule())
74 | .build();
75 | component.inject(this);
76 |
77 | ButterKnife.bind(this);
78 | }
79 |
80 | @Override
81 | protected void onDestroy() {
82 | super.onDestroy();
83 | dismissDialog();
84 | }
85 |
86 | @SuppressLint("StaticFieldLeak")
87 | @OnClick(R.id.crop_avatar_fab)
88 | public void crop() {
89 | showWaitDialog();
90 |
91 | new AsyncTask() {
92 | private Exception exception;
93 |
94 | @Override
95 | protected File doInBackground(Void... params) {
96 | File avatarFile = avatarPicker.createNewAvatarFile();
97 | executeCrop(avatarFile);
98 | try {
99 | avatarPicker.resizeAvatar(avatarFile);
100 | } catch (AvatarPicker.ResizeError resizeError) {
101 | exception = resizeError;
102 | }
103 | return avatarFile;
104 | }
105 |
106 | @Override
107 | protected void onPostExecute(File avatarFile) {
108 | if (exception != null) {
109 | finishWithError();
110 | } else {
111 | finishWithResult(avatarFile);
112 | }
113 | dismissDialog();
114 | }
115 | }.execute();
116 | }
117 |
118 | private void showWaitDialog() {
119 | waitDialog =
120 | ProgressDialog.show(this, null, getString(R.string.crop_avatar_dialog), true, false);
121 | }
122 |
123 | private void dismissDialog() {
124 | if (waitDialog != null && waitDialog.isShowing()) {
125 | waitDialog.dismiss();
126 | waitDialog = null;
127 | }
128 | }
129 |
130 | private void finishWithResult(File avatarFile) {
131 | Intent intent = new Intent();
132 | intent.putExtra(AvatarPicker.BundleArgs.FILE_PATH, avatarFile.getAbsolutePath());
133 | setResult(Activity.RESULT_OK, intent);
134 | finish();
135 | }
136 |
137 | private void finishWithError() {
138 | Intent intent = new Intent();
139 | setResult(RESULT_ERROR, intent);
140 | finish();
141 | }
142 |
143 | private void executeCrop(File avatarFile) {
144 | try {
145 | cropExtensions.crop()
146 | .quality(AvatarPicker.IMAGE_QUALITY)
147 | .format(Bitmap.CompressFormat.JPEG)
148 | .into(avatarFile)
149 | .get();
150 | } catch (InterruptedException | ExecutionException e) {
151 | throw new RuntimeException(e);
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/bloco/cardcase/presentation/UserActivityOnEditTest.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation;
2 |
3 | import android.content.Intent;
4 |
5 | import androidx.test.espresso.ViewInteraction;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 | import androidx.test.rule.ActivityTestRule;
8 |
9 | import org.junit.After;
10 | import org.junit.Before;
11 | import org.junit.Rule;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 |
15 | import io.bloco.cardcase.R;
16 | import io.bloco.cardcase.data.models.Card;
17 | import io.bloco.cardcase.helpers.CardFactory;
18 | import io.bloco.cardcase.presentation.home.HomeActivity;
19 | import io.bloco.cardcase.presentation.user.UserActivity;
20 | import io.bloco.faker.Faker;
21 |
22 | import static androidx.test.espresso.Espresso.onView;
23 | import static androidx.test.espresso.action.ViewActions.clearText;
24 | import static androidx.test.espresso.action.ViewActions.click;
25 | import static androidx.test.espresso.action.ViewActions.typeText;
26 | import static androidx.test.espresso.assertion.ViewAssertions.matches;
27 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
28 | import static androidx.test.espresso.matcher.ViewMatchers.isFocusable;
29 | import static androidx.test.espresso.matcher.ViewMatchers.withId;
30 | import static androidx.test.espresso.matcher.ViewMatchers.withText;
31 | import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
32 | import static org.hamcrest.CoreMatchers.not;
33 |
34 | @RunWith(AndroidJUnit4.class)
35 | public class UserActivityOnEditTest {
36 |
37 | @Rule
38 | public final ActivityTestRule initialTestRule =
39 | new ActivityTestRule<>(HomeActivity.class);
40 |
41 | @Rule
42 | public final ActivityTestRule activityTestRule =
43 | new ActivityTestRule<>(UserActivity.class, true, false);
44 |
45 | private CardFactory cardFactory;
46 | private Card userCard;
47 |
48 | @Before
49 | public void setUp() {
50 | cardFactory = new CardFactory(initialTestRule);
51 | userCard = cardFactory.createUserCard();
52 |
53 | startActivity();
54 | }
55 |
56 | @Test
57 | public void testUserFields() {
58 | onNameField().check(matches(withText(userCard.getName())));
59 | onEmailField().check(matches(withText(userCard.getEmail())));
60 | onPhoneField().check(matches(withText(userCard.getPhone())));
61 | }
62 |
63 | @Test
64 | public void testSwitchModes() {
65 | assertViewMode();
66 | startEdit();
67 | assertEditMode();
68 | cancelEdit();
69 | assertViewMode();
70 | }
71 |
72 | @Test
73 | public void testEditField() {
74 | String newName = new Faker().name.name();
75 | startEdit();
76 | onNameField().perform(clearText()).perform(typeText(newName));
77 | onDoneButton().perform(click());
78 |
79 | // Restart activity
80 | activityTestRule.getActivity().finish();
81 | startActivity();
82 |
83 | onNameField().check(matches(withText(newName)));
84 | }
85 |
86 | @After
87 | public void tearDown() {
88 | if (cardFactory != null) {
89 | cardFactory.clear();
90 | }
91 | }
92 |
93 | private void startActivity() {
94 | Intent intent = UserActivity.Factory.getIntent(getInstrumentation().getTargetContext());
95 | activityTestRule.launchActivity(intent);
96 | }
97 |
98 | private void startEdit() {
99 | onEditButton().perform(click());
100 | }
101 |
102 | private void cancelEdit() {
103 | onCancelButton().perform(click());
104 | }
105 |
106 | private void assertViewMode() {
107 | onNameField().check(matches(not(isFocusable())));
108 | onEmailField().check(matches(not(isFocusable())));
109 | onPhoneField().check(matches(not(isFocusable())));
110 | onEditButton().check(matches(isDisplayed()));
111 | onDoneButton().check(matches(not(isDisplayed())));
112 | onCancelButton().check(matches(not(isDisplayed())));
113 | }
114 |
115 | private void assertEditMode() {
116 | onNameField().check(matches(isFocusable()));
117 | onEmailField().check(matches(isFocusable()));
118 | onPhoneField().check(matches(isFocusable()));
119 | onEditButton().check(matches(not(isDisplayed())));
120 | onCancelButton().check(matches(isDisplayed()));
121 | }
122 |
123 | private ViewInteraction onNameField() {
124 | return onView(withId(R.id.card_name));
125 | }
126 |
127 | private ViewInteraction onEmailField() {
128 | return onView(withId(R.id.card_email));
129 | }
130 |
131 | private ViewInteraction onPhoneField() {
132 | return onView(withId(R.id.card_phone));
133 | }
134 |
135 | private ViewInteraction onDoneButton() {
136 | return onView(withId(R.id.user_done));
137 | }
138 |
139 | private ViewInteraction onEditButton() {
140 | return onView(withId(R.id.user_edit));
141 | }
142 |
143 | private ViewInteraction onCancelButton() {
144 | return onView(withId(R.id.toolbar_icon_start));
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
28 |
29 |
35 |
36 |
43 |
44 |
50 |
51 |
60 |
61 |
67 |
68 |
74 |
75 |
82 |
83 |
91 |
92 |
97 |
98 |
102 |
103 |
109 |
110 |
117 |
118 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/user/AvatarPicker.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.user;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.res.Resources;
7 | import android.graphics.Bitmap;
8 | import android.graphics.BitmapFactory;
9 | import android.hardware.Camera;
10 | import android.net.Uri;
11 | import android.provider.MediaStore;
12 |
13 | import androidx.core.content.FileProvider;
14 |
15 | import java.io.File;
16 | import java.io.FileOutputStream;
17 | import java.io.IOException;
18 |
19 | import javax.inject.Inject;
20 | import javax.inject.Singleton;
21 |
22 | import io.bloco.cardcase.BuildConfig;
23 | import io.bloco.cardcase.R;
24 | import io.bloco.cardcase.common.Preconditions;
25 | import io.bloco.cardcase.presentation.common.FileHelper;
26 |
27 | @Singleton
28 | public class AvatarPicker {
29 |
30 | class ReceivingError extends Exception {
31 | }
32 |
33 | class ResizeError extends Exception {
34 | }
35 |
36 | private static final int AVATAR_REQUEST_CODE = 21;
37 | private static final int CROP_REQUEST_CODE = 31;
38 | public static final int IMAGE_QUALITY = 50;
39 | private static final int AVATAR_SIZE = 512;
40 |
41 | private final Context context;
42 | private final Resources resources;
43 | private final FileHelper fileHelper;
44 | private File tempFile;
45 |
46 | public static class BundleArgs {
47 | public static final String FILE_PATH = "file_path";
48 | }
49 |
50 | @Inject
51 | public AvatarPicker(Context context, Resources resources, FileHelper fileHelper) {
52 | this.context = context;
53 | this.resources = resources;
54 | this.fileHelper = fileHelper;
55 | }
56 |
57 | public void startPicker(Activity activity) {
58 | tempFile = createTempFile();
59 |
60 | Intent pickIntent = new Intent();
61 | pickIntent.setType("image/*");
62 | pickIntent.setAction(Intent.ACTION_OPEN_DOCUMENT);
63 | pickIntent.addCategory(Intent.CATEGORY_OPENABLE);
64 |
65 | Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
66 | //noinspection deprecation
67 | takePhotoIntent.putExtra("android.intent.extras.CAMERA_FACING",
68 | Camera.CameraInfo.CAMERA_FACING_FRONT);
69 | takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, getTempFileUri());
70 | takePhotoIntent.addFlags(
71 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
72 |
73 | String pickTitle = resources.getString(R.string.avatar_picker);
74 | Intent chooserIntent = Intent.createChooser(pickIntent, pickTitle);
75 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{takePhotoIntent});
76 |
77 | activity.startActivityForResult(chooserIntent, AVATAR_REQUEST_CODE);
78 | }
79 |
80 | public File processActivityResult(int requestCode, int resultCode, Intent data,
81 | Activity activity) throws ReceivingError, ResizeError {
82 | // Crop
83 | if (requestCode == CROP_REQUEST_CODE) {
84 | clearTempFile();
85 | if (resultCode == Activity.RESULT_OK) {
86 | String croppedAvatarPath = data.getStringExtra("file_path");
87 | Preconditions.checkNotNull(croppedAvatarPath, "Empty cropped avatar");
88 | return new File(croppedAvatarPath);
89 | } else if (resultCode == CropAvatarActivity.RESULT_ERROR) {
90 | throw new ResizeError();
91 | }
92 | return null;
93 | }
94 |
95 | if (requestCode != AVATAR_REQUEST_CODE) {
96 | return null;
97 | }
98 |
99 | // Picker (camera or gallery)
100 |
101 | if (resultCode != Activity.RESULT_OK) {
102 | // There was an error, let's cancel
103 | clearTempFile();
104 | return null;
105 | }
106 |
107 | // Check if we still have the tempFile
108 | if (tempFile == null) {
109 | throw new ReceivingError();
110 | }
111 |
112 | // Gallery
113 | if (data != null) {
114 | Uri imageUri = data.getData();
115 | if (imageUri != null) {
116 | tempFile = fileHelper.saveUriToFile(imageUri, tempFile);
117 | if (tempFile == null) {
118 | throw new ReceivingError();
119 | }
120 | }
121 | }
122 |
123 | // Camera: nothing to do, the photo is already on tempFile
124 |
125 | cropPhoto(activity);
126 |
127 | return null;
128 | }
129 |
130 | public File createNewAvatarFile() {
131 | return fileHelper.createFinalImageFile();
132 | }
133 |
134 | public void resizeAvatar(File avatarFile) throws ResizeError {
135 | Bitmap originalBitmap = BitmapFactory.decodeFile(avatarFile.getAbsolutePath());
136 | if (originalBitmap == null) {
137 | throw new ResizeError();
138 | }
139 |
140 | Bitmap bitmap = Bitmap.createScaledBitmap(originalBitmap, AVATAR_SIZE, AVATAR_SIZE, false);
141 | FileOutputStream fos;
142 | try {
143 | fos = new FileOutputStream(avatarFile);
144 | bitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY, fos);
145 | fos.close();
146 | } catch (IOException exception) {
147 | throw new RuntimeException(exception);
148 | }
149 | }
150 |
151 | // Helpers
152 |
153 | private File createTempFile() {
154 | if (tempFile != null) {
155 | clearTempFile();
156 | }
157 | return fileHelper.createTemporaryFile();
158 | }
159 |
160 | private void clearTempFile() {
161 | if (tempFile != null && tempFile.exists()) {
162 | fileHelper.delete(tempFile);
163 | tempFile = null;
164 | }
165 | }
166 |
167 | private void cropPhoto(Activity activity) {
168 | String tempFilePath = tempFile.getAbsolutePath();
169 | Intent cropIntent = CropAvatarActivity.Factory.getIntent(activity, tempFilePath);
170 | activity.startActivityForResult(cropIntent, CROP_REQUEST_CODE);
171 | }
172 |
173 | private Uri getTempFileUri() {
174 | return FileProvider.getUriForFile(context, BuildConfig.FILES_AUTORITY, tempFile);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/user/UserActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.user;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.transition.Transition;
7 | import android.transition.TransitionInflater;
8 | import android.view.ViewGroup;
9 |
10 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
11 |
12 | import java.io.File;
13 |
14 | import javax.inject.Inject;
15 |
16 | import butterknife.BindView;
17 | import butterknife.ButterKnife;
18 | import butterknife.OnClick;
19 | import io.bloco.cardcase.R;
20 | import io.bloco.cardcase.common.di.ActivityComponent;
21 | import io.bloco.cardcase.common.di.DaggerActivityComponent;
22 | import io.bloco.cardcase.data.models.Card;
23 | import io.bloco.cardcase.presentation.BaseActivity;
24 | import io.bloco.cardcase.presentation.common.CardInfoView;
25 | import io.bloco.cardcase.presentation.common.ErrorDisplayer;
26 | import io.bloco.cardcase.presentation.common.ImageLoader;
27 | import io.bloco.cardcase.presentation.home.HomeActivity;
28 |
29 | @SuppressWarnings("unused")
30 | public class UserActivity extends BaseActivity
31 | implements UserContract.View, CardInfoView.CardEditListener {
32 |
33 | @Inject
34 | UserContract.Presenter presenter;
35 | @Inject
36 | ImageLoader imageLoader;
37 | @Inject
38 | AvatarPicker avatarPicker;
39 | @Inject
40 | ErrorDisplayer errorDisplayer;
41 |
42 | @BindView(R.id.user_layout)
43 | ViewGroup rootLayout;
44 | @BindView(R.id.user_card)
45 | CardInfoView cardView;
46 | @BindView(R.id.user_edit)
47 | FloatingActionButton edit;
48 | @BindView(R.id.user_done)
49 | FloatingActionButton done;
50 |
51 | public static class Factory {
52 | static class BundleArgs {
53 | private static final String ONBOARDING = "onboarding";
54 | }
55 |
56 | public static Intent getIntent(Context context) {
57 | return new Intent(context, UserActivity.class);
58 | }
59 |
60 | public static Intent getOnboardingIntent(Context context) {
61 | Intent intent = getIntent(context);
62 | intent.putExtra(BundleArgs.ONBOARDING, true);
63 | return intent;
64 | }
65 | }
66 |
67 | @Override
68 | protected void onCreate(Bundle savedInstanceState) {
69 | super.onCreate(savedInstanceState);
70 | setContentView(R.layout.activity_user);
71 |
72 | initializeInjectors();
73 |
74 | bindToolbar();
75 | toolbar.setTitle(R.string.user_card);
76 |
77 | Intent intent = getIntent();
78 | boolean onboarding = intent.getBooleanExtra(Factory.BundleArgs.ONBOARDING, false);
79 |
80 | cardView.setEditListener(this);
81 |
82 | presenter.start(this, onboarding);
83 |
84 | Transition slideEnd = TransitionInflater.from(this).inflateTransition(R.transition.slide_end);
85 | Transition slideStart =
86 | TransitionInflater.from(this).inflateTransition(R.transition.slide_start);
87 | getWindow().setEnterTransition(slideStart);
88 | getWindow().setExitTransition(slideEnd);
89 | }
90 |
91 | private void initializeInjectors() {
92 | ActivityComponent component = DaggerActivityComponent.builder()
93 | .applicationComponent(getApplicationComponent())
94 | .activityModule(getActivityModule())
95 | .build();
96 | component.inject(this);
97 |
98 | ButterKnife.bind(this);
99 | }
100 |
101 | @Override
102 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
103 | super.onActivityResult(requestCode, resultCode, data);
104 | File avatarFile;
105 |
106 | try {
107 | avatarFile = avatarPicker.processActivityResult(requestCode, resultCode, data, this);
108 | } catch (AvatarPicker.ReceivingError avatarReceivingError) {
109 | errorDisplayer.show(rootLayout, R.string.error_avatar_receiving);
110 | return;
111 | } catch (AvatarPicker.ResizeError resizeError) {
112 | errorDisplayer.show(rootLayout, R.string.error_avatar_resize);
113 | return;
114 | }
115 |
116 | if (avatarFile != null) {
117 | cardView.setAvatar(avatarFile.getAbsolutePath());
118 | presenter.onCardChanged(cardView.getCard());
119 | }
120 | }
121 |
122 | @OnClick(R.id.user_edit)
123 | public void onEditClicked() {
124 | presenter.clickedEdit();
125 | }
126 |
127 | @OnClick(R.id.user_done)
128 | public void onDoneClicked() {
129 | presenter.clickedDone(cardView.getCard());
130 | }
131 |
132 | @Override
133 | public void showUser(Card userCard) {
134 | cardView.setCard(userCard);
135 | }
136 |
137 | @Override
138 | public void showBack() {
139 | toolbar.setEndButton(R.drawable.ic_back_right, R.string.back, v -> presenter.clickedBack());
140 | }
141 |
142 | @Override
143 | public void hideBack() {
144 | toolbar.removeEndButton();
145 | }
146 |
147 | @Override
148 | public void showCancel() {
149 | toolbar.setStartButton(R.drawable.ic_close, R.string.back, v -> presenter.clickedCancel());
150 | }
151 |
152 | @Override
153 | public void hideCancel() {
154 | toolbar.removeStartButton();
155 | }
156 |
157 | @Override
158 | public void showEditButton() {
159 | edit.show();
160 | }
161 |
162 | @Override
163 | public void hideEditButton() {
164 | edit.hide();
165 | }
166 |
167 | @Override
168 | public void showDoneButton() {
169 | done.show();
170 | }
171 |
172 | @Override
173 | public void hideDoneButton() {
174 | done.hide();
175 | }
176 |
177 | @Override
178 | public void enableEditMode() {
179 | cardView.enableEditMode();
180 | }
181 |
182 | @Override
183 | public void disabledEditMode() {
184 | cardView.disabledEditMode();
185 | }
186 |
187 | @Override
188 | public void openHome() {
189 | Intent intent = HomeActivity.Factory.getIntent(this);
190 | startActivityWithAnimation(intent);
191 | }
192 |
193 | @Override
194 | public void close() {
195 | finishWithAnimation();
196 | }
197 |
198 | @Override
199 | public void onPickAvatar() {
200 | avatarPicker.startPicker(this);
201 | }
202 |
203 | @Override
204 | public void onChange(Card updatedCard) {
205 | presenter.onCardChanged(updatedCard);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/home/HomeActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.home;
2 |
3 | import android.animation.Animator;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.transition.Transition;
8 | import android.transition.TransitionInflater;
9 | import android.view.View;
10 | import android.view.ViewAnimationUtils;
11 | import android.view.ViewGroup;
12 |
13 | import androidx.recyclerview.widget.LinearLayoutManager;
14 | import androidx.recyclerview.widget.RecyclerView;
15 |
16 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
17 |
18 | import java.util.List;
19 |
20 | import javax.inject.Inject;
21 |
22 | import butterknife.BindView;
23 | import butterknife.ButterKnife;
24 | import butterknife.OnClick;
25 | import io.bloco.cardcase.R;
26 | import io.bloco.cardcase.common.di.ActivityComponent;
27 | import io.bloco.cardcase.common.di.DaggerActivityComponent;
28 | import io.bloco.cardcase.data.models.Card;
29 | import io.bloco.cardcase.presentation.BaseActivity;
30 | import io.bloco.cardcase.presentation.common.CardAdapter;
31 | import io.bloco.cardcase.presentation.common.SearchToolbar;
32 | import io.bloco.cardcase.presentation.exchange.ExchangeActivity;
33 | import io.bloco.cardcase.presentation.user.UserActivity;
34 | import io.bloco.cardcase.presentation.welcome.WelcomeActivity;
35 | import timber.log.Timber;
36 |
37 | @SuppressWarnings("ALL")
38 | public class HomeActivity extends BaseActivity
39 | implements HomeContract.View, SearchToolbar.SearchListener {
40 |
41 | @Inject HomeContract.Presenter presenter;
42 | @Inject CardAdapter cardAdapter;
43 |
44 | @BindView(R.id.toolbar_search)
45 | SearchToolbar searchToolbar;
46 | @BindView(R.id.home_empty)
47 | ViewGroup homeEmpty;
48 | @BindView(R.id.home_search_empty)
49 | ViewGroup homeSearchEmpty;
50 | @BindView(R.id.home_cards)
51 | RecyclerView cardsView;
52 | @BindView(R.id.home_exchange)
53 | FloatingActionButton exchangeButton;
54 | @BindView(R.id.home_transition_overlay)
55 | View transitionOverlay;
56 |
57 | public static class Factory {
58 | public static Intent getIntent(Context context) {
59 | return new Intent(context, HomeActivity.class);
60 | }
61 | }
62 |
63 | @Override
64 | protected void onCreate(Bundle savedInstanceState) {
65 | super.onCreate(savedInstanceState);
66 | setContentView(R.layout.activity_home);
67 |
68 | initializeInjectors();
69 |
70 | bindToolbar();
71 | toolbar.setTitle(R.string.cards_received);
72 | toolbar.setStartButton(R.drawable.ic_user, R.string.user_card, v -> presenter.clickedUser());
73 |
74 | Transition slideEnd = TransitionInflater.from(this).inflateTransition(R.transition.slide_end);
75 | getWindow().setEnterTransition(slideEnd);
76 | }
77 |
78 | private void initializeInjectors() {
79 | ActivityComponent component = DaggerActivityComponent.builder()
80 | .applicationComponent(getApplicationComponent())
81 | .activityModule(getActivityModule())
82 | .build();
83 | component.inject(this);
84 |
85 | ButterKnife.bind(this);
86 | }
87 |
88 | @Override
89 | protected void onStart() {
90 | super.onStart();
91 | transitionOverlay.setVisibility(View.GONE);
92 | presenter.start(this);
93 | }
94 |
95 | @Override
96 | public void onBackPressed() {
97 | if (searchToolbar.getVisibility() == View.VISIBLE) {
98 | presenter.clickedCloseSearch();
99 | } else {
100 | finish();
101 | }
102 | }
103 |
104 | @OnClick(R.id.home_exchange)
105 | public void onClickedExchange() {
106 | presenter.clickedExchange();
107 | }
108 |
109 | @Override
110 | public void showEmpty() {
111 | homeEmpty.setVisibility(View.VISIBLE);
112 | cardsView.setVisibility(View.GONE);
113 | toolbar.removeEndButton(); // Hide Search
114 | }
115 |
116 | @Override
117 | public void showCards(final List cards) {
118 | cardAdapter.setCards(cards);
119 | cardsView.setAdapter(cardAdapter);
120 | RecyclerView.LayoutManager layoutManager =
121 | new LinearLayoutManager(HomeActivity.this, LinearLayoutManager.VERTICAL, false);
122 | cardsView.setLayoutManager(layoutManager);
123 | cardsView.setVisibility(View.VISIBLE);
124 | homeEmpty.setVisibility(View.GONE);
125 |
126 | // Show Search
127 | toolbar.setEndButton(R.drawable.ic_search, R.string.search, v -> presenter.clickedSearch());
128 | }
129 |
130 | @Override
131 | public void hideEmptySearchResult() {
132 | homeSearchEmpty.setVisibility(View.GONE);
133 | }
134 |
135 | @Override
136 | public void showEmptySearchResult() {
137 | homeSearchEmpty.setVisibility(View.VISIBLE);
138 | }
139 |
140 | @Override
141 | public void openOnboarding() {
142 | Intent intent = WelcomeActivity.Factory.getIntent(HomeActivity.this);
143 | startActivity(intent);
144 | finish();
145 | }
146 |
147 | @Override
148 | public void openUser() {
149 | Intent intent = UserActivity.Factory.getIntent(this);
150 | startActivityWithAnimation(intent);
151 | }
152 |
153 | @Override
154 | public void openExchange() {
155 | animateExchangeOverlay();
156 | Intent intent = ExchangeActivity.Factory.getIntent(this);
157 | startActivityWithAnimation(intent);
158 | }
159 |
160 | @Override
161 | public void openSearch() {
162 | toolbar.setVisibility(View.GONE);
163 | searchToolbar.setVisibility(View.VISIBLE);
164 | searchToolbar.focus();
165 | searchToolbar.setListener(this);
166 | }
167 |
168 | @Override
169 | public void closeSearch() {
170 | toolbar.setVisibility(View.VISIBLE);
171 | searchToolbar.setVisibility(View.GONE);
172 | searchToolbar.clear();
173 | searchToolbar.setListener(null);
174 | }
175 |
176 | @Override
177 | public void onSearchClosed() {
178 | presenter.clickedCloseSearch();
179 | }
180 |
181 | @Override
182 | public void onSearchQuery(String query) {
183 | Timber.i("onSearchQuery");
184 | presenter.searchEntered(query);
185 | }
186 |
187 | private void animateExchangeOverlay() {
188 | int cx = (int) exchangeButton.getX() + exchangeButton.getWidth() / 2;
189 | int cy = (int) exchangeButton.getY() + exchangeButton.getHeight() / 2;
190 |
191 | View rootView = findViewById(android.R.id.content);
192 | float finalRadius = Math.max(rootView.getWidth(), rootView.getHeight());
193 |
194 | // create the animator for this view (the start radius is zero)
195 | Animator circularReveal =
196 | ViewAnimationUtils.createCircularReveal(transitionOverlay, cx, cy, 0, finalRadius);
197 | circularReveal.setDuration(getResources().getInteger(R.integer.animation_duration));
198 |
199 | // make the view visible and start the animation
200 | transitionOverlay.setVisibility(View.VISIBLE);
201 | circularReveal.start();
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/exchange/ExchangeActivity.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.exchange;
2 |
3 | import android.animation.Animator;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.text.Html;
8 | import android.view.View;
9 | import android.view.ViewAnimationUtils;
10 | import android.view.ViewGroup;
11 | import android.widget.Button;
12 |
13 | import androidx.annotation.StringRes;
14 | import androidx.appcompat.app.AlertDialog;
15 | import androidx.recyclerview.widget.DefaultItemAnimator;
16 | import androidx.recyclerview.widget.LinearLayoutManager;
17 | import androidx.recyclerview.widget.RecyclerView;
18 |
19 | import com.google.android.gms.appinvite.AppInviteInvitation;
20 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
21 |
22 | import java.util.List;
23 |
24 | import javax.inject.Inject;
25 |
26 | import butterknife.BindView;
27 | import butterknife.ButterKnife;
28 | import butterknife.OnClick;
29 | import io.bloco.cardcase.R;
30 | import io.bloco.cardcase.common.di.ActivityComponent;
31 | import io.bloco.cardcase.common.di.DaggerActivityComponent;
32 | import io.bloco.cardcase.data.models.Card;
33 | import io.bloco.cardcase.presentation.BaseActivity;
34 | import io.bloco.cardcase.presentation.common.CardAdapter;
35 |
36 | @SuppressWarnings("ALL")
37 | public class ExchangeActivity extends BaseActivity implements ExchangeContract.View {
38 |
39 | private static final int REQUEST_INVITE = 145;
40 |
41 | @Inject
42 | ExchangeContract.Presenter presenter;
43 | @SuppressWarnings("unused")
44 | @Inject
45 | CardAdapter cardAdapter;
46 |
47 | @BindView(R.id.exchange_container)
48 | View overlay;
49 | @BindView(R.id.exchange_empty)
50 | ViewGroup emptyView;
51 | @BindView(R.id.exchange_cards)
52 | ViewGroup cardsView;
53 | @BindView(R.id.exchange_cards_list)
54 | RecyclerView cardsListView;
55 | @BindView(R.id.exchange_done)
56 | FloatingActionButton done;
57 | @BindView(R.id.exchange_invite)
58 | Button invite;
59 | @BindView(R.id.exchange_loader)
60 | View loader;
61 |
62 | private AlertDialog errorDialog;
63 | private AlertDialog confirmDialog;
64 |
65 | public static class Factory {
66 | public static Intent getIntent(Context context) {
67 | return new Intent(context, ExchangeActivity.class);
68 | }
69 | }
70 |
71 | @Override
72 | protected void onCreate(Bundle savedInstanceState) {
73 | super.onCreate(savedInstanceState);
74 | setContentView(R.layout.activity_exchange);
75 |
76 | initializeInjectors();
77 |
78 | bindToolbar();
79 | toolbar.setTitle(R.string.exchange);
80 | toolbar.setStartButton(R.drawable.ic_close, R.string.close, v -> presenter.clickedClose());
81 |
82 | invite.setText(Html.fromHtml(getString(R.string.exchange_invite)));
83 |
84 | cardAdapter.showLoader();
85 | }
86 |
87 | private void initializeInjectors() {
88 | ActivityComponent component = DaggerActivityComponent.builder()
89 | .applicationComponent(getApplicationComponent())
90 | .activityModule(getActivityModule())
91 | .build();
92 | component.inject(this);
93 |
94 | ButterKnife.bind(this);
95 | }
96 |
97 | @Override
98 | protected void onStart() {
99 | super.onStart();
100 | presenter.start(this);
101 | }
102 |
103 | @Override
104 | protected void onStop() {
105 | super.onStop();
106 | presenter.stop();
107 | }
108 |
109 | @Override
110 | protected void onDestroy() {
111 | super.onDestroy();
112 | dismissDialogs();
113 | }
114 |
115 | @Override
116 | public void onBackPressed() {
117 | presenter.clickedClose();
118 | }
119 |
120 | @OnClick(R.id.exchange_done)
121 | public void onClickedDone() {
122 | presenter.clickedDone();
123 | }
124 |
125 | @OnClick(R.id.exchange_invite)
126 | public void onClickedInvite() {
127 | presenter.clickedInvite();
128 | }
129 |
130 | @Override
131 | public void setupCards(List receivedCards) {
132 | cardAdapter.setCards(receivedCards);
133 | cardsListView.setAdapter(cardAdapter);
134 | cardsListView.setItemAnimator(new DefaultItemAnimator());
135 | cardsListView.setLayoutManager(
136 | new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
137 | }
138 |
139 | @Override
140 | public void notifyCardAdded() {
141 | runOnUiThread(() -> cardAdapter.notifyItemInserted(cardAdapter.getItemCount() - 1));
142 | }
143 |
144 | @Override
145 | public void showCards() {
146 | runOnUiThread(() -> {
147 | emptyView.setVisibility(View.GONE);
148 | cardsView.setVisibility(View.VISIBLE);
149 | });
150 | }
151 |
152 | @Override
153 | public void showDone() {
154 | runOnUiThread(() -> done.show());
155 | }
156 |
157 | @Override
158 | public void showError(@StringRes int messageRes) {
159 | final String message = getString(messageRes);
160 | runOnUiThread(() -> errorDialog = new AlertDialog.Builder(ExchangeActivity.this).setTitle(R.string.error)
161 | .setMessage(message)
162 | .setNeutralButton(R.string.ok, (dialog, which) -> close())
163 | .setOnDismissListener(dialog -> close())
164 | .show());
165 | }
166 |
167 | @Override
168 | public void openInvite() {
169 | Intent intent =
170 | new AppInviteInvitation.IntentBuilder(getString(R.string.invite_title)).setMessage(
171 | getString(R.string.invite_message))
172 | .setCallToActionText(getString(R.string.invite_button))
173 | .build();
174 | startActivityForResult(intent, REQUEST_INVITE);
175 | }
176 |
177 | @Override
178 | public void closeWithConfirmation() {
179 | runOnUiThread(() -> confirmDialog = new AlertDialog.Builder(ExchangeActivity.this).setMessage(
180 | R.string.exchange_close_confirm)
181 | .setNegativeButton(R.string.cancel, null)
182 | .setPositiveButton(R.string.exchange_close_yes, (dialog, which) -> close())
183 | .show());
184 | }
185 |
186 | @Override
187 | public void close() {
188 | runOnUiThread(this::closeActivityWithAnimation);
189 | }
190 |
191 | private void dismissDialogs() {
192 | if (errorDialog != null && errorDialog.isShowing()) {
193 | errorDialog.dismiss();
194 | errorDialog = null;
195 | }
196 | if (confirmDialog != null && confirmDialog.isShowing()) {
197 | confirmDialog.dismiss();
198 | confirmDialog = null;
199 | }
200 | }
201 |
202 | private void closeActivityWithAnimation() {
203 | if (!overlay.isAttachedToWindow()) {
204 | finishWithAnimation();
205 | return;
206 | }
207 |
208 | float fabMargin = getResources().getDimension(R.dimen.fab_margin);
209 | float fabSize = getResources().getDimension(R.dimen.fab_size);
210 | float fabOffset = fabMargin + fabSize / 2;
211 | int cx = Math.round(overlay.getWidth() - fabOffset);
212 | int cy = Math.round(
213 | overlay.getHeight() - fabOffset + getResources().getDimension(R.dimen.nav_bar_subtraction));
214 |
215 | float startRadius = Math.max(overlay.getWidth(), overlay.getHeight());
216 |
217 | int duration = getResources().getInteger(R.integer.animation_duration);
218 |
219 | Animator circularReveal =
220 | ViewAnimationUtils.createCircularReveal(overlay, cx, cy, startRadius, 0);
221 | circularReveal.setDuration(duration);
222 | circularReveal.start();
223 |
224 | overlay.postDelayed(() -> {
225 | overlay.setVisibility(View.GONE);
226 | finishWithAnimation();
227 | }, duration);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/app/src/main/java/io/bloco/cardcase/presentation/common/CardInfoView.java:
--------------------------------------------------------------------------------
1 | package io.bloco.cardcase.presentation.common;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.text.format.DateUtils;
7 | import android.util.AttributeSet;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.view.inputmethod.InputMethodManager;
11 | import android.widget.EditText;
12 | import android.widget.FrameLayout;
13 | import android.widget.ImageView;
14 | import android.widget.TextView;
15 |
16 | import java.util.ArrayList;
17 |
18 | import javax.inject.Inject;
19 |
20 | import butterknife.BindView;
21 | import butterknife.ButterKnife;
22 | import butterknife.OnClick;
23 | import io.bloco.cardcase.AndroidApplication;
24 | import io.bloco.cardcase.R;
25 | import io.bloco.cardcase.data.models.Card;
26 | import io.bloco.cardcase.presentation.home.SimpleTextWatcher;
27 |
28 | @SuppressWarnings("ALL")
29 | public class CardInfoView extends FrameLayout {
30 |
31 | @Inject
32 | ImageLoader imageLoader;
33 |
34 | @BindView(R.id.card_avatar)
35 | ImageView avatar;
36 | @BindView(R.id.card_avatar_edit_overlay)
37 | View avatarEditOverlay;
38 | @BindView(R.id.card_name)
39 | EditText name;
40 | @BindView(R.id.card_email)
41 | EditText email;
42 | @BindView(R.id.card_phone)
43 | EditText phone;
44 | @BindView(R.id.card_fields)
45 | ViewGroup fields;
46 | @BindView(R.id.card_time)
47 | TextView time;
48 |
49 | private Card card;
50 | private CardEditListener editListener;
51 | private final FieldTextWatcher fieldTextWatcher;
52 | private boolean editMode;
53 |
54 | public CardInfoView(Context context, AttributeSet attrs) {
55 | super(context, attrs);
56 | ((AndroidApplication) context.getApplicationContext()).getApplicationComponent().inject(this);
57 |
58 | inflate(context, R.layout.view_card_info, this);
59 | ButterKnife.bind(this);
60 |
61 | fieldTextWatcher = new FieldTextWatcher();
62 | name.addTextChangedListener(fieldTextWatcher);
63 | email.addTextChangedListener(fieldTextWatcher);
64 | phone.addTextChangedListener(fieldTextWatcher);
65 |
66 | disabledEditMode();
67 | }
68 |
69 | @OnClick(R.id.card_email)
70 | public void clickEmail() {
71 | if (editMode) {
72 | return;
73 | }
74 |
75 | String uri = "mailto:" + card.getEmail();
76 | Intent intent = new Intent(Intent.ACTION_SENDTO);
77 | intent.setDataAndType(Uri.parse(uri), "text/plain");
78 |
79 | Intent chooser = Intent.createChooser(intent, getResources().getString(R.string.send_email));
80 | getContext().startActivity(chooser);
81 | }
82 |
83 | @OnClick(R.id.card_phone)
84 | public void clickPhone() {
85 | if (editMode) {
86 | return;
87 | }
88 |
89 | String uri = "tel:" + card.getPhone();
90 | Intent intent = new Intent(Intent.ACTION_DIAL);
91 | intent.setData(Uri.parse(uri));
92 | getContext().startActivity(intent);
93 | }
94 |
95 | @OnClick(R.id.card_avatar)
96 | public void pickAvatar() {
97 | if (editMode && editListener != null) {
98 | editListener.onPickAvatar();
99 | }
100 | }
101 |
102 | public void setAvatar(String avatarPath) {
103 | card.setAvatarPath(avatarPath);
104 | if (card.hasAvatar()) {
105 | imageLoader.loadAvatar(avatar, card.getAvatarPath());
106 | }
107 | }
108 |
109 | public void showTime() {
110 | time.setVisibility(VISIBLE);
111 | }
112 |
113 | public Card getCard() {
114 | card.setName(name.getText().toString().trim());
115 | card.setEmail(email.getText().toString().trim());
116 | card.setPhone(phone.getText().toString().trim());
117 |
118 | int fieldsCount = fields.getChildCount();
119 | ArrayList fieldValues = new ArrayList<>(fieldsCount);
120 | for (int i = 0; i < fieldsCount; i++) {
121 | EditText field = (EditText) fields.getChildAt(i);
122 | String fieldValue = getEditTextValue(field);
123 | if (!fieldValue.isEmpty()) {
124 | fieldValues.add(fieldValue);
125 | }
126 | }
127 | card.setFields(fieldValues);
128 |
129 | return card;
130 | }
131 |
132 | public void setCard(Card card) {
133 | this.card = card.copy();
134 |
135 | name.setText(card.getName());
136 | email.setText(card.getEmail());
137 | phone.setText(card.getPhone());
138 |
139 | setAvatar(card.getAvatarPath());
140 |
141 | fields.removeAllViews();
142 | for (String fieldValue : card.getFields()) {
143 | addNewField(fieldValue);
144 | }
145 |
146 | if (card.getUpdatedAt() != null) {
147 | long timestamp = card.getUpdatedAt().getTime();
148 | String timeCaption = DateUtils.getRelativeTimeSpanString(timestamp).toString();
149 | String timePhrase = getResources().getString(R.string.card_time, timeCaption);
150 | time.setText(timePhrase);
151 | }
152 |
153 | if (editMode) {
154 | enableEditMode();
155 | } else {
156 | disabledEditMode();
157 | }
158 | }
159 |
160 | public void setEditListener(CardEditListener editListener) {
161 | this.editListener = editListener;
162 | }
163 |
164 | public void enableEditMode() {
165 | editMode = true;
166 |
167 | enableEditText(name);
168 | enableEditText(email);
169 | enableEditText(phone);
170 |
171 | if (card == null || !card.hasAvatar()) {
172 | avatar.setImageResource(R.drawable.avatar_edit);
173 | }
174 |
175 | for (int i = 0, count = fields.getChildCount(); i < count; i++) {
176 | EditText field = (EditText) fields.getChildAt(i);
177 | enableEditText(field);
178 | }
179 | addNewField("");
180 |
181 | avatarEditOverlay.setVisibility(View.VISIBLE);
182 | }
183 |
184 | public void disabledEditMode() {
185 | editMode = false;
186 |
187 | disabledEditText(name);
188 | disabledEditText(email);
189 | disabledEditText(phone);
190 |
191 | if (card == null || !card.hasAvatar()) {
192 | avatar.setImageResource(R.drawable.ic_avatar);
193 | }
194 |
195 | for (int i = 0; i < fields.getChildCount(); i++) {
196 | EditText field = (EditText) fields.getChildAt(i);
197 | if (getEditTextValue(field).isEmpty()) {
198 | fields.removeView(field);
199 | i--;
200 | } else {
201 | disabledEditText(field);
202 | }
203 | }
204 |
205 | avatarEditOverlay.setVisibility(View.GONE);
206 |
207 | // Close keyboard
208 | InputMethodManager imm = (InputMethodManager)
209 | getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
210 | imm.hideSoftInputFromWindow(getWindowToken(), 0);
211 | }
212 |
213 | private void addNewField(String value) {
214 | inflate(getContext(), R.layout.item_card_field, fields);
215 | EditText field = (EditText) fields.getChildAt(fields.getChildCount() - 1);
216 | field.setText(value);
217 | field.addTextChangedListener(fieldTextWatcher);
218 | if (!editMode) {
219 | disabledEditText(field);
220 | }
221 | }
222 |
223 | // Private
224 |
225 | private String getEditTextValue(EditText editText) {
226 | return editText.getText().toString().trim();
227 | }
228 |
229 | private void disabledEditText(EditText editText) {
230 | // Hide if empty
231 | if (editText.getText().length() == 0) {
232 | editText.setVisibility(View.GONE);
233 | } else {
234 | editText.setVisibility(View.VISIBLE);
235 | }
236 |
237 | editText.setCursorVisible(false);
238 | editText.setLongClickable(false);
239 | editText.setFocusable(false);
240 | editText.setFocusableInTouchMode(false);
241 | editText.setSelected(false);
242 |
243 | // Hide background
244 | editText.setBackgroundTintList(getResources().getColorStateList(android.R.color.white));
245 | }
246 |
247 | private void enableEditText(EditText editText) {
248 | editText.setVisibility(View.VISIBLE);
249 |
250 | editText.setCursorVisible(true);
251 | editText.setLongClickable(true);
252 | editText.setFocusable(true);
253 | editText.setFocusableInTouchMode(true);
254 | editText.setSelected(true);
255 |
256 | // Show background
257 | editText.setBackgroundTintList(getResources().getColorStateList(R.color.secondary));
258 | }
259 |
260 | private boolean allFieldsHaveContent() {
261 | for (int i = 0, count = fields.getChildCount(); i < count; i++) {
262 | EditText field = (EditText) fields.getChildAt(i);
263 | if (getEditTextValue(field).isEmpty()) {
264 | return false;
265 | }
266 | }
267 | return true;
268 | }
269 |
270 | public interface CardEditListener {
271 | void onPickAvatar();
272 |
273 | void onChange(Card updatedCard);
274 | }
275 |
276 | private class FieldTextWatcher extends SimpleTextWatcher {
277 | @Override
278 | public void onTextChanged(String newText) {
279 | if (editMode && editListener != null) {
280 | editListener.onChange(getCard());
281 |
282 | if (allFieldsHaveContent()) {
283 | addNewField("");
284 | }
285 | }
286 | }
287 | }
288 | }
289 |
--------------------------------------------------------------------------------