diff --git a/AndroidClient/build.gradle b/AndroidClient/build.gradle index 74add1e..46f57fd 100644 --- a/AndroidClient/build.gradle +++ b/AndroidClient/build.gradle @@ -40,7 +40,6 @@ android { baseline = file("lint-baseline.xml") } } - dependencies { implementation libs.appcompat @@ -48,8 +47,8 @@ dependencies { implementation libs.material implementation libs.cardview - implementation libs.lifecycleextensions - annotationProcessor libs.lifecyclecompiler + implementation libs.lifecycle.extensions + annotationProcessor libs.lifecycle.compiler implementation libs.socketioclient implementation libs.eventbus @@ -76,17 +75,23 @@ dependencies { // Retrofit & OkHttp implementation libs.retrofit - implementation libs.retrofitconvertergson + implementation libs.retrofit.converter.jackson + //implementation libs.retrofitconvertergson + // https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 + implementation(libs.jackson.datatype.jsr310) + // https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8 + implementation(libs.jackson.datatype.jdk8) + implementation libs.okhttp // Dagger 2 implementation libs.dagger - annotationProcessor libs.daggercompiler + annotationProcessor libs.dagger.compiler // Room - implementation libs.roomruntime - implementation libs.roomrxjava2 - annotationProcessor libs.roomcompiler + implementation libs.room.runtime + implementation libs.room.rxjava2 + annotationProcessor libs.room.compiler //LocalDateTime features @@ -97,6 +102,8 @@ dependencies { // https://mvnrepository.com/artifact/androidx.preference/preference implementation 'androidx.preference:preference:1.2.1' + implementation libs.core + /*TESTS*/ @@ -112,3 +119,4 @@ dependencies { //testImplementation("org.powermock:powermock-module-junit4:2.0.9") } + diff --git a/AndroidClient/src/main/AndroidManifest.xml b/AndroidClient/src/main/AndroidManifest.xml index 7fdabcf..b06c7d5 100644 --- a/AndroidClient/src/main/AndroidManifest.xml +++ b/AndroidClient/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + + diff --git a/AndroidClient/src/main/java/com/tom/meeter/App.java b/AndroidClient/src/main/java/com/tom/meeter/App.java index ce76f46..6356188 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/App.java +++ b/AndroidClient/src/main/java/com/tom/meeter/App.java @@ -1,5 +1,6 @@ package com.tom.meeter; +import static com.tom.meeter.context.notification.NotificationHelper.createNotificationChannel; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.app.Application; @@ -8,8 +9,12 @@ import com.tom.meeter.context.auth.DaggerAuthComponent; import com.tom.meeter.context.event.DaggerEventComponent; import com.tom.meeter.context.event.EventComponent; +import com.tom.meeter.context.image.DaggerImageComponent; +import com.tom.meeter.context.image.ImageComponent; import com.tom.meeter.context.token.DaggerTokenComponent; import com.tom.meeter.context.token.TokenComponent; +import com.tom.meeter.context.user.DaggerUserComponent; +import com.tom.meeter.context.user.UserComponent; public class App extends Application { @@ -17,18 +22,27 @@ public class App extends Application { private AppComponent component; private AuthComponent authComponent; private TokenComponent tokenComponent; + private ImageComponent imageComponent; private EventComponent eventComponent; + private UserComponent userComponent; @Override public void onCreate() { super.onCreate(); logMethod(TAG, this); + /* Independent */ tokenComponent = buildTokenComponent(); authComponent = buildAuthComponent(); + imageComponent = buildImageComponent(); + + /* Dependent */ eventComponent = buildEventComponent(); + userComponent = buildUserComponent(); component = buildComponent(); + + createNotificationChannel(this); } @Override @@ -40,8 +54,10 @@ public void onTerminate() { protected AppComponent buildComponent() { return DaggerAppComponent.builder() .tokenComponent(tokenComponent) + .imageComponent(imageComponent) .authComponent(authComponent) .eventComponent(eventComponent) + .userComponent(userComponent) .application(this) .build(); } @@ -58,9 +74,25 @@ protected TokenComponent buildTokenComponent() { .build(); } + protected ImageComponent buildImageComponent() { + return DaggerImageComponent.builder() + .application(this) + .build(); + } + protected EventComponent buildEventComponent() { return DaggerEventComponent.builder() .application(this) + .tokenComponent(tokenComponent) + .imageComponent(imageComponent) + .build(); + } + + protected UserComponent buildUserComponent() { + return DaggerUserComponent.builder() + .application(this) + .tokenComponent(tokenComponent) + .imageComponent(imageComponent) .build(); } @@ -79,4 +111,12 @@ public TokenComponent getTokenComponent() { public EventComponent getEventComponent() { return eventComponent; } + + public ImageComponent getImageComponent() { + return imageComponent; + } + + public UserComponent getUserComponent() { + return userComponent; + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/AppComponent.java b/AndroidClient/src/main/java/com/tom/meeter/AppComponent.java index f61c54b..c15f536 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/AppComponent.java +++ b/AndroidClient/src/main/java/com/tom/meeter/AppComponent.java @@ -4,21 +4,33 @@ import com.tom.meeter.context.auth.AuthComponent; import com.tom.meeter.context.event.EventComponent; -import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.image.ImageComponent; import com.tom.meeter.context.profile.activity.ProfileActivity; import com.tom.meeter.context.profile.activity.SettingsActivity; +import com.tom.meeter.context.profile.fragment.ActiveEventsFragment; +import com.tom.meeter.context.profile.fragment.GoogleMapsFragment; import com.tom.meeter.context.profile.fragment.ProfileFragment; -import com.tom.meeter.context.profile.fragment.UserEventsFragment; +import com.tom.meeter.context.profile.fragment.ProfileEventsFragment; import com.tom.meeter.context.token.TokenComponent; -import com.tom.meeter.context.user.activity.UserActivity; +import com.tom.meeter.context.user.UserComponent; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelModule; import dagger.BindsInstance; import dagger.Component; @Component( - modules = {AppModule.class, ViewModelModule.class}, - dependencies = {TokenComponent.class, AuthComponent.class, EventComponent.class}) + modules = { + AppModule.class, + ViewModelModule.class + }, + dependencies = { + TokenComponent.class, + AuthComponent.class, + ImageComponent.class, + + EventComponent.class, + UserComponent.class + }) @AppScope public interface AppComponent { @@ -29,21 +41,27 @@ interface Builder { Builder application(Application application); Builder authComponent(AuthComponent authComponent); + Builder tokenComponent(TokenComponent tokenComponent); + Builder eventComponent(EventComponent eventComponent); + Builder imageComponent(ImageComponent imageComponent); + + Builder userComponent(UserComponent userComponent); + AppComponent build(); } void inject(ProfileActivity profileActivity); - void inject(ProfileFragment profileFragment); + void inject(SettingsActivity settingsActivity); - void inject(UserEventsFragment userEventsFragment); + void inject(ProfileFragment profileFragment); - void inject(SettingsActivity settingsActivity); + void inject(GoogleMapsFragment googleMapsFragment); - void inject(UserActivity userActivity); + void inject(ActiveEventsFragment activeEventsFragment); - void inject(EventActivity eventActivity); + void inject(ProfileEventsFragment profileEventsFragment); } \ No newline at end of file diff --git a/AndroidClient/src/main/java/com/tom/meeter/AppModule.java b/AndroidClient/src/main/java/com/tom/meeter/AppModule.java index 26855bd..efc0c8c 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/AppModule.java +++ b/AndroidClient/src/main/java/com/tom/meeter/AppModule.java @@ -8,15 +8,20 @@ import androidx.annotation.NonNull; import androidx.room.Room; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.tom.meeter.context.profile.event.database.EventDao; import com.tom.meeter.context.profile.event.database.EventDatabase; -import com.tom.meeter.context.profile.event.service.EventService; import com.tom.meeter.context.profile.service.ProfileService; import com.tom.meeter.context.profile.settings.service.SettingsService; import com.tom.meeter.context.profile.user.database.UserDao; import com.tom.meeter.context.profile.user.database.UserDatabase; -import com.tom.meeter.context.user.service.UserService; +import com.tom.meeter.infrastructure.http.HttpClient; +import java.util.TimeZone; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @@ -25,7 +30,7 @@ import dagger.Module; import dagger.Provides; import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; @Module public class AppModule { @@ -42,22 +47,18 @@ public AppModule() { public ProfileService provideProfileService(Application app) { return new Retrofit.Builder() .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create( + JsonMapper.builder() + .addModule(new JavaTimeModule()) + .addModule(new Jdk8Module()) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .setTimeZone(TimeZone.getDefault()))) .build() .create(ProfileService.class); } - @AppScope - @NonNull - @Provides - public UserService provideUserService(Application app) { - return new Retrofit.Builder() - .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(UserService.class); - } - @AppScope @NonNull @Provides @@ -82,17 +83,6 @@ public Executor provideExecutor() { new ArrayBlockingQueue<>(15, false)); } - @AppScope - @NonNull - @Provides - public EventService provideEventService(Application app) { - return new Retrofit.Builder() - .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(EventService.class); - } - @AppScope @NonNull @Provides @@ -115,8 +105,16 @@ public EventDao provideEventDao(EventDatabase eventDatabase) { public SettingsService provideSettingsService(Application app) { return new Retrofit.Builder() .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + //.addConverterFactory(GsonConverterFactory.create()) .build() .create(SettingsService.class); } + + @AppScope + @NonNull + @Provides + public HttpClient provideHttpClient(Application app) { + return new HttpClient(getServerPath(app)); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/components/ExpandableHeightGridView.java b/AndroidClient/src/main/java/com/tom/meeter/components/ExpandableHeightGridView.java deleted file mode 100644 index b9afe48..0000000 --- a/AndroidClient/src/main/java/com/tom/meeter/components/ExpandableHeightGridView.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.tom.meeter.components; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.GridView; - -public class ExpandableHeightGridView extends GridView { - private boolean expanded = false; - - public ExpandableHeightGridView(Context context) { - super(context); - } - - public ExpandableHeightGridView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ExpandableHeightGridView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public boolean isExpanded() { - return expanded; - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (isExpanded()) { - // Calculate entire height by providing a very large height hint. - // View.MEASURED_SIZE_MASK represents the largest height possible. - int expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, - MeasureSpec.AT_MOST); - super.onMeasure(widthMeasureSpec, expandSpec); - - ViewGroup.LayoutParams params = getLayoutParams(); - params.height = getMeasuredHeight(); - } else { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - - public void setExpanded(boolean expanded) { - this.expanded = expanded; - } -} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/AuthModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/AuthModule.java index 2738374..3ab22e7 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/AuthModule.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/AuthModule.java @@ -15,7 +15,8 @@ import dagger.Module; import dagger.Provides; import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; +//import retrofit2.converter.gson.GsonConverterFactory; @Module public class AuthModule { @@ -32,7 +33,8 @@ public AuthModule() { public AuthService provideAuthService(Application app) { return new Retrofit.Builder() .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + //.addConverterFactory(GsonConverterFactory.create()) .build() .create(AuthService.class); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/LoginActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/LoginActivity.java index 8e5c9f1..6051647 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/LoginActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/LoginActivity.java @@ -2,6 +2,7 @@ import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.ACCOUNT_TYPE; import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.USER_PASS_KEY; +import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.USER_UUID_KEY; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.accounts.Account; @@ -170,8 +171,10 @@ public void onResponse(Call call, Response respons Intent intent = new Intent(); intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, userLogin); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - intent.putExtra(AccountManager.KEY_AUTHTOKEN, response.body().getToken()); + TokenResponse res = response.body(); + intent.putExtra(AccountManager.KEY_AUTHTOKEN, res.getToken()); intent.putExtra(USER_PASS_KEY, userPass); + intent.putExtra(USER_UUID_KEY, res.getUuid()); finishLogin(intent); } else { new AlertDialog.Builder(LoginActivity.this) @@ -198,6 +201,7 @@ private void finishLogin(Intent intent) { String login = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); String accountType = intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE); String token = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN); + String uuid = intent.getStringExtra(USER_UUID_KEY); String pass = intent.getStringExtra(AccountAuthenticator.USER_PASS_KEY); Account account = new Account(login, accountType); if (getIntent().getBooleanExtra(AccountAuthenticator.IS_ADDING_NEW_ACCOUNT_KEY, false)) { @@ -205,6 +209,7 @@ private void finishLogin(Intent intent) { // (Not setting the auth token will cause another call to the server to authenticate the user) accountManager.addAccountExplicitly(account, pass, null); accountManager.setAuthToken(account, AccountAuthenticator.AUTH_TYPE, token); + accountManager.setUserData(account, USER_UUID_KEY, uuid); } else { accountManager.setPassword(account, pass); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/RegistrationActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/RegistrationActivity.java index 3cb67e4..be555fb 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/RegistrationActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/activity/RegistrationActivity.java @@ -1,6 +1,7 @@ package com.tom.meeter.context.auth.activity; import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.ACCOUNT_TYPE; +import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.USER_UUID_KEY; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.accounts.AccountManager; @@ -131,12 +132,14 @@ public void submit() { registerCall.enqueue(new Callback<>() { @Override public void onResponse(Call call, Response response) { + TokenResponse authRes = response.body(); if (response.code() == HttpCodes.OK) { Bundle bundle = new Bundle(); bundle.putString(AccountManager.KEY_ACCOUNT_NAME, userLogin); bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - bundle.putString(AccountManager.KEY_AUTHTOKEN, response.body().getToken()); + bundle.putString(AccountManager.KEY_AUTHTOKEN, authRes.getToken()); bundle.putString(AccountAuthenticator.USER_PASS_KEY, userPass); + bundle.putString(USER_UUID_KEY, authRes.getUuid()); Intent res = new Intent(); res.putExtras(bundle); @@ -147,12 +150,12 @@ public void onResponse(Call call, Response respons .show();*/ new AlertDialog.Builder(RegistrationActivity.this) .setTitle(getString(R.string.register_error)) - .setMessage(response.code() + ":" + response.body()) + .setMessage(response.code() + ":" + authRes) .setNegativeButton(getString(R.string.ok), (dialog, id) -> dialog.cancel()) .create() .show(); Log.d(TAG, "Response failed with [" + response.code() - + "] code and body {" + response.body() + "}"); + + "] code and body {" + authRes + "}"); } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AccountAuthenticator.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AccountAuthenticator.java index a77443f..d3dd061 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AccountAuthenticator.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AccountAuthenticator.java @@ -36,6 +36,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator { public static final String AUTH_TYPE = "jwt_auth"; public static final String IS_ADDING_NEW_ACCOUNT_KEY = "is-adding-new-account"; public static final String USER_PASS_KEY = "the-password"; + public static final String USER_UUID_KEY = "the-uuid"; private static final String LABEL = " label"; //public static final String ACCOUNT_NAME = "Meeter"; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AuthHelper.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AuthHelper.java index 4cb135c..fb8044d 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AuthHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/infrastructure/AuthHelper.java @@ -2,6 +2,7 @@ import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.ACCOUNT_TYPE; import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.AUTH_TYPE; +import static com.tom.meeter.context.auth.infrastructure.AccountAuthenticator.USER_UUID_KEY; import android.accounts.Account; import android.accounts.AccountManager; @@ -15,7 +16,7 @@ import com.tom.meeter.R; import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.infrastructure.common.Globals; -import com.tom.meeter.infrastructure.http.DisconnectLogger; +import com.tom.meeter.infrastructure.http.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; import java.io.IOException; @@ -39,6 +40,14 @@ public static String peekToken(AccountManager am) { return am.peekAuthToken(getSingleAccount(am), AccountAuthenticator.AUTH_TYPE); } + public static String getAuthHeader(AccountManager am) { + return Globals.getAuthHeader(peekToken(am)); + } + + public static String getUserUuid(AccountManager am) { + return am.getUserData(getSingleAccount(am), USER_UUID_KEY); + } + public static void setToken(AccountManager am, String token) { am.setAuthToken(getSingleAccount(am), AccountAuthenticator.AUTH_TYPE, token); } @@ -76,7 +85,7 @@ public static void checkToken( return; } tokenService.checkToken(Globals.getAuthHeader(token)).enqueue( - new DisconnectLogger<>(activity) { + new ErrorLogger<>(activity) { @Override public void onResponse(Call call, Response response) { if (response.code() == HttpCodes.NOT_AUTHENTICATED) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/auth/message/TokenResponse.java b/AndroidClient/src/main/java/com/tom/meeter/context/auth/message/TokenResponse.java index a891f7e..9e4318c 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/auth/message/TokenResponse.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/auth/message/TokenResponse.java @@ -1,13 +1,54 @@ package com.tom.meeter.context.auth.message; -public class TokenResponse { +import java.util.Objects; + +public final class TokenResponse { private String token; + private String uuid; + + public TokenResponse() { + } - public TokenResponse(String token) { + public TokenResponse(String token, String uuid) { this.token = token; + this.uuid = uuid; } public String getToken() { return token; } + + public String getUuid() { + return uuid; + } + + public void setToken(String token) { + this.token = token; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (TokenResponse) obj; + return Objects.equals(this.token, that.token) && + Objects.equals(this.uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(token, uuid); + } + + @Override + public String toString() { + return "TokenResponse[" + + "token=" + token + ", " + + "uuid=" + uuid + ']'; + } + } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/EventComponent.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventComponent.java index 4c46446..e834b05 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/EventComponent.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventComponent.java @@ -2,15 +2,27 @@ import android.app.Application; +import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.event.activity.EventLocationMapActivity; +import com.tom.meeter.context.event.activity.EventOnMapActivity; import com.tom.meeter.context.event.service.EventService; - -import javax.inject.Singleton; +import com.tom.meeter.context.event.viewmodel.EventViewModelModule; +import com.tom.meeter.context.image.ImageComponent; +import com.tom.meeter.context.token.TokenComponent; import dagger.BindsInstance; import dagger.Component; -@Singleton -@Component(modules = {EventModule.class}) +@EventScope +@Component( + modules = { + EventModule.class, + EventViewModelModule.class + }, + dependencies = { + TokenComponent.class, + ImageComponent.class + }) public interface EventComponent { EventService provideEventService(); @@ -20,6 +32,16 @@ interface Builder { @BindsInstance Builder application(Application application); + Builder tokenComponent(TokenComponent tokenComponent); + + Builder imageComponent(ImageComponent tokenComponent); + EventComponent build(); } + + void inject(EventActivity eventActivity); + + void inject(EventOnMapActivity eventOnMapActivity); + + void inject(EventLocationMapActivity eventLocationMapActivity); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/EventModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventModule.java index 8c54e61..5bb0b9c 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/EventModule.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventModule.java @@ -7,14 +7,19 @@ import androidx.annotation.NonNull; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.tom.meeter.context.event.service.EventService; -import javax.inject.Singleton; +import java.util.TimeZone; import dagger.Module; import dagger.Provides; import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; @Module public class EventModule { @@ -25,13 +30,21 @@ public EventModule() { logMethod(TAG, this); } - @Singleton + @EventScope @NonNull @Provides public EventService provideEventService(Application app) { return new Retrofit.Builder() .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create( + JsonMapper.builder() + .addModule(new JavaTimeModule()) + //.addModule(new Jdk8Module().configureReadAbsentAsNull(false)) + .addModule(new Jdk8Module()) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .setTimeZone(TimeZone.getDefault()))) .build() .create(EventService.class); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/EventScope.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventScope.java new file mode 100644 index 0000000..85a3b66 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/EventScope.java @@ -0,0 +1,11 @@ +package com.tom.meeter.context.event; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface EventScope { +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventActivity.java index 7e9bd4a..0d97e26 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventActivity.java @@ -1,50 +1,101 @@ package com.tom.meeter.context.event.activity; import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.getCircleBitmap; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.randomPicResource; +import static com.tom.meeter.context.event.activity.EventLocationMapActivity.createEventLocationMapActivityIntent; +import static com.tom.meeter.context.event.activity.EventOnMapActivity.dispatchToEventOnMapActivity; +import static com.tom.meeter.context.event.utils.Utils.createUpdateEventRequest; +import static com.tom.meeter.context.user.activity.UserActivity.dispatchToUserActivity; +import static com.tom.meeter.infrastructure.common.CommonHelper.UI_DATE_TIME_FORMAT; +import static com.tom.meeter.infrastructure.common.CommonHelper.dateOrNull; +import static com.tom.meeter.infrastructure.common.CommonHelper.textOrNull; +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; import android.accounts.AccountManager; +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.view.View; +import android.widget.EditText; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProviders; +import androidx.viewbinding.ViewBinding; +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.event.message.UpdateEventRequest; +import com.tom.meeter.context.event.service.EventService; import com.tom.meeter.context.event.viewmodel.EventViewModel; +import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.token.service.TokenService; -import com.tom.meeter.context.user.activity.UserActivity; -import com.tom.meeter.databinding.EventLayoutBinding; +import com.tom.meeter.databinding.ActivityEventEditableBinding; +import com.tom.meeter.databinding.ActivityEventReadableBinding; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + import javax.inject.Inject; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + public class EventActivity extends AppCompatActivity { + public static final String EVENT_ID_KEY = "event_id"; + public static final String EXTRA_LAT = "extra_lat"; + public static final String EXTRA_LNG = "extra_lng"; + private static final String TAG = EventActivity.class.getCanonicalName(); - EventLayoutBinding binding; + + ViewBinding binding; @Inject TokenService tokenService; @Inject + EventService eventService; + @Inject ViewModelFactory viewModelFactory; private EventViewModel eventViewModel; - private String eventId; private AccountManager accountManager; + private ActivityResultLauncher mapResult; + + private EventDTO eventCache; + private ResponseBody photoCache; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mapResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + double lat = result.getData().getDoubleExtra(EXTRA_LAT, 0.0); + double lng = result.getData().getDoubleExtra(EXTRA_LNG, 0.0); + if (binding instanceof ActivityEventEditableBinding eBinding) { + eBinding.eventLatitude.setText(String.valueOf(lat)); + eBinding.eventLongitude.setText(String.valueOf(lng)); + } + } + }); + logMethod(TAG, this); Bundle extras = getIntent().getExtras(); @@ -53,45 +104,137 @@ protected void onCreate(Bundle savedInstanceState) { finish(); return; } - eventId = extras.getString(EVENT_ID_KEY); + String eventId = extras.getString(EVENT_ID_KEY); if (eventId == null) { Log.d(TAG, "Unable to create event activity without 'event_id' provided."); finish(); return; } - ((App) getApplication()).getComponent().inject(this); + ((App) getApplication()).getEventComponent().inject(this); accountManager = AccountManager.get(this); //setToken(accountManager, Launcher.EXPIRED); - checkToken(this::onInit, this::finish, accountManager, this, tokenService); + checkToken((token) -> onInit(token, eventId), this::finish, accountManager, this, tokenService); } - private void onInit(String token) { - binding = EventLayoutBinding.inflate(getLayoutInflater()); - View view = binding.getRoot(); - setContentView(view); - + private void onInit(String token, String eventId) { eventViewModel = ViewModelProviders.of(this, viewModelFactory) .get(EventViewModel.class); eventViewModel.fetchEventInformation(token, eventId, this); eventViewModel.getEventLiveData() .observe(this, event -> { - if (event != null) { - Bitmap src = BitmapFactory.decodeResource(getResources(), randomPicResource()); - Bitmap scaled = Bitmap.createScaledBitmap(src, 150, 150, true); - Bitmap circled = getCircleBitmap(scaled); - binding.eventPhoto.setImageBitmap(circled); - binding.eventName.setText(event.getName()); - binding.eventDescription.setText(event.getDescription()); - binding.eventCreatorIdBtn.setOnClickListener(v -> { - startActivity(new Intent(this, UserActivity.class) - .putExtra(UserActivity.USER_ID_KEY, event.getCreatorId())); - }); + eventCache = event; + if (AuthHelper.getUserUuid(accountManager).equals(eventCache.getCreatorId())) { + initEditableLayout(token); + } else { + initReadableLayout(); } }); } + private void initReadableLayout() { + binding = ActivityEventReadableBinding.inflate(getLayoutInflater()); + ActivityEventReadableBinding rBinding = (ActivityEventReadableBinding) binding; + View view = rBinding.getRoot(); + setContentView(view); + + updateReadableLayout(); + rBinding.eventCreator.setOnClickListener( + v -> dispatchToUserActivity(this, eventCache.getCreatorId())); + rBinding.btnEventLocationMap.setOnClickListener( + v -> dispatchToEventOnMapActivity(this, eventCache.getId())); + + + eventViewModel.getEventPhotoLiveData() + .observe( + this, photo -> rBinding.eventPhoto.setImageBitmap( + circleImage(photo, 600, 600))); + } + + private void initEditableLayout(String token) { + binding = ActivityEventEditableBinding.inflate(getLayoutInflater()); + ActivityEventEditableBinding eBinding = (ActivityEventEditableBinding) binding; + View view = eBinding.getRoot(); + setContentView(view); + + eBinding.saveEventButton.setOnClickListener(v -> { + UpdateEventRequest req = createUpdateEventRequest(eventCache, eBinding); + if (req.isEmpty()) { + showMessage(this, "Empty update request is not sent."); + return; + } + eventService.updateEvent(Globals.getAuthHeader(token), eventCache.getId(), req).enqueue( + new HttpErrorLogger<>(getApplicationContext()) { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + eventCache = response.body(); + updateEditableLayout(); + showMessage(EventActivity.this, "Saved."); + } + } + }); + }); + + + /* + TODO photoPath; + * */ + + updateEditableLayout(); + + eBinding.selectStartingDateButton.setOnClickListener( + v -> showDateTimePicker(eBinding.eventStarting)); + eBinding.selectEndingDateButton.setOnClickListener( + v -> showDateTimePicker(eBinding.eventEnding)); + eBinding.btnEventLocationMap.setOnClickListener( + v -> mapResult.launch( + createEventLocationMapActivityIntent(this, eventCache.getId()))); + eBinding.selectPhotoButton.setOnClickListener( + v -> showMessage(EventActivity.this, "Кнопка пока не работает...")); + + eventViewModel.getEventPhotoLiveData() + .observe( + this, photo -> { + photoCache = photo; + updateEditablePhoto(); + }); + } + + private void updateEditablePhoto() { + if (binding instanceof ActivityEventEditableBinding eBinding) { + eBinding.eventPhoto.setImageBitmap(circleImage(photoCache, 600, 600)); + } + } + + private void updateEditableLayout() { + ActivityEventEditableBinding eBinding = (ActivityEventEditableBinding) binding; + eBinding.eventName.setText(eventCache.getName()); + eBinding.eventCreated.setText(UI_DATE_TIME_FORMAT.format(eventCache.getCreated())); + + eBinding.eventDescription.setText(eventCache.getDescription()); + eBinding.eventLatitude.setText(textOrNull(eventCache.getLatitude())); + eBinding.eventLongitude.setText(textOrNull(eventCache.getLongitude())); + eBinding.eventStarting.setText(dateOrNull(eventCache.getStarting())); + eBinding.eventEnding.setText(dateOrNull(eventCache.getEnding())); + eBinding.eventCity.setText(eventCache.getCity()); + + } + + private void updateReadableLayout() { + ActivityEventReadableBinding rBinding = (ActivityEventReadableBinding) binding; + rBinding.eventName.setText(eventCache.getName()); + rBinding.eventCreated.setText(UI_DATE_TIME_FORMAT.format(eventCache.getCreated())); + rBinding.eventDescription.setText(eventCache.getDescription()); + rBinding.eventLatitude.setText(textOrNull(eventCache.getLatitude())); + rBinding.eventLongitude.setText(textOrNull(eventCache.getLongitude())); + rBinding.eventStarting.setText(dateOrNull(eventCache.getStarting())); + rBinding.eventEnding.setText(dateOrNull(eventCache.getEnding())); + rBinding.eventCity.setText(eventCache.getCity()); + } + + @Nullable @Override public View onCreateView( @@ -99,4 +242,100 @@ public View onCreateView( @NonNull AttributeSet attrs) { return super.onCreateView(parent, name, ctx, attrs); } + + // Метод для отображения DatePickerDialog + private void showDatePickerDialog(final EditText targetEditText) { + // Получаем текущую дату + Calendar calendar = Calendar.getInstance(); + int year = calendar.get(Calendar.YEAR); + int month = calendar.get(Calendar.MONTH); + int day = calendar.get(Calendar.DAY_OF_MONTH); + + // Создаем и показываем DatePickerDialog + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + (view, selectedYear, selectedMonth, selectedDay) -> { + // Устанавливаем выбранную дату в EditText + String selectedDate = selectedDay + "/" + (selectedMonth + 1) + "/" + selectedYear; + targetEditText.setText(selectedDate); + }, year, month, day); + + // Показываем диалог + datePickerDialog.show(); + } + + // Метод для отображения Material DatePicker + private void showMaterialDatePicker(final EditText targetEditText) { + // Создаём constraints (ограничения для выбора даты) + CalendarConstraints.Builder constraintsBuilder = new CalendarConstraints.Builder(); + Calendar calendar = Calendar.getInstance(); + constraintsBuilder.setValidator(DateValidatorPointForward.from(calendar.getTimeInMillis())); + + // Создаем Material DatePicker + MaterialDatePicker.Builder builder = MaterialDatePicker.Builder.datePicker(); + builder.setCalendarConstraints(constraintsBuilder.build()); + builder.setTitleText("Select Date"); + + MaterialDatePicker datePicker = builder.build(); + + // Устанавливаем слушатель на выбор даты + datePicker.addOnPositiveButtonClickListener(selection -> { + // Форматируем выбранную дату + Calendar selectedDate = Calendar.getInstance(); + selectedDate.setTimeInMillis(selection); + String selectedDateString = selectedDate.get(Calendar.DAY_OF_MONTH) + "/" + + (selectedDate.get(Calendar.MONTH) + 1) + "/" + + selectedDate.get(Calendar.YEAR); + + // Устанавливаем выбранную дату в поле + targetEditText.setText(selectedDateString); + }); + + // Показываем диалог + datePicker.show(getSupportFragmentManager(), datePicker.toString()); + } + + private void showDateTimePicker(EditText target) { + final Calendar calendar = Calendar.getInstance(); + + DatePickerDialog datePickerDialog = new DatePickerDialog( + this, + (view, year, month, dayOfMonth) -> { + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + + TimePickerDialog timePickerDialog = new TimePickerDialog( + this, + (timeView, hourOfDay, minute) -> { + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); + calendar.set(Calendar.MINUTE, minute); + + SimpleDateFormat sdf = new SimpleDateFormat( + "yyyy-MM-dd HH:mm", Locale.getDefault()); + String formatted = sdf.format(calendar.getTime()); + target.setText(formatted); + }, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + true + ); + + timePickerDialog.show(); + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ); + + datePickerDialog.show(); + } + + public static void dispatchToEventActivity(Context ctx, String eventId) { + ctx.startActivity(createEventActivityIntent(ctx, eventId)); + } + + public static Intent createEventActivityIntent(Context ctx, String eventId) { + return new Intent(ctx, EventActivity.class) + .putExtra(EventActivity.EVENT_ID_KEY, eventId); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventLocationMapActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventLocationMapActivity.java new file mode 100644 index 0000000..c83cc54 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventLocationMapActivity.java @@ -0,0 +1,235 @@ +package com.tom.meeter.context.event.activity; + +import static com.tom.meeter.context.profile.fragment.GoogleMapsFragment.ZOOM_VALUE; +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; + +import android.accounts.AccountManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.location.Location; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.UiSettings; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.tom.meeter.App; +import com.tom.meeter.R; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.event.service.EventService; +import com.tom.meeter.context.gps.domain.LocationTrackerListener; +import com.tom.meeter.context.gps.service.LocationTrackerService; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.databinding.ActivityEventPositionBinding; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.http.ErrorLogger; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class EventLocationMapActivity extends AppCompatActivity + implements OnMapReadyCallback { + + private static final String TAG = EventLocationMapActivity.class.getCanonicalName(); + private static final String EVENT_ID_KEY = "event_id"; + private Marker eventMarker; + private GoogleMap gmap; + private ActivityEventPositionBinding binding; + + private ServiceConnection locationServiceConn; + private LocationTrackerService locationService; + private boolean cameraMoved = false; + + private LocationTrackerListener singleLocationUpdateListener; + private String eventId; + + @Inject + EventService eventService; + @Inject + ImageDownloader imageDownloader; + private AccountManager accountManager; + private LatLng userLocation; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + showMessage(this, "Unable to show map without extras provided."); + finish(); + } + eventId = extras.getString(EVENT_ID_KEY); + if (eventId == null) { + showMessage(this, "Unable to show map without event_id provided."); + finish(); + } + + binding = ActivityEventPositionBinding.inflate(getLayoutInflater()); + FrameLayout view = binding.getRoot(); + setContentView(view); + + ((App) getApplication()).getEventComponent().inject(this); + accountManager = AccountManager.get(this); + + SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() + .findFragmentById(R.id.eventSelectPosition); + if (mapFragment != null) { + mapFragment.getMapAsync(this); + } + binding.btnConfirm.setOnClickListener(v -> { + if (eventMarker != null) { + Intent resultIntent = new Intent(); + LatLng position = eventMarker.getPosition(); + resultIntent.putExtra(EventActivity.EXTRA_LAT, position.latitude); + resultIntent.putExtra(EventActivity.EXTRA_LNG, position.longitude); + setResult(RESULT_OK, resultIntent); + finish(); + } else { + Toast.makeText(this, "Выберите точку на карте", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void setupSingleLocationListener() { + singleLocationUpdateListener = new LocationTrackerListener() { + @Override + public void onLocationChanged(Location location) { + logMethod(TAG, this); + if (gmap != null && !cameraMoved) { + userLocation = new LatLng(location.getLatitude(), location.getLongitude()); + gmap.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, ZOOM_VALUE)); + cameraMoved = true; + locationService.removeLocationTrackerListener(this); + singleLocationUpdateListener = null; + unbindService(locationServiceConn); + locationService = null; + locationServiceConn = null; + } + } + }; + locationServiceConn = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder binder) { + logMethod(TAG, this); + locationService = ((LocationTrackerService.ServiceBinder) binder).getService(); + locationService.addLocationTrackerListener(singleLocationUpdateListener); + } + + public void onServiceDisconnected(ComponentName name) { + logMethod(TAG, this); + locationService = null; + locationServiceConn = null; + } + }; + Intent service = new Intent(this, LocationTrackerService.class); + bindService(service, locationServiceConn, BIND_AUTO_CREATE); + } + + @Override + public void onMapReady(GoogleMap googleMap) { + gmap = googleMap; + UiSettings uiSettings = gmap.getUiSettings(); + uiSettings.setZoomControlsEnabled(true); + + String token = AuthHelper.peekToken(accountManager); + String authHeader = Globals.getAuthHeader(token); + eventService.getEvent(authHeader, eventId).enqueue(new ErrorLogger<>(this) { + @Override + public void onResponse(Call call, Response response) { + if (response.code() == HttpCodes.OK) { + EventDTO event = response.body(); + + gmap.setOnMapClickListener( + latLng -> { + if (eventMarker != null) { + eventMarker.setPosition(latLng); + return; + } + if (userLocation != null) { + eventMarker = createEventMarker(latLng, event); + downloadEventImage(event); + } + Log.d(TAG, "Event marker is null, user location is null, nothing to do..."); + }); + + if (event.getLatitude() == null || event.getLongitude() == null) { + setupSingleLocationListener(); + return; + } + LatLng eventLatLng = new LatLng(event.getLatitude(), event.getLongitude()); + eventMarker = createEventMarker(eventLatLng, event); + gmap.moveCamera(CameraUpdateFactory.newLatLngZoom(eventLatLng, ZOOM_VALUE)); + downloadEventImage(event); + return; + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + EventLocationMapActivity.this.recreate(); + } + Log.i(TAG, "/event/{id}: " + response.code() + " : " + response.body()); + } + }); + } + + @Nullable + private Marker createEventMarker(LatLng latLng, EventDTO event) { + return gmap.addMarker( + new MarkerOptions() + .position(latLng) + .title(event.getName())); + } + + private void downloadEventImage(EventDTO event) { + assert eventMarker != null; + String photoPath = event.getPhotoPath(); + if (photoPath != null) { + imageDownloader.downloadEventImage( + photoPath, + EventLocationMapActivity.this.getApplicationContext(), + (photo) -> eventMarker.setIcon(BitmapDescriptorFactory.fromBitmap(circleImage(photo))), + EventLocationMapActivity.this::recreate); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + logMethod(TAG, this); + if (locationService != null && singleLocationUpdateListener != null) { + locationService.removeLocationTrackerListener(singleLocationUpdateListener); + } + if (locationServiceConn != null) { + unbindService(locationServiceConn); + } + } + + public static void dispatchToEventLocationMapActivity( + Context ctx, String eventId) { + ctx.startActivity(createEventLocationMapActivityIntent(ctx, eventId)); + } + + public static Intent createEventLocationMapActivityIntent(Context ctx, String eventId) { + Intent result = new Intent(ctx, EventLocationMapActivity.class); + result.putExtra(EVENT_ID_KEY, eventId); + return result; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventOnMapActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventOnMapActivity.java new file mode 100644 index 0000000..b253c25 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventOnMapActivity.java @@ -0,0 +1,157 @@ +package com.tom.meeter.context.event.activity; + +import static com.tom.meeter.context.profile.fragment.GoogleMapsFragment.ZOOM_VALUE; +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.FrameLayout; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.UiSettings; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.MarkerOptions; +import com.tom.meeter.App; +import com.tom.meeter.R; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.event.service.EventService; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.databinding.ActivityEventOnMapBinding; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.http.ErrorLogger; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class EventOnMapActivity extends AppCompatActivity + implements OnMapReadyCallback { + + private static final String TAG = EventOnMapActivity.class.getCanonicalName(); + private static final String EVENT_ID_KEY = "event_id"; + private GoogleMap gmap; + private ActivityEventOnMapBinding binding; + + @Inject + EventService eventService; + @Inject + ImageDownloader imageDownloader; + + private AccountManager accountManager; + private String eventId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + showMessage(this, "Unable to show map without extras provided."); + finish(); + } + eventId = extras.getString(EVENT_ID_KEY); + if (eventId == null) { + showMessage(this, "Unable to show map without event_id provided."); + finish(); + } + + binding = ActivityEventOnMapBinding.inflate(getLayoutInflater()); + FrameLayout view = binding.getRoot(); + setContentView(view); + + ((App) getApplication()).getEventComponent().inject(this); + accountManager = AccountManager.get(this); + + + SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() + .findFragmentById(R.id.eventOnMap); + if (mapFragment != null) { + mapFragment.getMapAsync(this); + } + } + + @Override + public void onMapReady(GoogleMap googleMap) { + gmap = googleMap; + UiSettings uiSettings = gmap.getUiSettings(); + uiSettings.setZoomControlsEnabled(true); + + String token = AuthHelper.peekToken(accountManager); + String authHeader = Globals.getAuthHeader(token); + eventService.getEvent(authHeader, eventId).enqueue(new ErrorLogger<>(this) { + @Override + public void onResponse(Call call, Response response) { + if (response.code() == HttpCodes.OK) { + EventDTO event = response.body(); + Double latitude = event.getLatitude(); + Double longitude = event.getLongitude(); + if (latitude == null || longitude == null) { + showMessage(EventOnMapActivity.this, "Event location is not set yet."); + return; + } + LatLng eventLatLng = new LatLng(latitude, longitude); + String photoPath = event.getPhotoPath(); + if (photoPath == null) { + gmap.addMarker( + new MarkerOptions() + .position(eventLatLng) + .title(event.getName())); + gmap.moveCamera(CameraUpdateFactory.newLatLngZoom(eventLatLng, ZOOM_VALUE)); + } else { + imageDownloader.downloadEventImage( + photoPath, + EventOnMapActivity.this.getApplicationContext(), + (photo) -> { + gmap.addMarker( + new MarkerOptions() + .position(eventLatLng) + .icon(BitmapDescriptorFactory.fromBitmap(circleImage(photo))) + .title(event.getName())); + gmap.moveCamera(CameraUpdateFactory.newLatLngZoom(eventLatLng, ZOOM_VALUE)); + }, + EventOnMapActivity.this::recreate); + } + return; + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + EventOnMapActivity.this.recreate(); + } + Log.i(TAG, "/event/{id}: " + response.code() + " : " + response.body()); + } + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + logMethod(TAG, this); + } + + public static void dispatchToEventOnMapActivity(Context ctx, String eventId) { + if (ctx == null || eventId == null) { + throw new IllegalArgumentException("ctx and event_id should present"); + } + ctx.startActivity(createEventOnMapActivityIntent(ctx, eventId)); + } + + public static Intent createEventOnMapActivityIntent( + Context ctx, String eventId) { + Intent result = new Intent(ctx, EventOnMapActivity.class); + result.putExtra(EVENT_ID_KEY, eventId); + return result; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/message/UpdateEventRequest.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/message/UpdateEventRequest.java new file mode 100644 index 0000000..bf6535c --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/message/UpdateEventRequest.java @@ -0,0 +1,154 @@ +package com.tom.meeter.context.event.message; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; +import java.util.Optional; + +public class UpdateEventRequest { + private final String PHOTO_PATH_KEY = "photo_path"; + + private Optional name; + private Optional description; + private Optional starting; + private Optional ending; + private Optional city; + private Optional latitude; + private Optional longitude; + @JsonProperty(value = PHOTO_PATH_KEY) + private Optional photoPath; + + public UpdateEventRequest() { + } + + public Optional getName() { + return name; + } + + public Optional getDescription() { + return description; + } + + public Optional getStarting() { + return starting; + } + + public Optional getEnding() { + return ending; + } + + public Optional getCity() { + return city; + } + + public Optional getLatitude() { + return latitude; + } + + public Optional getLongitude() { + return longitude; + } + + public Optional getPhotoPath() { + return photoPath; + } + + public void setName(String name) { + this.name = Optional.ofNullable(name); + } + + public void setDescription(String description) { + this.description = Optional.ofNullable(description); + } + + public void setStarting(OffsetDateTime starting) { + this.starting = Optional.ofNullable(starting); + } + + public void setEnding(OffsetDateTime ending) { + this.ending = Optional.ofNullable(ending); + } + + public void setCity(String city) { + this.city = Optional.ofNullable(city); + } + + public void setLatitude(Float latitude) { + this.latitude = Optional.ofNullable(latitude); + } + + public void setLongitude(Float longitude) { + this.longitude = Optional.ofNullable(longitude); + } + + public void setPhotoPath(String photoPath) { + this.photoPath = Optional.ofNullable(photoPath); + } + + public boolean isEmpty() { + return name == null && description == null && starting == null && ending == null + && city == null && latitude == null && longitude == null && photoPath == null; + } + +/* + + + private final String NAME_KEY = "name"; + private final String DESCR_KEY = "description"; + private final String STARTING_KEY = "starting"; + private final String ENDING_KEY = "ending"; + private final String CITY_KEY = "city"; + private final String LATITUDE_KEY = "latitude"; + private final String LONGITUDE_KEY = "longitude"; + + + public Map toUpdateMap() { + Map result = new HashMap<>(); + putIfNotNull(name, NAME_KEY, result); + putIfNotNull(description, DESCR_KEY, result); + putIfNotNull(starting, STARTING_KEY, result); + putIfNotNull(ending, ENDING_KEY, result); + putIfNotNull(city, CITY_KEY, result); + putIfNotNull(latitude, LATITUDE_KEY, result); + putIfNotNull(longitude, LONGITUDE_KEY, result); + putIfNotNull(photoPath, PHOTO_PATH_KEY, result); + return result; + } + + public JSONObject toJson() { + JSONObject result = new JSONObject(); + putIfNotNull(name, NAME_KEY, result); + putIfNotNull(description, DESCR_KEY, result); + putIfNotNull(starting, STARTING_KEY, result); + putIfNotNull(ending, ENDING_KEY, result); + putIfNotNull(city, CITY_KEY, result); + putIfNotNull(latitude, LATITUDE_KEY, result); + putIfNotNull(longitude, LONGITUDE_KEY, result); + putIfNotNull(photoPath, PHOTO_PATH_KEY, result); + return result; + } + + private void putIfNotNull(Optional field, String key, Map result) { + if (field != null) { + result.put(key, field.orElse(null)); + } + } + + private void putIfNotNull(Optional field, String key, JSONObject result) { + if (field != null) { + if (field.isPresent()) { + try { + result.put(key, field.get()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } else { + try { + result.put(key, JSONObject.NULL); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + }*/ +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/service/EventService.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/service/EventService.java index 993dc69..e8b107e 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/service/EventService.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/service/EventService.java @@ -2,14 +2,23 @@ import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; +import com.tom.meeter.context.event.message.UpdateEventRequest; import com.tom.meeter.context.network.dto.EventDTO; import retrofit2.Call; +import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.PATCH; import retrofit2.http.Path; public interface EventService { + @GET("/event/{id}") Call getEvent(@Header(AUTH_HEADER) String authHeader, @Path("id") String eventId); + + @PATCH("/event/{id}") + Call updateEvent( + @Header(AUTH_HEADER) String authHeader, @Path("id") String eventId, + @Body UpdateEventRequest req); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/utils/Utils.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/utils/Utils.java new file mode 100644 index 0000000..7f57b4c --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/utils/Utils.java @@ -0,0 +1,55 @@ +package com.tom.meeter.context.event.utils; + +import static com.tom.meeter.infrastructure.common.CommonHelper.getFloatOrNull; +import static com.tom.meeter.infrastructure.common.CommonHelper.getOffsetDateTime; +import static com.tom.meeter.infrastructure.common.CommonHelper.getStringOrNull; + +import com.tom.meeter.context.event.message.UpdateEventRequest; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.databinding.ActivityEventEditableBinding; + +import java.time.OffsetDateTime; +import java.util.Objects; + +public class Utils { + private Utils() { + } + + public static UpdateEventRequest createUpdateEventRequest( + EventDTO event, ActivityEventEditableBinding eBinding) { + UpdateEventRequest req = new UpdateEventRequest(); + + String eventNameChange = getStringOrNull(eBinding.eventName.getText()); + if (!Objects.equals(event.getName(), eventNameChange)) { + req.setName(eventNameChange); + } + String eventDescrChange = getStringOrNull(eBinding.eventDescription.getText()); + if (!Objects.equals(event.getDescription(), eventDescrChange)) { + req.setDescription(eventDescrChange); + } + OffsetDateTime eventStartingChange = getOffsetDateTime(eBinding.eventStarting.getText()); + if (!Objects.equals(event.getStarting(), eventStartingChange)) { + req.setStarting(eventStartingChange); + } + OffsetDateTime eventEndingChange = getOffsetDateTime(eBinding.eventEnding.getText()); + if (!Objects.equals(event.getEnding(), eventEndingChange)) { + req.setEnding(eventEndingChange); + } + String eventCityChange = getStringOrNull(eBinding.eventCity.getText()); + if (!Objects.equals(event.getCity(), eventCityChange)) { + req.setCity(eventCityChange); + } + //TODO FLOAT -> DOUBLE + Float eventLatitudeChange = getFloatOrNull(eBinding.eventLatitude.getText()); + if (!Objects.equals(event.getLatitude(), eventLatitudeChange)) { + req.setLatitude(eventLatitudeChange); + } + //TODO FLOAT -> DOUBLE + Float eventLongitudeChange = getFloatOrNull(eBinding.eventLongitude.getText()); + if (!Objects.equals(event.getLongitude(), eventLongitudeChange)) { + req.setLongitude(eventLongitudeChange); + } + //TODO: eventCache.getPhotoPath(); + return req; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModel.java index 3d8546a..c403dc6 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModel.java @@ -10,14 +10,16 @@ import androidx.lifecycle.ViewModel; import com.tom.meeter.context.event.service.EventService; +import com.tom.meeter.context.image.ImageDownloader; import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.user.viewmodel.UserViewModel; import com.tom.meeter.infrastructure.common.Globals; -import com.tom.meeter.infrastructure.http.DisconnectLogger; +import com.tom.meeter.infrastructure.http.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; import javax.inject.Inject; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Response; @@ -26,28 +28,40 @@ public class EventViewModel extends ViewModel { private static final String TAG = UserViewModel.class.getCanonicalName(); private final MutableLiveData eventLiveData = new MutableLiveData<>(); + private final MutableLiveData eventPhotoLiveData = new MutableLiveData<>(); private final EventService eventService; + private final ImageDownloader imageDownloader; @Inject - public EventViewModel(EventService eventService) { + public EventViewModel(EventService eventService, ImageDownloader imageDownloader) { logMethod(TAG, this); this.eventService = eventService; + this.imageDownloader = imageDownloader; } public void fetchEventInformation(String token, String eventId, Activity activity) { eventService.getEvent(Globals.getAuthHeader(token), eventId).enqueue( - new DisconnectLogger<>(activity) { + new ErrorLogger<>(activity) { @Override public void onResponse(Call call, Response response) { - if (response.code() == HttpCodes.OK && response.body() != null) { - eventLiveData.setValue(response.body()); + EventDTO body = response.body(); + if (response.code() == HttpCodes.OK && body != null) { + eventLiveData.setValue(body); + String photoPath = body.getPhotoPath(); + if (photoPath != null) { + imageDownloader.downloadEventImage( + photoPath, + activity.getApplicationContext(), + eventPhotoLiveData::setValue, + activity::recreate); + } return; } if (response.code() == HttpCodes.NOT_AUTHENTICATED) { activity.recreate(); } - Log.i(TAG, "/event/{id}: " + response.code() + " : " + response.body()); + Log.i(TAG, "/event/{id}: " + response.code() + " : " + body); } } ); @@ -62,5 +76,9 @@ protected void onCleared() { public LiveData getEventLiveData() { return eventLiveData; } + + public LiveData getEventPhotoLiveData() { + return eventPhotoLiveData; + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModelModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModelModule.java new file mode 100644 index 0000000..d79434b --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/viewmodel/EventViewModelModule.java @@ -0,0 +1,17 @@ +package com.tom.meeter.context.event.viewmodel; + +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelKey; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class EventViewModelModule { + @Binds + @IntoMap + @ViewModelKey(EventViewModel.class) + abstract ViewModel eventViewModel(EventViewModel eventViewModel); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageComponent.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageComponent.java new file mode 100644 index 0000000..c4e103f --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageComponent.java @@ -0,0 +1,23 @@ +package com.tom.meeter.context.image; + +import android.app.Application; + +import javax.inject.Singleton; + +import dagger.BindsInstance; +import dagger.Component; + +@Singleton +@Component(modules = {ImageModule.class}) +public interface ImageComponent { + + ImageDownloader provideImageDownloader(); + + @Component.Builder + interface Builder { + @BindsInstance + Builder application(Application application); + + ImageComponent build(); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageDownloader.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageDownloader.java new file mode 100644 index 0000000..d143032 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageDownloader.java @@ -0,0 +1,75 @@ +package com.tom.meeter.context.image; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.content.Context; +import android.util.Log; + +import com.tom.meeter.context.image.service.ImageService; +import com.tom.meeter.infrastructure.http.ErrorLogger; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import java.util.function.Consumer; + +import javax.inject.Inject; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + +public class ImageDownloader { + + private static final String TAG = ImageDownloader.class.getCanonicalName(); + private final ImageService imageService; + + @Inject + public ImageDownloader(ImageService imageService) { + logMethod(TAG, this); + this.imageService = imageService; + } + + public void downloadEventImage( + String photoPath, Context ctx, + Consumer onDownloaded, Runnable onNotAuthenticated) { + imageService.downloadEventImage(getAuthHeader(AccountManager.get(ctx)), photoPath) + .enqueue(new ErrorLogger<>(ctx) { + @Override + public void onResponse(Call call, Response response) { + //Log.d(TAG, "/images/event" + photoPath + " downloaded..."); + try (ResponseBody body = response.body()) { + if (response.code() == HttpCodes.OK && body != null) { + onDownloaded.accept(response.body()); + return; + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + onNotAuthenticated.run(); + } + Log.i(TAG, "/images/event/: " + response.code() + " : " + body); + } + } + }); + } + + public void downloadUserImage( + String photoPath, Context ctx, + Consumer onDownloaded, Runnable onNotAuthenticated) { + imageService.downloadUserImage(getAuthHeader(AccountManager.get(ctx)), photoPath) + .enqueue(new ErrorLogger<>(ctx) { + @Override + public void onResponse(Call call, Response response) { + try (ResponseBody body = response.body()) { + if (response.code() == HttpCodes.OK && body != null) { + onDownloaded.accept(response.body()); + return; + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + onNotAuthenticated.run(); + } + Log.i(TAG, "/images/user/: " + response.code() + " : " + body); + } + } + }); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageModule.java new file mode 100644 index 0000000..54f7b68 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageModule.java @@ -0,0 +1,43 @@ +package com.tom.meeter.context.image; + +import static com.tom.meeter.infrastructure.common.Globals.getServerPath; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Application; + +import androidx.annotation.NonNull; + +import com.tom.meeter.context.image.service.ImageService; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import retrofit2.Retrofit; + +@Module +public class ImageModule { + + private static final String TAG = ImageModule.class.getCanonicalName(); + + public ImageModule() { + logMethod(TAG, this); + } + + @Singleton + @NonNull + @Provides + public ImageService provideImageService(Application app) { + return new Retrofit.Builder() + .baseUrl(getServerPath(app)) + .build() + .create(ImageService.class); + } + + @Singleton + @NonNull + @Provides + public ImageDownloader provideImageDownloader(ImageService imageService) { + return new ImageDownloader(imageService); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/service/ImageService.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/service/ImageService.java new file mode 100644 index 0000000..03964a3 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/service/ImageService.java @@ -0,0 +1,22 @@ +package com.tom.meeter.context.image.service; + +import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Path; + +public interface ImageService { + + @GET("/images/event{imagePath}") + Call downloadEventImage( + @Header(AUTH_HEADER) String authHeader, + @Path(value = "imagePath", encoded = true) String imagePath); + + @GET("/images/user{imagePath}") + Call downloadUserImage( + @Header(AUTH_HEADER) String authHeader, + @Path(value = "imagePath", encoded = true) String imagePath); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/launcher/Launcher.java b/AndroidClient/src/main/java/com/tom/meeter/context/launcher/Launcher.java index 2167c48..b437678 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/launcher/Launcher.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/launcher/Launcher.java @@ -24,8 +24,8 @@ import com.tom.meeter.App; import com.tom.meeter.R; -import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.context.profile.activity.ProfileActivity; +import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.databinding.LauncherBinding; import java.io.IOException; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/EventDTO.java b/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/EventDTO.java index 0813221..65cb544 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/EventDTO.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/EventDTO.java @@ -1,16 +1,18 @@ package com.tom.meeter.context.network.dto; -import com.google.gson.annotations.SerializedName; +import androidx.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; import org.json.JSONException; import org.json.JSONObject; +import java.time.OffsetDateTime; import java.util.Objects; /** * created by Tom on 10.02.2017. */ - public class EventDTO { private static final String EVENT_ID_KEY = "id"; @@ -22,130 +24,171 @@ public class EventDTO { private static final String CREATED_KEY = "created"; private static final String STARTING_KEY = "starting"; private static final String ENDING_KEY = "ending"; + private static final String PHOTO_PATH_KEY = "photo_path"; + private static final String CITY_KEY = "city"; private String id; private String name; private String description; - private double latitude; - private double longitude; - @SerializedName(value = CREATOR_ID_KEY) + private Double latitude; + private Double longitude; + @JsonProperty(value = CREATOR_ID_KEY) private String creatorId; - private String created; - private String starting; - private String ending; - - public EventDTO() { - } + private OffsetDateTime created; + private OffsetDateTime starting; + private OffsetDateTime ending; + private String city; + @JsonProperty(value = PHOTO_PATH_KEY) + private String photoPath; public static EventDTO encode(JSONObject json) { EventDTO result = new EventDTO(); try { + //Non nullable. result.id = json.getString(EVENT_ID_KEY); result.name = json.getString(NAME_KEY); - result.description = json.getString(DESCRIPTION_KEY); result.creatorId = json.getString(CREATOR_ID_KEY); - result.latitude = json.getDouble(LATITUDE_KEY); - result.longitude = json.getDouble(LONGITUDE_KEY); - result.created = json.getString(CREATED_KEY); - result.starting = json.getString(STARTING_KEY); - result.ending = json.getString(ENDING_KEY); + result.created = OffsetDateTime.parse(json.getString(CREATED_KEY)); + + //Nullable. + result.description = getStringOrNull(DESCRIPTION_KEY, json); + result.latitude = getDoubleOrNull(LATITUDE_KEY, json); + result.longitude = getDoubleOrNull(LONGITUDE_KEY, json); + result.starting = getOffsetDateTimeOrNull(STARTING_KEY, json); + result.ending = getOffsetDateTimeOrNull(ENDING_KEY, json); + result.photoPath = getStringOrNull(PHOTO_PATH_KEY, json); + result.city = getStringOrNull(CITY_KEY, json); } catch (JSONException e) { - throw new RuntimeException("Unable to encode EventDTO from jsonObject, ", e); + throw new RuntimeException("Unable to encode EventDTO from jsonObject: ", e); } return result; } - public String getId() { - return id; + public void setName(String name) { + this.name = name; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; } public void setId(String id) { this.id = id; } - public String getName() { - return name; + public void setDescription(String description) { + this.description = description; } - public void setName(String name) { - this.name = name; + public void setCreatorId(String creatorId) { + this.creatorId = creatorId; } - public String getDescription() { - return description; + public void setCreated(OffsetDateTime created) { + this.created = created; } - public void setDescription(String description) { - this.description = description; + public void setStarting(OffsetDateTime starting) { + this.starting = starting; } - public double getLatitude() { - return latitude; + public void setEnding(OffsetDateTime ending) { + this.ending = ending; } - public void setLatitude(double latitude) { - this.latitude = latitude; + public void setCity(String city) { + this.city = city; } - public double getLongitude() { - return longitude; + public void setPhotoPath(String photoPath) { + this.photoPath = photoPath; } - public void setLongitude(double longitude) { - this.longitude = longitude; + public String getId() { + return id; } - public String getCreatorId() { - return creatorId; + public String getName() { + return name; } - public void setCreatorId(String creatorId) { - this.creatorId = creatorId; + public String getDescription() { + return description; } - public String getCreated() { - return created; + public Double getLatitude() { + return latitude; } - public void setCreated(String created) { - this.created = created; + public Double getLongitude() { + return longitude; } - public String getStarting() { - return starting; + public String getCreatorId() { + return creatorId; } - public void setStarting(String starting) { - this.starting = starting; + public OffsetDateTime getCreated() { + return created; + } + + public OffsetDateTime getStarting() { + return starting; } - public String getEnding() { + public OffsetDateTime getEnding() { return ending; } - public void setEnding(String ending) { - this.ending = ending; + public String getPhotoPath() { + return photoPath; + } + + public String getCity() { + return city; } @Override public boolean equals(Object o) { - if (!(o instanceof EventDTO eventDTO)) { - return false; - } - return Double.compare(latitude, eventDTO.latitude) == 0 - && Double.compare(longitude, eventDTO.longitude) == 0 - && Objects.equals(id, eventDTO.id) + if (o == null || getClass() != o.getClass()) return false; + EventDTO eventDTO = (EventDTO) o; + return Objects.equals(id, eventDTO.id) && Objects.equals(name, eventDTO.name) && Objects.equals(description, eventDTO.description) + && Objects.equals(latitude, eventDTO.latitude) + && Objects.equals(longitude, eventDTO.longitude) && Objects.equals(creatorId, eventDTO.creatorId) && Objects.equals(created, eventDTO.created) && Objects.equals(starting, eventDTO.starting) - && Objects.equals(ending, eventDTO.ending); + && Objects.equals(ending, eventDTO.ending) + && Objects.equals(city, eventDTO.city) + && Objects.equals(photoPath, eventDTO.photoPath); } @Override public int hashCode() { - return Objects.hash(id, name, description, latitude, longitude, - creatorId, created, starting, ending); + return Objects.hash( + id, name, description, latitude, longitude, creatorId, + created, starting, ending, city, photoPath); + } + + @Nullable + private static String getStringOrNull(String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : json.getString(key); + } + + @Nullable + private static OffsetDateTime getOffsetDateTimeOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : OffsetDateTime.parse(json.getString(key)); + } + + @Nullable + private static Double getDoubleOrNull(String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : json.getDouble(key); } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/UserDTO.java b/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/UserDTO.java new file mode 100644 index 0000000..695772a --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/network/dto/UserDTO.java @@ -0,0 +1,6 @@ +package com.tom.meeter.context.network.dto; + + +//TODO: make me +public class UserDTO { +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/network/service/SocketIOService.java b/AndroidClient/src/main/java/com/tom/meeter/context/network/service/SocketIOService.java index 847ec05..e13b3c5 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/network/service/SocketIOService.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/network/service/SocketIOService.java @@ -1,6 +1,8 @@ package com.tom.meeter.context.network.service; import static com.tom.meeter.context.auth.infrastructure.AuthHelper.peekToken; +import static com.tom.meeter.context.network.utils.SocketIOCodes.EVENT_CREATED_CODE; +import static com.tom.meeter.context.notification.NotificationHelper.sendNotificationEventCreated; import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; import static com.tom.meeter.infrastructure.common.Globals.getSocketIOPath; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; @@ -17,6 +19,7 @@ import com.tom.meeter.context.network.domain.CreateNewEventAttempt; import com.tom.meeter.context.network.domain.SearchForEvents; +import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.infrastructure.common.Globals; import com.tom.meeter.infrastructure.common.JsonHelper; import com.tom.meeter.infrastructure.eventbus.events.FailureEventCreation; @@ -26,6 +29,7 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; @@ -48,6 +52,7 @@ public class SocketIOService extends Service { private static final String GREETINGS_CHANNEL = "greetings"; private static final String EVENTS_CREATE_CHANNEL = "events:create"; private static final String EVENTS_SEARCH_CHANNEL = "events:search"; + private static final String EVENTS_NOTIFICATIONS_CHANNEL = "events:notifications"; private static final String CODE_KEY = "code"; private static final String ID_KEY = "id"; @@ -133,7 +138,7 @@ private void initializeSocketClient( Log.d(TAG, "SocketIOService is not going to initialize, since it is already initialized."); return; } - String uri = getSocketIOPath(getBaseContext()); + String uri = getSocketIOPath(getApplicationContext()); Log.d(TAG, "Configuring SocketIOClient for server: " + uri); socketClient = IO.socket(uri, setupOptions(authToken)); @@ -154,7 +159,7 @@ private void initializeSocketClient( if (cause instanceof IOException ioException) { - if (cause instanceof SocketTimeoutException socketTimeoutException) { + if (cause instanceof SocketTimeoutException ste) { Log.i(TAG, "SocketIOService received SocketTimeoutException. Server is unavailable."); return; } @@ -172,6 +177,7 @@ private void initializeSocketClient( socketClient.on(GREETINGS_CHANNEL, SocketIOService::greetingsHandler); socketClient.on(EVENTS_SEARCH_CHANNEL, SocketIOService::eventsSearchHandler); + socketClient.on(EVENTS_NOTIFICATIONS_CHANNEL, this::eventsNotificationsChannel); socketClient.on(EVENTS_CREATE_CHANNEL, SocketIOService::eventsCreateHandler); socketClient.connect(); EventBus.getDefault().register(this); @@ -204,6 +210,7 @@ private void disconnect() { socketClient.disconnect(); socketClient.off(GREETINGS_CHANNEL, SocketIOService::greetingsHandler); socketClient.off(EVENTS_SEARCH_CHANNEL, SocketIOService::eventsSearchHandler); + socketClient.off(EVENTS_NOTIFICATIONS_CHANNEL, this::eventsNotificationsChannel); socketClient.off(EVENTS_CREATE_CHANNEL, SocketIOService::eventsCreateHandler); initialized = false; } @@ -253,6 +260,19 @@ private static void eventsSearchHandler(Object... args) { EventBus.getDefault().post(IncomeEvents.fromJsonArray(response)); } + private void eventsNotificationsChannel(Object... args) { + JSONObject response = getSimpleResponse(JSONObject.class, args); + Log.d(TAG, EVENTS_NOTIFICATIONS_CHANNEL + " : " + response); + try { + if (response.getInt("code") == EVENT_CREATED_CODE) { + EventDTO event = EventDTO.encode(response.getJSONObject("message")); + sendNotificationEventCreated(this, event); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + private static T getSimpleResponse(Class aClass, Object[] args) { if (!validateSingleMessageResponse(aClass, args)) { throw new RuntimeException("Incorrect response for " + aClass + " with response " + Arrays.toString(args)); diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/network/utils/SocketIOCodes.java b/AndroidClient/src/main/java/com/tom/meeter/context/network/utils/SocketIOCodes.java new file mode 100644 index 0000000..733029f --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/network/utils/SocketIOCodes.java @@ -0,0 +1,9 @@ +package com.tom.meeter.context.network.utils; + +public final class SocketIOCodes { + + public static int EVENT_CREATED_CODE = 100; + + private SocketIOCodes() { + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/notification/NotificationHelper.java b/AndroidClient/src/main/java/com/tom/meeter/context/notification/NotificationHelper.java new file mode 100644 index 0000000..def249c --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/notification/NotificationHelper.java @@ -0,0 +1,91 @@ +package com.tom.meeter.context.notification; + +import android.Manifest; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.tom.meeter.R; +import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.network.dto.EventDTO; + +public class NotificationHelper { + + private static final String EVENTS_NOTIFY = "events_notify"; + private static final String ALL_EVENTS_GROUP = "com.tom.meeter.all_events_group"; + private static final int SUMMARY_ID = 9999; + + public static void createNotificationChannel(Context ctx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "New events"; + String description = "Information about newly created events"; + int importance = NotificationManager.IMPORTANCE_DEFAULT; + + NotificationChannel channel = new NotificationChannel(EVENTS_NOTIFY, name, importance); + channel.setDescription(description); + + NotificationManager notificationManager = ctx.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + public static void sendNotificationEventCreated(Context ctx, EventDTO event) { + if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return; + } + NotificationManagerCompat mgr = NotificationManagerCompat.from(ctx); + mgr.notify(event.getId().hashCode(), getNotification(ctx, event)); + mgr.notify(SUMMARY_ID, getSummaryNotification(ctx)); + } + + private static Notification getNotification(Context ctx, EventDTO event) { + return new NotificationCompat.Builder(ctx, EVENTS_NOTIFY) + .setSmallIcon(R.drawable.ic_meeter_lr) + .setContentTitle(ctx.getString(R.string.notification_new_event)) + //.setContentTitle(event.getCreatorId() + " published new event!") + .setContentText(event.getName()) + .setStyle(getBigStyle(event.getDescription())) + .setContentIntent(createPendingIntent(ctx, event)) + .setGroup(ALL_EVENTS_GROUP) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .build(); + } + + private static Notification getSummaryNotification(Context ctx) { + return new NotificationCompat.Builder(ctx, EVENTS_NOTIFY) + .setSmallIcon(R.drawable.ic_meeter_lr) + .setGroup(ALL_EVENTS_GROUP) + .setGroupSummary(true) + .build(); + } + + private static NotificationCompat.BigTextStyle getBigStyle(String descr) { + return new NotificationCompat.BigTextStyle().bigText(descr); + } + + private static PendingIntent createPendingIntent(Context ctx, EventDTO event) { + Intent intent = EventActivity.createEventActivityIntent(ctx, event.getId()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + return PendingIntent.getActivity( + ctx, event.getId().hashCode(), intent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/ProfileActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/ProfileActivity.java index 4a78c3b..42478b6 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/ProfileActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/ProfileActivity.java @@ -2,6 +2,7 @@ import static androidx.preference.PreferenceManager.getDefaultSharedPreferences; import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getSingleAccount; import static com.tom.meeter.context.auth.infrastructure.AuthHelper.invalidateToken; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; @@ -48,19 +49,18 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.tom.meeter.App; import com.tom.meeter.R; -import com.tom.meeter.context.auth.infrastructure.AccountAuthenticator; -import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.context.network.service.SocketIOService; import com.tom.meeter.context.profile.fragment.CreateNewEventFragment; import com.tom.meeter.context.profile.fragment.EventsFragment; +import com.tom.meeter.context.profile.fragment.ProfileEventsFragment; import com.tom.meeter.context.profile.fragment.ProfileFragment; -import com.tom.meeter.context.profile.fragment.UserEventsFragment; import com.tom.meeter.context.profile.service.ProfileService; import com.tom.meeter.context.profile.settings.message.SettingsResponse; import com.tom.meeter.context.profile.settings.service.SettingsService; +import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.databinding.ProfileActivityBinding; import com.tom.meeter.infrastructure.common.Globals; -import com.tom.meeter.infrastructure.http.DisconnectLogger; +import com.tom.meeter.infrastructure.http.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; import java.util.HashMap; @@ -200,7 +200,7 @@ private void onInit(String token, boolean isSavedInstanceStateExist) { private void setupPreferences(String token) { Call settings = settingsService.getSettings(Globals.getAuthHeader(token)); settings.enqueue( - new DisconnectLogger<>(this) { + new ErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { if (res.code() == HttpCodes.NOT_AUTHENTICATED) { @@ -222,7 +222,7 @@ public void onResponse(Call call, Response r private void setupPreferencesRetry(String freshToken) { settingsService.getSettings(Globals.getAuthHeader(freshToken)) - .enqueue(new DisconnectLogger<>(this) { + .enqueue(new ErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { if (res.code() == HttpCodes.NOT_FOUND) { @@ -388,11 +388,11 @@ private void renderSelectedFragment() { private void handleLogout() { getDefaultSharedPreferences(ProfileActivity.this) .edit().clear().apply(); - Account[] accs = accountManager.getAccountsByType(AccountAuthenticator.ACCOUNT_TYPE); + Account acc = getSingleAccount(accountManager); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { accountManager.removeAccount( - accs[0], this, future -> { - Log.d(TAG, "Account '" + accs[0].name + "' removed."); + acc, this, future -> { + Log.d(TAG, "Account '" + acc.name + "' removed."); unbindSocketService(); finishAndRemoveTask(); }, null); @@ -431,7 +431,7 @@ private static Fragment createFragment(long navigationMenuIndex) { } else if (navigationMenuIndex == DRAWER_NEW_EVENT_ID) { result = new CreateNewEventFragment(); } else if (navigationMenuIndex == DRAWER_NOTIFICATION_ID) { - result = new UserEventsFragment(); + result = new ProfileEventsFragment(); } else { result = new ProfileFragment(); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SettingsActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SettingsActivity.java index 54420e1..bdd37cd 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SettingsActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SettingsActivity.java @@ -26,7 +26,7 @@ import com.tom.meeter.databinding.SettingsActivityBinding; import com.tom.meeter.infrastructure.common.Globals; import com.tom.meeter.infrastructure.common.PreferencesHelper; -import com.tom.meeter.infrastructure.http.DisconnectLogger; +import com.tom.meeter.infrastructure.http.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; import javax.inject.Inject; @@ -112,7 +112,7 @@ private void sendSavePrefs(int searchArea, boolean trackUser) { settingsService.createOrUpdateSettings( new SettingsCreateOrUpdate(searchArea, trackUser), Globals.getAuthHeader(AuthHelper.peekToken(accountManager))) - .enqueue(new DisconnectLogger<>(this) { + .enqueue(new ErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { if (res.code() == HttpCodes.NOT_AUTHENTICATED) { @@ -135,7 +135,7 @@ private void sendSavePrefsRetry(String token, int searchArea, boolean trackUser) settingsService.createOrUpdateSettings( new SettingsCreateOrUpdate(searchArea, trackUser), Globals.getAuthHeader(token)) - .enqueue(new DisconnectLogger<>(this) { + .enqueue(new ErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { if (res.code() == HttpCodes.OK || res.code() == HttpCodes.CREATED) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/ActiveEventsAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/ActiveEventsAdapter.java new file mode 100644 index 0000000..9c563b8 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/ActiveEventsAdapter.java @@ -0,0 +1,35 @@ +package com.tom.meeter.context.profile.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.databinding.EventViewBinding; +import com.tom.meeter.infrastructure.components.adapter.BaseEventAdapter; +import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; +import com.tom.meeter.infrastructure.components.viewholder.EventViewHolder; + +/** + * created by Tom on 10.02.2017. + */ +public class ActiveEventsAdapter extends BaseEventAdapter { + + private static final String TAG = ActiveEventsAdapter.class.getCanonicalName(); + + public ActiveEventsAdapter(ViewHolderEventBinder binder) { + super(binder); + } + + @Override + public EventViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new EventViewHolder( + EventViewBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventsAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventsAdapter.java new file mode 100644 index 0000000..444dc25 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventsAdapter.java @@ -0,0 +1,40 @@ +package com.tom.meeter.context.profile.adapter; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.databinding.EventViewBinding; +import com.tom.meeter.infrastructure.components.adapter.BaseEventAdapter; +import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; +import com.tom.meeter.infrastructure.components.viewholder.EventViewHolder; + +/** + * created by Tom on 10.02.2017. + */ +public class EventsAdapter extends BaseEventAdapter { + + private static final String TAG = EventsAdapter.class.getCanonicalName(); + + public EventsAdapter(ViewHolderEventBinder binder) { + super(binder); + logMethod(TAG, this); + } + + @Override + public EventViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + logMethod(TAG, this); + return new EventViewHolder( + EventViewBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + logMethod(TAG, this); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewActiveEventsAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewActiveEventsAdapter.java deleted file mode 100644 index 8da7a35..0000000 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewActiveEventsAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.tom.meeter.context.profile.adapter; - -import static com.tom.meeter.infrastructure.Image.ImagesHelper.getCircleBitmap; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.randomPicResource; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.recyclerview.widget.RecyclerView; - -import com.tom.meeter.context.event.activity.EventActivity; -import com.tom.meeter.context.network.dto.EventDTO; -import com.tom.meeter.databinding.EventViewBinding; - -import java.util.ArrayList; -import java.util.List; - -/** - * created by Tom on 10.02.2017. - */ -public class RecycleViewActiveEventsAdapter extends RecyclerView.Adapter { - private static final String TAG = RecycleViewUserEventsAdapter.class.getCanonicalName(); - private final List events = new ArrayList<>(); - private Context ctx; - - public RecycleViewActiveEventsAdapter() { - } - - //TODO remove me when image can be downloaded from the server - public RecycleViewActiveEventsAdapter(Context ctx) { - this.ctx = ctx; - } - - public List getEvents() { - return events; - } - - public void addEvent(EventDTO event) { - events.add(event); - } - - public void addEvents(List events) { - this.events.addAll(events); - } - - public void cleanEvents() { - events.clear(); - } - - - @Override - public EventViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new EventViewHolder( - EventViewBinding.inflate( - LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(EventViewHolder holder, int position) { - EventDTO event = events.get(position); - - Bitmap src = BitmapFactory.decodeResource(ctx.getResources(), randomPicResource()); - Bitmap scaled = Bitmap.createScaledBitmap(src, 150, 150, true); - Bitmap circled = getCircleBitmap(scaled); - - holder.bind(event.getName(), event.getDescription(), circled, - v -> ctx.startActivity( - new Intent(ctx, EventActivity.class) - .putExtra(EventActivity.EVENT_ID_KEY, event.getId()))); - } - - @Override - public int getItemCount() { - return events.size(); - } - - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - } -} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewUserEventsAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewUserEventsAdapter.java deleted file mode 100644 index c0b3f8c..0000000 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/RecycleViewUserEventsAdapter.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.tom.meeter.context.profile.adapter; - -import static com.tom.meeter.infrastructure.Image.ImagesHelper.getCircleBitmap; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.randomPicResource; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.RecyclerView; - -import com.tom.meeter.context.event.activity.EventActivity; -import com.tom.meeter.context.network.dto.EventDTO; -import com.tom.meeter.databinding.EventViewBinding; - -import java.util.ArrayList; -import java.util.List; - -/** - * created by Tom on 10.02.2017. - */ - -public class RecycleViewUserEventsAdapter extends RecyclerView.Adapter { - private final List events = new ArrayList<>(); - - //TODO remove me when ... - private Context ctx; - public RecycleViewUserEventsAdapter(Context ctx) { - this.ctx = ctx; - } - - public RecycleViewUserEventsAdapter() { - } - - public void setData(List events) { - EventDiffCallback eventDiffCallback = new EventDiffCallback(this.events, events); - DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(eventDiffCallback); - this.events.clear(); - this.events.addAll(events); - diffResult.dispatchUpdatesTo(this); - //Do we need it? - notifyDataSetChanged(); - } - - @Override - public EventViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new EventViewHolder( - EventViewBinding.inflate( - LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(EventViewHolder holder, int position) { - EventDTO event = events.get(position); - - Bitmap src = BitmapFactory.decodeResource(ctx.getResources(), randomPicResource()); - Bitmap scaled = Bitmap.createScaledBitmap(src, 150, 150, true); - Bitmap circled = getCircleBitmap(scaled); - - holder.bind(event.getName(), event.getDescription(), circled, - v -> ctx.startActivity( - new Intent(ctx, EventActivity.class) - .putExtra(EventActivity.EVENT_ID_KEY, event.getId()))); - /* - btnDelete.setOnClickListener(v -> { - if (onDeleteButtonClickListener != null) - onDeleteButtonClickListener.onDeleteButtonClicked(post); - });*/ - } - - @Override - public int getItemCount() { - return events.size(); - } - - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - } - - private static class EventDiffCallback extends DiffUtil.Callback { - - private final List oldPosts, newPosts; - - EventDiffCallback(List oldPosts, List newPosts) { - this.oldPosts = oldPosts; - this.newPosts = newPosts; - } - - @Override - public int getOldListSize() { - return oldPosts.size(); - } - - @Override - public int getNewListSize() { - return newPosts.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return oldPosts.get(oldItemPosition).getId().equals(newPosts.get(newItemPosition).getId()); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return oldPosts.get(oldItemPosition).equals(newPosts.get(newItemPosition)); - } - } - -} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/domain/GMapEvent.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/domain/GMapEvent.java index 071d9b2..75cee25 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/domain/GMapEvent.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/domain/GMapEvent.java @@ -8,7 +8,7 @@ public class GMapEvent { - private EventDTO event; + private final EventDTO event; private Marker marker; public GMapEvent(EventDTO event, Marker marker) { @@ -21,6 +21,10 @@ public void removeMarker() { marker.remove(); } + public EventDTO getEvent() { + return event; + } + public String getName() { return event.getName(); } @@ -98,4 +102,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(event, marker); } -} \ No newline at end of file +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/event/repository/EventRepository.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/event/repository/EventRepository.java index ff0b100..0f1fa67 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/event/repository/EventRepository.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/event/repository/EventRepository.java @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData; -import com.tom.meeter.AppScope; import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.profile.event.database.EventDao; import com.tom.meeter.context.profile.event.domain.Event; @@ -15,11 +14,10 @@ import java.util.List; import java.util.concurrent.Executor; -import javax.inject.Inject; - import retrofit2.Response; -@AppScope +//@AppScope +@Deprecated public class EventRepository { private static final String TAG = EventRepository.class.getCanonicalName(); @@ -28,7 +26,7 @@ public class EventRepository { private final EventDao eventDao; private final Executor executor; - @Inject + //@Inject public EventRepository(EventService eventService, EventDao eventDao, Executor executor) { this.eventService = eventService; this.eventDao = eventDao; @@ -60,8 +58,8 @@ private void refreshUserEvents(String userId) { body.stream() .forEach(i -> result.add( new Event(i.getId(), i.getName(), i.getDescription(), i.getLatitude(), - i.getLongitude(), i.getCreatorId(), i.getCreated(), i.getStarting(), - i.getEnding()) + i.getLongitude(), i.getCreatorId(), null/*i.getCreated()*/, null/*i.getStarting()*/, + null/*i.getEnding()*/) )); eventDao.deleteByUserId(userId); eventDao.saveAll(result); diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ActiveEventsFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ActiveEventsFragment.java index 56fc0c1..40bc7a6 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ActiveEventsFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ActiveEventsFragment.java @@ -4,8 +4,10 @@ * Created by Tom on 09.12.2016. */ +import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -17,21 +19,30 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; -import com.tom.meeter.context.profile.adapter.RecycleViewActiveEventsAdapter; +import com.tom.meeter.App; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.adapter.EventsAdapter; import com.tom.meeter.databinding.SubFragmentActiveEventsBinding; +import com.tom.meeter.infrastructure.common.InfrastructureHelper; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheEventEventBinder; import com.tom.meeter.infrastructure.eventbus.events.IncomeEvents; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import javax.inject.Inject; + public class ActiveEventsFragment extends Fragment { private static final String TAG = ActiveEventsFragment.class.getCanonicalName(); SubFragmentActiveEventsBinding binding; - private RecycleViewActiveEventsAdapter recycleViewActiveEventsAdapter; + @Inject + ImageDownloader imageDownloader; + + private EventsAdapter adapter; public ActiveEventsFragment() { logMethod(TAG, this); @@ -41,7 +52,18 @@ public ActiveEventsFragment() { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); logMethod(TAG, this); + + ((App) getActivity().getApplication()).getComponent().inject(this); + EventBus.getDefault().register(this); + + Context ctx = getContext(); + adapter = new EventsAdapter( + new PhotoDownloaderWithCacheEventEventBinder( + ctx, imageDownloader, + (e) -> dispatchToEventActivity(ctx, e.getId()), + () -> InfrastructureHelper.restartActivityFromFragment(this))); + Log.d(TAG, "ActiveEventsFragment Registering eventBus"); } @@ -57,27 +79,13 @@ public View onCreateView( public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); logMethod(TAG, this); - - // use this setting to improve performance if you know that changes - // in content do not change the layout size of the RecyclerView - //rView.setHasFixedSize(true); - - // use a linear layout manager binding.activeEventsFragmentRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - - // specify an adapter (see also next example) - recycleViewActiveEventsAdapter = new RecycleViewActiveEventsAdapter(getContext()); - binding.activeEventsFragmentRecyclerView.setAdapter(recycleViewActiveEventsAdapter); - binding.activeEventsFragmentRecyclerView.invalidate(); + binding.activeEventsFragmentRecyclerView.setAdapter(adapter); } @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(IncomeEvents eventsSearch) { - recycleViewActiveEventsAdapter.cleanEvents(); - if (!eventsSearch.events().isEmpty()) { - recycleViewActiveEventsAdapter.addEvents(eventsSearch.events()); - } - binding.activeEventsFragmentRecyclerView.requestLayout(); + adapter.setData(eventsSearch.events()); } @Override diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/CreateNewEventFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/CreateNewEventFragment.java index 368c7b2..ed7f8a7 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/CreateNewEventFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/CreateNewEventFragment.java @@ -1,6 +1,7 @@ package com.tom.meeter.context.profile.fragment; import static android.content.Context.BIND_AUTO_CREATE; +import static com.tom.meeter.infrastructure.common.CommonHelper.EMPTY_STR; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.annotation.SuppressLint; @@ -50,7 +51,6 @@ public class CreateNewEventFragment extends Fragment { private static final String TAG = CreateNewEventFragment.class.getCanonicalName(); - private static final String EMPTY_STR = ""; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm"); diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/EventsFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/EventsFragment.java index 84b3783..d5704a4 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/EventsFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/EventsFragment.java @@ -65,7 +65,7 @@ private static ViewPagerAdapter createViewPagerAdapter( ViewPagerAdapter adapter = new ViewPagerAdapter(fMgr); adapter.addFragment(new GoogleMapsFragment(), mapTitle); adapter.addFragment(new ActiveEventsFragment(), eventsTitle); - adapter.addFragment(new UserEventsFragment(), yourEventsTitle); + adapter.addFragment(new ProfileEventsFragment(), yourEventsTitle); return adapter; } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/GoogleMapsFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/GoogleMapsFragment.java index a04d448..f74e018 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/GoogleMapsFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/GoogleMapsFragment.java @@ -1,8 +1,8 @@ package com.tom.meeter.context.profile.fragment; import static android.content.Context.BIND_AUTO_CREATE; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.getCircleBitmap; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.randomPicResource; +import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.content.ComponentName; @@ -39,13 +39,15 @@ import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.common.collect.Sets; +import com.tom.meeter.App; import com.tom.meeter.R; -import com.tom.meeter.context.event.activity.EventActivity; import com.tom.meeter.context.gps.domain.LocationTrackerListener; import com.tom.meeter.context.gps.service.LocationTrackerService; +import com.tom.meeter.context.image.ImageDownloader; import com.tom.meeter.context.network.domain.SearchForEvents; import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.profile.domain.GMapEvent; +import com.tom.meeter.infrastructure.common.InfrastructureHelper; import com.tom.meeter.infrastructure.common.PreferencesHelper; import com.tom.meeter.infrastructure.eventbus.events.IncomeEvents; @@ -59,14 +61,16 @@ import java.util.Objects; import java.util.Set; +import javax.inject.Inject; + /** * Created by Tom on 09.12.2016. */ public class GoogleMapsFragment extends Fragment implements OnMapReadyCallback, LocationTrackerListener { + public static final float ZOOM_VALUE = 17; private static final String TAG = GoogleMapsFragment.class.getCanonicalName(); - private static final float ZOOM_VALUE = 17; private static final LatLng DEFAULT = new LatLng(0.0, 0.0); private ServiceConnection locationServiceConnection; @@ -86,6 +90,9 @@ public class GoogleMapsFragment extends Fragment private GMapEvent lastClickedEvent; + @Inject + ImageDownloader imageDownloader; + public GoogleMapsFragment() { logMethod(TAG, this); } @@ -103,6 +110,8 @@ public void onCreate(Bundle savedInstanceState) { logMethod(TAG, this); readPreferences(); + ((App) getActivity().getApplication()).getComponent().inject(this); + MapsInitializer.initialize(getContext()); userIcon = BitmapDescriptorFactory.fromBitmap( Bitmap.createScaledBitmap( @@ -241,7 +250,7 @@ private boolean markerClickListener(Marker marker) { } if (target.equals(lastClickedEvent)) { Log.d(TAG, "Double Click on: " + target.getName()); - startEventActivityFor(target.getId()); + dispatchToEventActivity(getContext(), target.getId()); return false; } lastClickedEvent = target; @@ -261,26 +270,27 @@ private GMapEvent searchForEvent(Marker marker) { } private void infoWindowClickListener(Marker marker) { - Log.d(TAG, "infoWindowClickListener() " + marker.getId() + ", is info shown ? " + marker.isInfoWindowShown()); + Log.d(TAG, "infoWindowClickListener() " + marker.getId() + + ", is info shown ? " + marker.isInfoWindowShown()); GMapEvent gMapEvent = searchForEvent(marker); if (gMapEvent != null) { - startEventActivityFor(gMapEvent.getId()); + dispatchToEventActivity(getContext(), gMapEvent.getId()); } } - private void startEventActivityFor(String eventId) { - startActivity(new Intent(this.getContext(), EventActivity.class) - .putExtra(EventActivity.EVENT_ID_KEY, eventId)); - } - private void putExistingMarkersOnMap() { for (GMapEvent e : events.values()) { - e.replaceMarker( - gmap.addMarker( - createEventMarkerOptions(e.getName(), e.getLatitude(), e.getLongitude()))); + e.replaceMarker(addMarkerWithPhoto(e.getEvent())); } } + private Marker addMarkerWithPhoto(EventDTO event) { + Marker marker = gmap.addMarker(createEventMarkerOptions( + event.getName(), event.getLatitude(), event.getLongitude())); + downloadPhotoForMarker(event.getPhotoPath(), marker); + return marker; + } + @Override public void onDestroy() { super.onDestroy(); @@ -315,11 +325,7 @@ public void onMessageEvent(IncomeEvents msg) { // Events to add - for (String eId : toAdd) { EventDTO ev = incomeEvents.get(eId); - events.put( - eId, - new GMapEvent( - ev, gmap.addMarker( - createEventMarkerOptions(ev.getName(), ev.getLatitude(), ev.getLongitude())))); + events.put(eId, new GMapEvent(ev, addMarkerWithPhoto(ev))); } // Intersection - need to apply events update, if any @@ -328,12 +334,26 @@ public void onMessageEvent(IncomeEvents msg) { } } + private void downloadPhotoForMarker(String photoPath, Marker marker) { + if (photoPath == null) { + return; + } + imageDownloader.downloadEventImage( + photoPath, getContext(), + photo -> { + if (photo != null) { + marker.setIcon(BitmapDescriptorFactory.fromBitmap(circleImage(photo))); + } + }, + () -> InfrastructureHelper.restartActivityFromFragment(this)); + } + private void readPreferences() { searchArea = PreferencesHelper.getSearchArea(getContext()); trackUser = PreferencesHelper.getNeedTrackUser(getContext()); } - private static void moveCamera( + public static void moveCamera( LatLng lastKnownUserLocation, GoogleMap gmap, boolean firstOpening, CameraPosition camPosition) { if (firstOpening) { if (lastKnownUserLocation != null) { @@ -354,6 +374,7 @@ private static void updateWith(GMapEvent me, EventDTO update) { + " " + update.getId() + " is changed. Moving the marker."); me.updatePosition(update.getLatitude(), update.getLongitude()); } + //TODO Every field could changed... } private MarkerOptions createUserMarkerOptions(LatLng latLng) { @@ -363,17 +384,14 @@ private MarkerOptions createUserMarkerOptions(LatLng latLng) { .position(latLng); } - private MarkerOptions createEventMarkerOptions(String name, double latitude, double longitude) { - //TODO Download event photo - Bitmap src = BitmapFactory.decodeResource(getContext().getResources(), randomPicResource()); + private MarkerOptions createEventMarkerOptions( + String name, double latitude, double longitude) { return new MarkerOptions() .position(new LatLng(latitude, longitude)) - .title(name) - .icon(BitmapDescriptorFactory.fromBitmap( - getCircleBitmap(Bitmap.createScaledBitmap(src, 150, 150, true)))); + .title(name); } - private static LatLng mapToLatTng(Location location) { + public static LatLng mapToLatTng(Location location) { return new LatLng(location.getLatitude(), location.getLongitude()); } @@ -444,4 +462,22 @@ public int hashCode() { return Objects.hash(id, latitude, longitude); } } + + @Override + public void onStop() { + super.onStop(); + logMethod(TAG, this); + } + + @Override + public void onPause() { + super.onPause(); + logMethod(TAG, this); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + logMethod(TAG, this); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/UserEventsFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java similarity index 54% rename from AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/UserEventsFragment.java rename to AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java index 4718c19..3f3ee8e 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/UserEventsFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java @@ -1,9 +1,11 @@ package com.tom.meeter.context.profile.fragment; -import static com.tom.meeter.context.auth.infrastructure.AuthHelper.peekToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.accounts.AccountManager; +import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -16,29 +18,34 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.tom.meeter.App; -import com.tom.meeter.context.profile.adapter.RecycleViewUserEventsAdapter; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.adapter.EventsAdapter; import com.tom.meeter.context.profile.viewmodel.ProfileEventsViewModel; import com.tom.meeter.databinding.SubFragmentUserEventsBinding; +import com.tom.meeter.infrastructure.common.InfrastructureHelper; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheEventEventBinder; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; import javax.inject.Inject; -public class UserEventsFragment extends Fragment { +public class ProfileEventsFragment extends Fragment { - private static final String TAG = UserEventsFragment.class.getCanonicalName(); + private static final String TAG = ProfileEventsFragment.class.getCanonicalName(); SubFragmentUserEventsBinding binding; - private RecycleViewUserEventsAdapter adapter; + private EventsAdapter adapter; @Inject ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imageDownloader; private ProfileEventsViewModel profileEventsViewModel; private AccountManager accountManager; - public UserEventsFragment() { + public ProfileEventsFragment() { logMethod(TAG, this); } @@ -46,8 +53,22 @@ public UserEventsFragment() { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); logMethod(TAG, this); + ((App) getActivity().getApplication()).getComponent().inject(this); - accountManager = AccountManager.get(this.getContext()); + + Context ctx = getContext(); + accountManager = AccountManager.get(ctx); + + adapter = new EventsAdapter( + new PhotoDownloaderWithCacheEventEventBinder( + ctx, imageDownloader, + (e) -> dispatchToEventActivity(ctx, e.getId()), + () -> InfrastructureHelper.restartActivityFromFragment(this))); + /* + btnDelete.setOnClickListener(v -> { + if (onDeleteButtonClickListener != null) + onDeleteButtonClickListener.onDeleteButtonClicked(post); + });*/ } @Override @@ -64,21 +85,34 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat logMethod(TAG, this); profileEventsViewModel = ViewModelProviders.of(this, viewModelFactory) .get(ProfileEventsViewModel.class); - - profileEventsViewModel.getProfileEvents(peekToken(accountManager), this); - - adapter = new RecycleViewUserEventsAdapter(getContext()); + profileEventsViewModel.fetchProfileEvents(getAuthHeader(accountManager), this); profileEventsViewModel.getProfileEventsLiveData() - .observe(getViewLifecycleOwner(), ev -> adapter.setData(ev)); - + .observe(getViewLifecycleOwner(), events -> adapter.setData(events)); binding.userEventsFragmentRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); binding.userEventsFragmentRecyclerView.setAdapter(adapter); - binding.userEventsFragmentRecyclerView.invalidate(); + } - /* - adapter = new RecycleViewUserEventsAdapter(events); - rView.swapAdapter(adapter, false); - */ + @Override + public void onPause() { + super.onPause(); + logMethod(TAG, this); + } + + @Override + public void onStop() { + super.onStop(); + logMethod(TAG, this); + } + @Override + public void onDestroyView() { + super.onDestroyView(); + logMethod(TAG, this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + logMethod(TAG, this); } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileFragment.java index 6de7f25..42016ee 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileFragment.java @@ -1,12 +1,16 @@ package com.tom.meeter.context.profile.fragment; -import static com.tom.meeter.context.auth.infrastructure.AuthHelper.peekToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.CommonHelper.genderResolver; +import static com.tom.meeter.infrastructure.common.CommonHelper.getLocalDateOrNull; +import static com.tom.meeter.infrastructure.common.CommonHelper.getStringOrNull; import static com.tom.meeter.infrastructure.common.DateHelper.getAgeFromDate; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; import android.accounts.AccountManager; -import android.content.Intent; +import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -16,32 +20,54 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.GridLayoutManager; import com.tom.meeter.App; import com.tom.meeter.R; -import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.message.UpdateProfileRequest; +import com.tom.meeter.context.profile.service.ProfileService; +import com.tom.meeter.context.profile.user.domain.User; import com.tom.meeter.context.profile.viewmodel.ProfileViewModel; -import com.tom.meeter.context.user.GridViewAdapter; import com.tom.meeter.databinding.FragmentProfileBinding; +import com.tom.meeter.infrastructure.common.InfrastructureHelper; +import com.tom.meeter.infrastructure.components.adapter.EventsCardAdapter; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderEventBinder; +import com.tom.meeter.infrastructure.http.ActivityRestarterOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; +import java.time.LocalDate; +import java.util.Objects; + import javax.inject.Inject; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + /** * Created by Tom on 14.12.2016. */ public class ProfileFragment extends Fragment { private static final String TAG = ProfileFragment.class.getCanonicalName(); + private boolean isEditableModeEnabled = false; private FragmentProfileBinding binding; @Inject ViewModelFactory viewModelFactory; - + @Inject + ImageDownloader imageDownloader; + @Inject + ProfileService profileService; private ProfileViewModel profileViewModel; - private AccountManager accountManager; + private EventsCardAdapter adapter; + + private User userCache; + private ResponseBody photoCache; public ProfileFragment() { logMethod(TAG, this); @@ -50,9 +76,17 @@ public ProfileFragment() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((App) getActivity().getApplication()).getComponent().inject(this); - accountManager = AccountManager.get(this.getContext()); logMethod(TAG, this); + + ((App) getActivity().getApplication()).getComponent().inject(this); + + Context ctx = getContext(); + accountManager = AccountManager.get(ctx); + + adapter = new EventsCardAdapter( + new PhotoDownloaderEventBinder(ctx, imageDownloader, + event -> dispatchToEventActivity(ctx, event.getId()), + () -> InfrastructureHelper.restartActivityFromFragment(this))); } @Override @@ -67,31 +101,96 @@ public View onCreateView( public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); logMethod(TAG, this); + profileViewModel = ViewModelProviders.of(this, viewModelFactory) .get(ProfileViewModel.class); - profileViewModel.getProfile(peekToken(accountManager), this); - profileViewModel.getProfileLiveData().observe( - getViewLifecycleOwner(), - user -> { - if (user != null) { - binding.profileId.setText(getString(R.string.profile_user_id_format, user.getId())); - binding.profileName.setText(getString(R.string.profile_user_name_format, user.getName(), user.getSurname())); - binding.profileGender.setText(getString(R.string.profile_gender_format, genderResolver(getContext(), user.getGender()))); - binding.profileAge.setText(getString(R.string.profile_age_format, getAgeFromDate(user.getBirthday()))); - binding.profileInfo.setText(getString(R.string.profile_info_format, user.getInfo())); - } - }); - profileViewModel.getProfileEventsLiveData().observe( - getViewLifecycleOwner(), - events -> { - binding.profileEventsGrid.setAdapter(new GridViewAdapter(getContext(), events)); - binding.profileEventsGrid.setExpanded(true); - binding.profileEventsGrid.setOnItemClickListener( - (parent, view1, position, id) -> - startActivity(new Intent(getActivity(), EventActivity.class) - .putExtra(EventActivity.EVENT_ID_KEY, events.get(position).getId()))); - } - ); + String authHeader = getAuthHeader(accountManager); + + profileViewModel.fetchProfile(authHeader, this); + + profileViewModel.getProfileLiveData() + .observe( + getViewLifecycleOwner(), + user -> { + userCache = user; + updateLayoutValues(); + }); + + profileViewModel.getProfileEventsLiveData() + .observe(getViewLifecycleOwner(), events -> adapter.setData(events)); + + binding.events.setLayoutManager(new GridLayoutManager(getContext(), 2)); + binding.events.setAdapter(adapter); + + binding.btnEdit.setOnClickListener(v -> { + if (isEditableModeEnabled) { + UpdateProfileRequest req = createUpdateProfileRequest(); + if (req.isEmpty()) { + showMessage(this.getActivity(), "Empty update request is not sent."); + updateLayoutValues(); + switchEditMode(); + return; + } + profileService.updateProfile(authHeader, req) + .enqueue(new ActivityRestarterOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response response) { + super.onResponse(call, response); + if (response.code() == HttpCodes.OK && response.body() != null) { + userCache = response.body(); + showMessage(ProfileFragment.this.getActivity(), "Saved"); + } + updateLayoutValues(); + } + }); + } + switchEditMode(); + }); + } + + private void updateLayoutValues() { + /*binding.profileId.setText(userCache.getId());*/ + binding.name.setText(userCache.getName()); + binding.surname.setText(userCache.getSurname()); + binding.gender.setText(genderResolver(getContext(), userCache.getGender())); + binding.birthday.setText(userCache.getBirthday()); + binding.age.setText(getString(R.string.profile_age_format, getAgeFromDate(userCache.getBirthday()))); + binding.info.setText(userCache.getInfo()); + } + + private void switchEditMode() { + isEditableModeEnabled = !isEditableModeEnabled; + binding.name.setEnabled(isEditableModeEnabled); + binding.surname.setEnabled(isEditableModeEnabled); + binding.birthday.setEnabled(isEditableModeEnabled); + binding.info.setEnabled(isEditableModeEnabled); + binding.btnEdit.setText(isEditableModeEnabled ? "Save" : "Edit"); + } + + private UpdateProfileRequest createUpdateProfileRequest() { + UpdateProfileRequest req = new UpdateProfileRequest(); + + String nameChange = getStringOrNull(binding.name.getText()); + if (!Objects.equals(userCache.getName(), nameChange)) { + req.setName(nameChange); + } + String surnameChange = getStringOrNull(binding.surname.getText()); + if (!Objects.equals(userCache.getSurname(), surnameChange)) { + req.setSurname(surnameChange); + } + LocalDate birthdayChange = getLocalDateOrNull(binding.birthday.getText()); + //todo userCache.getBirthday() [String -> LocalDate] + if (!Objects.equals( + userCache.getBirthday(), + birthdayChange == null ? null : birthdayChange.toString())) { + req.setBirthday(birthdayChange); + } + String infoChange = getStringOrNull(binding.info.getText()); + if (!Objects.equals(userCache.getInfo(), infoChange)) { + req.setInfo(infoChange); + } + //TODO: userCache.getPhotoPath(); + return req; } @Override diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/message/UpdateProfileRequest.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/message/UpdateProfileRequest.java new file mode 100644 index 0000000..1d003bd --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/message/UpdateProfileRequest.java @@ -0,0 +1,81 @@ +package com.tom.meeter.context.profile.message; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.LocalDate; +import java.util.Optional; + +public class UpdateProfileRequest { + + private Optional name; + private Optional surname; + private Optional info; + @JsonProperty(value = "old_password") + private Optional oldPassword; + @JsonProperty(value = "new_password") + private Optional newPassword; + private Optional birthday; + @JsonProperty(value = "photo_path") + private Optional photoPath; + + public boolean isEmpty() { + return (name == null && surname == null && info == null && oldPassword == null + && newPassword == null && birthday == null && photoPath == null); + } + + public Optional getName() { + return name; + } + + public void setName(String name) { + this.name = Optional.ofNullable(name); + } + + public Optional getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = Optional.ofNullable(surname); + } + + public Optional getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = Optional.ofNullable(info); + } + + public Optional getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = Optional.ofNullable(oldPassword); + } + + public Optional getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = Optional.ofNullable(newPassword); + } + + public Optional getBirthday() { + return birthday; + } + + public void setBirthday(LocalDate birthday) { + this.birthday = Optional.ofNullable(birthday); + } + + public Optional getPhotoPath() { + return photoPath; + } + + public void setPhotoPath(String photoPath) { + this.photoPath = Optional.ofNullable(photoPath); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/service/ProfileService.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/service/ProfileService.java index b0f1e1a..398c56e 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/service/ProfileService.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/service/ProfileService.java @@ -3,18 +3,25 @@ import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.profile.message.UpdateProfileRequest; import com.tom.meeter.context.profile.user.domain.User; import java.util.List; import retrofit2.Call; +import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.PATCH; public interface ProfileService { @GET("/profile") Call getProfile(@Header(AUTH_HEADER) String authHeader); + @PATCH("/profile/update") + Call updateProfile( + @Header(AUTH_HEADER) String authHeader, @Body UpdateProfileRequest req); + @GET("/profile/events") Call> getProfileEvents(@Header(AUTH_HEADER) String authHeader); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsCreateOrUpdate.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsCreateOrUpdate.java index 1cc6219..69977cc 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsCreateOrUpdate.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsCreateOrUpdate.java @@ -1,12 +1,12 @@ package com.tom.meeter.context.profile.settings.message; -import com.google.gson.annotations.SerializedName; +import com.fasterxml.jackson.annotation.JsonProperty; public class SettingsCreateOrUpdate { - @SerializedName(value = "search_area") + @JsonProperty(value = "search_area") private Integer searchArea; - @SerializedName(value = "need_track_user") + @JsonProperty(value = "need_track_user") private Boolean needTrackUser; public SettingsCreateOrUpdate(Integer searchArea, Boolean needTrackUser) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsResponse.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsResponse.java index 9183f0b..238fe94 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsResponse.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/settings/message/SettingsResponse.java @@ -1,16 +1,20 @@ package com.tom.meeter.context.profile.settings.message; -import com.google.gson.annotations.SerializedName; +import com.fasterxml.jackson.annotation.JsonProperty; public class SettingsResponse { - private final String id; - @SerializedName(value = "user_id") - private final String userId; - @SerializedName(value = "search_area") - private final Integer searchArea; - @SerializedName(value = "need_track_user") - private final Boolean needTrackUser; + private String id; + @JsonProperty(value = "user_id") + private String userId; + @JsonProperty(value = "search_area") + private Integer searchArea; + @JsonProperty(value = "need_track_user") + private Boolean needTrackUser; + + public SettingsResponse() { + //Jackson requires empty c-tor + } public SettingsResponse(String id, String userId, Integer searchArea, Boolean needTrackUser) { this.id = id; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/domain/User.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/domain/User.java index 48f49fa..d92a6b3 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/domain/User.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/domain/User.java @@ -4,7 +4,11 @@ import androidx.room.Entity; import androidx.room.PrimaryKey; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + @Entity +@JsonIgnoreProperties(ignoreUnknown = true) +//login? public class User { @PrimaryKey @@ -16,6 +20,10 @@ public class User { private String info; private String birthday; + public User() { + //Jackson. + } + public User( @NonNull String id, String name, String gender, String surname, String info, String birthday) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/repository/UserRepository.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/repository/UserRepository.java index 4907453..36b8560 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/repository/UserRepository.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/user/repository/UserRepository.java @@ -4,20 +4,18 @@ import androidx.lifecycle.LiveData; -import com.tom.meeter.AppScope; import com.tom.meeter.context.profile.user.database.UserDao; import com.tom.meeter.context.profile.user.domain.User; import com.tom.meeter.context.user.service.UserService; import java.util.concurrent.Executor; -import javax.inject.Inject; - import io.reactivex.Completable; import io.reactivex.Maybe; import retrofit2.Response; -@AppScope +//@AppScope +@Deprecated public class UserRepository { private static final String TAG = UserRepository.class.getCanonicalName(); @@ -27,7 +25,7 @@ public class UserRepository { private final UserDao userDao; private final Executor executor; - @Inject + //@Inject public UserRepository(UserService userService, UserDao userDao, Executor executor) { this.userService = userService; this.userDao = userDao; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileEventsViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileEventsViewModel.java index fe1bf13..6469205 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileEventsViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileEventsViewModel.java @@ -11,7 +11,6 @@ import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.profile.service.ProfileService; -import com.tom.meeter.infrastructure.common.Globals; import com.tom.meeter.infrastructure.http.ActivityRestarterOnAuthFailure; import com.tom.meeter.infrastructure.http.HttpCodes; @@ -36,8 +35,8 @@ public ProfileEventsViewModel(ProfileService profileService) { this.profileService = profileService; } - public void getProfileEvents(String token, Fragment fragment) { - profileService.getProfileEvents(Globals.getAuthHeader(token)).enqueue( + public void fetchProfileEvents(String auth, Fragment fragment) { + profileService.getProfileEvents(auth).enqueue( new ActivityRestarterOnAuthFailure<>(fragment) { @Override public void onResponse(Call> call, Response> response) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileViewModel.java index ebaf70e..3be0d01 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileViewModel.java @@ -12,7 +12,6 @@ import com.tom.meeter.context.network.dto.EventDTO; import com.tom.meeter.context.profile.service.ProfileService; import com.tom.meeter.context.profile.user.domain.User; -import com.tom.meeter.infrastructure.common.Globals; import com.tom.meeter.infrastructure.http.ActivityRestarterOnAuthFailure; import com.tom.meeter.infrastructure.http.HttpCodes; @@ -38,8 +37,8 @@ public ProfileViewModel(ProfileService profileService) { this.profileService = profileService; } - public void getProfile(String token, Fragment fragment) { - profileService.getProfile(Globals.getAuthHeader(token)).enqueue( + public void fetchProfile(String auth, Fragment fragment) { + profileService.getProfile(auth).enqueue( new ActivityRestarterOnAuthFailure<>(fragment) { @Override public void onResponse(Call call, Response response) { @@ -52,7 +51,7 @@ public void onResponse(Call call, Response response) { } } ); - profileService.getProfileEvents(Globals.getAuthHeader(token)).enqueue( + profileService.getProfileEvents(auth).enqueue( new ActivityRestarterOnAuthFailure<>(fragment) { @Override public void onResponse(Call> call, Response> response) { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserEventsViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserEventsViewModel.java index 3eee107..158ee14 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserEventsViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserEventsViewModel.java @@ -8,8 +8,6 @@ import java.util.List; -import javax.inject.Inject; - @Deprecated public class UserEventsViewModel extends ViewModel { @@ -18,7 +16,7 @@ public class UserEventsViewModel extends ViewModel { private final EventRepository eventRepository; - @Inject + //@Inject public UserEventsViewModel(EventRepository eventRepository) { this.eventRepository = eventRepository; } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserProfileViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserProfileViewModel.java index d75e6d7..36f2082 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserProfileViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/UserProfileViewModel.java @@ -8,8 +8,6 @@ import com.tom.meeter.context.profile.user.domain.User; import com.tom.meeter.context.profile.user.repository.UserRepository; -import javax.inject.Inject; - @Deprecated public class UserProfileViewModel extends ViewModel { @@ -20,7 +18,7 @@ public class UserProfileViewModel extends ViewModel { private final UserRepository userRepository; - @Inject + //@Inject public UserProfileViewModel(UserRepository userRepository) { logMethod(TAG, this); this.userRepository = userRepository; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/token/TokenModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/token/TokenModule.java index e3372ba..b76c7b4 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/token/TokenModule.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/token/TokenModule.java @@ -14,7 +14,8 @@ import dagger.Module; import dagger.Provides; import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; +//import retrofit2.converter.gson.GsonConverterFactory; @Module public class TokenModule { @@ -31,7 +32,8 @@ public TokenModule() { public TokenService providesTokenService(Application app) { return new Retrofit.Builder() .baseUrl(getServerPath(app)) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + //.addConverterFactory(GsonConverterFactory.create()) .build() .create(TokenService.class); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/token/service/TokenService.java b/AndroidClient/src/main/java/com/tom/meeter/context/token/service/TokenService.java index 1d4ac03..e172ccf 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/token/service/TokenService.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/token/service/TokenService.java @@ -7,10 +7,10 @@ import retrofit2.http.Header; /** - * Purpose is to check the token and fail auth in case of failed authorization. + * Purpose is to check the token and fail-fast the request ASAP, starting auth process + * for requesting new token or even new credentials in case of failed authentication. */ public interface TokenService { - //TODO change GET path to '/check-token' when backend will be done - @GET("/profile") + @GET("/api/token_check") Call checkToken(@Header(AUTH_HEADER) String authHeader); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/GridViewAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/GridViewAdapter.java deleted file mode 100644 index 9e87a37..0000000 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/GridViewAdapter.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.tom.meeter.context.user; - -import static com.tom.meeter.infrastructure.Image.ImagesHelper.getCircleBitmap; -import static com.tom.meeter.infrastructure.Image.ImagesHelper.randomPicResource; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.tom.meeter.R; -import com.tom.meeter.context.network.dto.EventDTO; - -import java.util.List; - -public class GridViewAdapter extends ArrayAdapter { - - private final Context ctx; - - public GridViewAdapter(Context context, List list) { - super(context, 0, list); - ctx = context; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - View itemView = convertView; - if (itemView == null) { - itemView = LayoutInflater.from(getContext()) - .inflate(R.layout.card_item, parent, false); - } - - EventDTO event = getItem(position); - - TextView textView = itemView.findViewById(R.id.text_view); - ImageView imageView = itemView.findViewById(R.id.image_view); - - if (event != null) { - textView.setText(event.getName()); - - Bitmap src = BitmapFactory.decodeResource(ctx.getResources(), randomPicResource()); - Bitmap scaled = Bitmap.createScaledBitmap(src, 150, 150, true); - Bitmap circled = getCircleBitmap(scaled); - - imageView.setImageBitmap(circled); - } - - return itemView; - } -} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/UserComponent.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserComponent.java new file mode 100644 index 0000000..3b8c309 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserComponent.java @@ -0,0 +1,38 @@ +package com.tom.meeter.context.user; + +import android.app.Application; + +import com.tom.meeter.context.image.ImageComponent; +import com.tom.meeter.context.token.TokenComponent; +import com.tom.meeter.context.user.activity.UserActivity; +import com.tom.meeter.context.user.viewmodel.UserViewModelModule; + +import dagger.BindsInstance; +import dagger.Component; + +@UserScope +@Component( + modules = { + UserModule.class, + UserViewModelModule.class + }, + dependencies = { + TokenComponent.class, + ImageComponent.class + }) +public interface UserComponent { + + @Component.Builder + interface Builder { + @BindsInstance + Builder application(Application application); + + Builder tokenComponent(TokenComponent tokenComponent); + + Builder imageComponent(ImageComponent tokenComponent); + + UserComponent build(); + } + + void inject(UserActivity userActivity); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/UserModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserModule.java new file mode 100644 index 0000000..ebfa342 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserModule.java @@ -0,0 +1,46 @@ +package com.tom.meeter.context.user; + +import static com.tom.meeter.infrastructure.common.Globals.getServerPath; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Application; + +import androidx.annotation.NonNull; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.tom.meeter.context.user.service.UserService; + +import java.util.TimeZone; + +import dagger.Module; +import dagger.Provides; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; + +@Module +public class UserModule { + + private static final String TAG = UserModule.class.getCanonicalName(); + + public UserModule() { + logMethod(TAG, this); + } + + @UserScope + @NonNull + @Provides + public UserService provideUserService(Application app) { + return new Retrofit.Builder() + .baseUrl(getServerPath(app)) + .addConverterFactory(JacksonConverterFactory.create( + JsonMapper.builder() + .addModule(new JavaTimeModule()) + .build() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .setTimeZone(TimeZone.getDefault()))) + .build() + .create(UserService.class); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/UserScope.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserScope.java new file mode 100644 index 0000000..e747bcc --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserScope.java @@ -0,0 +1,11 @@ +package com.tom.meeter.context.user; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface UserScope { +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserActivity.java index b790775..45cee42 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserActivity.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserActivity.java @@ -1,9 +1,11 @@ package com.tom.meeter.context.user.activity; import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.CommonHelper.genderResolver; import static com.tom.meeter.infrastructure.common.DateHelper.getAgeFromDate; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; import android.accounts.AccountManager; import android.content.Context; @@ -17,29 +19,46 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.GridLayoutManager; import com.tom.meeter.App; import com.tom.meeter.R; -import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.image.ImageDownloader; import com.tom.meeter.context.token.service.TokenService; -import com.tom.meeter.context.user.GridViewAdapter; +import com.tom.meeter.context.user.service.UserService; import com.tom.meeter.context.user.viewmodel.UserViewModel; -import com.tom.meeter.databinding.UserLayoutBinding; +import com.tom.meeter.databinding.ActivityUserBinding; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.components.adapter.EventsCardAdapter; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderEventBinder; +import com.tom.meeter.infrastructure.http.HttpCodes; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; import javax.inject.Inject; +import retrofit2.Call; +import retrofit2.Response; + public class UserActivity extends AppCompatActivity { + private static final String TAG = UserActivity.class.getCanonicalName(); - public static final String USER_ID_KEY = "user_id"; - UserLayoutBinding binding; + private static final String USER_ID_KEY = "user_id"; + @Inject TokenService tokenService; @Inject + UserService userService; + @Inject ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + private ActivityUserBinding binding; private UserViewModel userViewModel; private String userId; private AccountManager accountManager; + private EventsCardAdapter adapter; + private Boolean amISubscriber; @Override protected void onCreate(Bundle savedInstanceState) { @@ -60,45 +79,99 @@ protected void onCreate(Bundle savedInstanceState) { return; } - ((App) getApplication()).getComponent().inject(this); + ((App) getApplication()).getUserComponent().inject(this); accountManager = AccountManager.get(this); + adapter = new EventsCardAdapter( + new PhotoDownloaderEventBinder( + this, imgDownloader, + event -> dispatchToEventActivity(this, event.getId()), this::recreate)); + //setToken(accountManager, Launcher.EXPIRED); checkToken(this::onInit, this::finish, accountManager, this, tokenService); } private void onInit(String token) { - binding = UserLayoutBinding.inflate(getLayoutInflater()); + binding = ActivityUserBinding.inflate(getLayoutInflater()); View view = binding.getRoot(); setContentView(view); + binding.subscribeBtn.setOnClickListener(v -> { + if (amISubscriber == null) { + // As not initialized atm... + return; + } + if (amISubscriber) { + userService.unsubscribe(Globals.getAuthHeader(token), userId).enqueue( + new HttpErrorLogger<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + amISubscriber = false; + updateSubscribeButtonText(); + showMessage(UserActivity.this, "Successfully unsubscribed."); + return; + } + if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { + UserActivity.this.recreate(); + } + } + }); + } else { + userService.subscribe(Globals.getAuthHeader(token), userId).enqueue( + new HttpErrorLogger<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + amISubscriber = true; + updateSubscribeButtonText(); + showMessage(UserActivity.this, "Successfully subscribed."); + return; + } + if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { + UserActivity.this.recreate(); + } + } + }); + } + }); + userViewModel = ViewModelProviders.of(this, viewModelFactory) .get(UserViewModel.class); userViewModel.fetchUserInformation(token, userId, this); userViewModel.getUserLiveData() .observe(this, user -> { - if (user != null) { - binding.userFormat.setText(getString( - R.string.user_format, user.getName(), user.getSurname(), - getAgeFromDate(user.getBirthday()), - genderResolver(this, user.getGender()))); - binding.userInfo.setText(user.getInfo()); - } + binding.name.setText(user.getName()); + binding.surname.setText(user.getSurname()); + binding.gender.setText(genderResolver(getApplicationContext(), user.getGender())); + binding.birthday.setText(user.getBirthday()); + binding.age.setText(getString(R.string.profile_age_format, getAgeFromDate(user.getBirthday()))); + binding.info.setText(user.getInfo()); + //binding.userPhoto.setImageBitmap(); }); - userViewModel.getUserEventsLiveData() - .observe(this, events -> { - if (events != null && !events.isEmpty()) { - //GridAdapter adapter = new GridAdapter(this); - //binding.userEventsGrid.setAdapter(adapter); - - GridViewAdapter adapter = new GridViewAdapter(this, events); - binding.userEventsGrid.setAdapter(adapter); - binding.userEventsGrid.setExpanded(true); - binding.userEventsGrid.setOnItemClickListener( - (parent, view1, position, id) -> - startActivity(new Intent(UserActivity.this, EventActivity.class).putExtra(EventActivity.EVENT_ID_KEY, events.get(position).getId()))); - } + userViewModel.getAmISubscriber() + .observe(this, val -> { + amISubscriber = val; + updateSubscribeButtonText(); }); + userViewModel.getUserEventsLiveData() + .observe(this, events -> adapter.setData(events)); + + binding.events.setLayoutManager(new GridLayoutManager(this, 2)); + binding.events.setAdapter(adapter); + } + + private void updateSubscribeButtonText() { + if (amISubscriber == null) { + binding.subscribeBtn.setText("..."); + } + if (amISubscriber) { + binding.subscribeBtn.setText("Unsubscribe"); + } else { + binding.subscribeBtn.setText("Subscribe"); + } } @Nullable @@ -108,4 +181,13 @@ public View onCreateView( @NonNull AttributeSet attrs) { return super.onCreateView(parent, name, ctx, attrs); } + + public static void dispatchToUserActivity(Context ctx, String userId) { + ctx.startActivity(createUserActivityIntent(ctx, userId)); + } + + private static Intent createUserActivityIntent(Context ctx, String userId) { + return new Intent(ctx, UserActivity.class) + .putExtra(USER_ID_KEY, userId); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/GridAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridAdapter.java similarity index 95% rename from AndroidClient/src/main/java/com/tom/meeter/context/user/GridAdapter.java rename to AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridAdapter.java index 8231a2e..105a2fe 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/GridAdapter.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridAdapter.java @@ -1,4 +1,4 @@ -package com.tom.meeter.context.user; +package com.tom.meeter.context.user.adapter; import android.content.Context; import android.view.View; @@ -7,6 +7,7 @@ import android.widget.GridView; import android.widget.ImageView; +@Deprecated public class GridAdapter extends BaseAdapter { private Context mContext; private String[] mThumbIds = { diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridViewAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridViewAdapter.java new file mode 100644 index 0000000..3ef5748 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/GridViewAdapter.java @@ -0,0 +1,73 @@ +package com.tom.meeter.context.user.adapter; + +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.databinding.CardItemBinding; + +import java.util.List; + +public class GridViewAdapter extends ArrayAdapter { + + private static final String TAG = GridViewAdapter.class.getCanonicalName(); + + private final Context ctx; + private final ImageDownloader imageDownloader; + private final Runnable onAuthFail; + + public GridViewAdapter( + Context ctx, List events, + ImageDownloader imgDownloader, Runnable onAuthFail) { + super(ctx, 0, events); + this.ctx = ctx; + this.imageDownloader = imgDownloader; + this.onAuthFail = onAuthFail; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + CardItemBinding iBinding = CardItemBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + holder = new ViewHolder(iBinding); + holder.view = iBinding.getRoot(); + holder.view.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + EventDTO event = getItem(position); + if (event != null) { + holder.binding.textView.setText(event.getName()); + String photoPath = event.getPhotoPath(); + if (photoPath != null) { + imageDownloader.downloadEventImage( + photoPath, ctx, + (photo) -> holder.binding.imageView.setImageBitmap(circleImage(photo)), + onAuthFail); + } + } else { + Log.w(TAG, "null event at [" + position + "]."); + } + return holder.view; + } + + private static class ViewHolder { + private View view; + private final CardItemBinding binding; + + ViewHolder(CardItemBinding binding) { + this.view = binding.getRoot(); + this.binding = binding; + } + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/service/UserService.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/service/UserService.java index 9d68982..54270d5 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/service/UserService.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/service/UserService.java @@ -23,4 +23,13 @@ public interface UserService { @GET("/user/{id}/events") Call> getUserEvents(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + + @GET("/user/{id}/am_i_subscribed") + Call amISubscribed(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + + @GET("/user/{id}/subscribe") + Call subscribe(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + + @GET("/user/{id}/unsubscribe") + Call unsubscribe(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModel.java index bf4279c..8af6f68 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModel.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModel.java @@ -13,7 +13,7 @@ import com.tom.meeter.context.profile.user.domain.User; import com.tom.meeter.context.user.service.UserService; import com.tom.meeter.infrastructure.common.Globals; -import com.tom.meeter.infrastructure.http.DisconnectLogger; +import com.tom.meeter.infrastructure.http.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; import java.util.List; @@ -28,6 +28,7 @@ public class UserViewModel extends ViewModel { private static final String TAG = UserViewModel.class.getCanonicalName(); private final MutableLiveData userLiveData = new MutableLiveData<>(); + private final MutableLiveData amISubscriber = new MutableLiveData<>(); private final MutableLiveData> userEventsLiveData = new MutableLiveData<>(); private final UserService userService; @@ -40,7 +41,7 @@ public UserViewModel(UserService userService) { public void fetchUserInformation(String token, String userId, Activity activity) { userService.getUser(Globals.getAuthHeader(token), userId).enqueue( - new DisconnectLogger<>(activity) { + new ErrorLogger<>(activity) { @Override public void onResponse(Call call, Response response) { if (response.code() == HttpCodes.OK && response.body() != null) { @@ -55,8 +56,24 @@ public void onResponse(Call call, Response response) { } ); + userService.amISubscribed(Globals.getAuthHeader(token), userId).enqueue( + new ErrorLogger<>(activity) { + @Override + public void onResponse(Call call, Response response) { + if (response.code() == HttpCodes.OK && response.body() != null) { + amISubscriber.setValue(response.body()); + return; + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + activity.recreate(); + } + Log.i(TAG, "/user/{id}/am_i_subscribed: " + response.code() + " : " + response.body()); + } + } + ); + userService.getUserEvents(Globals.getAuthHeader(token), userId).enqueue( - new DisconnectLogger<>(activity) { + new ErrorLogger<>(activity) { @Override public void onResponse(Call> call, Response> response) { if (response.code() == HttpCodes.OK && response.body() != null) { @@ -68,8 +85,7 @@ public void onResponse(Call> call, Response> respo } Log.i(TAG, "/user/{id}/events: " + response.code() + " : " + response.body()); } - } - ); + }); } @Override @@ -85,5 +101,8 @@ public LiveData getUserLiveData() { public LiveData> getUserEventsLiveData() { return userEventsLiveData; } -} + public MutableLiveData getAmISubscriber() { + return amISubscriber; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModelModule.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModelModule.java new file mode 100644 index 0000000..92249c4 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserViewModelModule.java @@ -0,0 +1,17 @@ +package com.tom.meeter.context.user.viewmodel; + +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelKey; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class UserViewModelModule { + @Binds + @IntoMap + @ViewModelKey(UserViewModel.class) + abstract ViewModel userViewModel(UserViewModel userViewModel); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/CommonHelper.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/CommonHelper.java index 103582c..488418d 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/CommonHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/CommonHelper.java @@ -2,12 +2,30 @@ import android.content.Context; +import androidx.annotation.Nullable; + import com.tom.meeter.R; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + public final class CommonHelper { + private CommonHelper() { } + public static final DateTimeFormatter UI_DATE_TIME_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public static final DateTimeFormatter UI_DATE_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public static final String EMPTY_STR = ""; + public static String genderResolver(Context ctx, String gender) { return switch (gender.toLowerCase()) { case "female" -> ctx.getString(R.string.female_gender); @@ -15,4 +33,44 @@ public static String genderResolver(Context ctx, String gender) { default -> throw new IllegalArgumentException("#args " + gender); }; } + + @Nullable + public static CharSequence dateOrNull(OffsetDateTime date) { + return date == null ? null : UI_DATE_TIME_FORMAT.format(date); + } + + @Nullable + public static CharSequence textOrNull(Double val) { + return val == null ? null : val.toString(); + } + + public static String getStringOrNull(CharSequence input) { + if (input == null || EMPTY_STR.contentEquals(input)) { + return null; + } + return input.toString(); + } + + public static Float getFloatOrNull(CharSequence input) { + if (input == null || EMPTY_STR.contentEquals(input)) { + return null; + } + return Float.valueOf(input.toString()); + } + + public static OffsetDateTime getOffsetDateTime(CharSequence input) { + if (input == null || EMPTY_STR.contentEquals(input)) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.parse(input, UI_DATE_TIME_FORMAT); + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault()); + return zonedDateTime.toOffsetDateTime(); + } + + public static LocalDate getLocalDateOrNull(CharSequence input) { + if (input == null || EMPTY_STR.contentEquals(input)) { + return null; + } + return LocalDate.parse(input, UI_DATE_FORMAT); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/Globals.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/Globals.java index 158b117..b9fadf7 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/Globals.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/Globals.java @@ -32,7 +32,6 @@ private Globals() { public static final String AUTH_HEADER = "Authorization"; public static final String BEARER_FORMAT = "Bearer %s"; - public static final String TOKEN_KEY = "token"; private static String serverPath; private static String socketIOPath; diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/Image/ImagesHelper.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/ImagesHelper.java similarity index 81% rename from AndroidClient/src/main/java/com/tom/meeter/infrastructure/Image/ImagesHelper.java rename to AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/ImagesHelper.java index 0328a4b..222dab4 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/Image/ImagesHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/ImagesHelper.java @@ -1,4 +1,4 @@ -package com.tom.meeter.infrastructure.Image; +package com.tom.meeter.infrastructure.common; import android.content.Context; import android.graphics.Bitmap; @@ -25,18 +25,41 @@ import java.util.Random; +import okhttp3.ResponseBody; + public class ImagesHelper { private static final FontAwesome FONT_AWESOME = new FontAwesome(); - public static Bitmap getCircleBitmap(Bitmap bitmap) { - final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), - bitmap.getHeight(), Bitmap.Config.ARGB_8888); + public static Bitmap circleImage(ResponseBody body) { + return circleImage(body, 150, 150); + } + + public static Bitmap circleImage(ResponseBody body, int scaleWidth, int scaleHeight) { + Bitmap from = from(body); + if (from == null) { + return null; + } + return getCircleBitmap(Bitmap.createScaledBitmap(from, scaleWidth, scaleHeight, true)); + } + + public static Bitmap circleImage(Bitmap src) { + return getCircleBitmap(Bitmap.createScaledBitmap(src, 150, 150, true)); + } + + public static Bitmap from(ResponseBody body) { + return BitmapFactory.decodeStream(body.byteStream()); + } + + + public static Bitmap getCircleBitmap(Bitmap src) { + final Bitmap output = Bitmap.createBitmap(src.getWidth(), + src.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(output); final int color = 0xff424242; final Paint paint = new Paint(); - final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + final Rect rect = new Rect(0, 0, src.getWidth(), src.getHeight()); final RectF rectF = new RectF(rect); paint.setAntiAlias(true); @@ -45,9 +68,9 @@ public static Bitmap getCircleBitmap(Bitmap bitmap) { canvas.drawOval(rectF, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(bitmap, rect, rect, paint); + canvas.drawBitmap(src, rect, rect, paint); - bitmap.recycle(); + src.recycle(); return output; } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/InfrastructureHelper.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/InfrastructureHelper.java index 0f9b3b0..a6fbe81 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/InfrastructureHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/InfrastructureHelper.java @@ -1,6 +1,7 @@ package com.tom.meeter.infrastructure.common; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Looper; @@ -26,6 +27,12 @@ public static void showMessage(Activity activity, String msg) { () -> Toast.makeText(activity.getApplicationContext(), msg, Toast.LENGTH_SHORT).show()); } + public static void showMessage(Context ctx, String msg) { + if (TextUtils.isEmpty(msg)) + return; + Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show(); + } + public static void logMethod(String tag, Object obj) { if (obj.getClass().isAnonymousClass()) { Log.d(tag, obj.getClass().getName() + " " + getCurrentMethodName()); diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseEventAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseEventAdapter.java new file mode 100644 index 0000000..65e81c0 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseEventAdapter.java @@ -0,0 +1,72 @@ +package com.tom.meeter.infrastructure.components.adapter; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseEventAdapter + extends RecyclerView.Adapter { + + private final ViewHolderEventBinder binder; + private final List events = new ArrayList<>(); + + protected BaseEventAdapter(ViewHolderEventBinder binder) { + this.binder = binder; + } + + public void setData(List newEvents) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( + new EventsDiffCallback(events, newEvents)); + this.events.clear(); + this.events.addAll(newEvents); + diffResult.dispatchUpdatesTo(this); + } + + @Override + public void onBindViewHolder(@NonNull T holder, int position) { + binder.bind(holder, events.get(position)); + } + + @Override + public int getItemCount() { + return events.size(); + } + + static class EventsDiffCallback extends DiffUtil.Callback { + + private final List oldEvents, newEvents; + + public EventsDiffCallback(List oldEvents, List newEvents) { + this.oldEvents = oldEvents; + this.newEvents = newEvents; + } + + @Override + public int getOldListSize() { + return oldEvents.size(); + } + + @Override + public int getNewListSize() { + return newEvents.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldEvents.get(oldItemPosition).getId() + .equals(newEvents.get(newItemPosition).getId()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldEvents.get(oldItemPosition) + .equals(newEvents.get(newItemPosition)); + } + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/EventsCardAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/EventsCardAdapter.java new file mode 100644 index 0000000..d9495b4 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/EventsCardAdapter.java @@ -0,0 +1,27 @@ +package com.tom.meeter.infrastructure.components.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.tom.meeter.databinding.CardItemBinding; +import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; +import com.tom.meeter.infrastructure.components.viewholder.CardItemHolder; + +public class EventsCardAdapter extends BaseEventAdapter { + + private static final String TAG = EventsCardAdapter.class.getCanonicalName(); + + public EventsCardAdapter(ViewHolderEventBinder binder) { + super(binder); + } + + @NonNull + @Override + public CardItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new CardItemHolder( + CardItemBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false)); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnEventClickListener.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnEventClickListener.java new file mode 100644 index 0000000..a17d354 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnEventClickListener.java @@ -0,0 +1,7 @@ +package com.tom.meeter.infrastructure.components.adapter; + +import com.tom.meeter.context.network.dto.EventDTO; + +public interface OnEventClickListener { + void onEventClick(EventDTO event); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderEventBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderEventBinder.java new file mode 100644 index 0000000..c1a6a17 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderEventBinder.java @@ -0,0 +1,41 @@ +package com.tom.meeter.infrastructure.components.binder; + +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; + +import android.content.Context; + +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.infrastructure.components.adapter.OnEventClickListener; +import com.tom.meeter.infrastructure.components.viewholder.CardItemHolder; + +public class PhotoDownloaderEventBinder + implements ViewHolderEventBinder { + + private static final String TAG = PhotoDownloaderEventBinder.class.getCanonicalName(); + + private final Context ctx; + private final ImageDownloader imageDownloader; + private final OnEventClickListener listener; + private final Runnable onAuthFail; + + public PhotoDownloaderEventBinder( + Context ctx, ImageDownloader imageDownloader, + OnEventClickListener listener, Runnable onAuthFail) { + this.ctx = ctx; + this.imageDownloader = imageDownloader; + this.listener = listener; + this.onAuthFail = onAuthFail; + } + + @Override + public void bind(CardItemHolder holder, EventDTO event) { + holder.bind(event.getName(), null, (view) -> listener.onEventClick(event)); + String photoPath = event.getPhotoPath(); + if (photoPath == null) { + return; + } + imageDownloader.downloadEventImage( + photoPath, ctx, (photo) -> holder.updatePhoto(circleImage(photo)), onAuthFail); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventEventBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventEventBinder.java new file mode 100644 index 0000000..1469321 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventEventBinder.java @@ -0,0 +1,66 @@ +package com.tom.meeter.infrastructure.components.binder; + +import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.infrastructure.components.adapter.OnEventClickListener; +import com.tom.meeter.infrastructure.components.viewholder.EventViewHolder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PhotoDownloaderWithCacheEventEventBinder + implements ViewHolderEventBinder { + + private static final String TAG = PhotoDownloaderWithCacheEventEventBinder.class.getCanonicalName(); + + private final Context ctx; + private final ImageDownloader imageDownloader; + private final OnEventClickListener listener; + private final Runnable onAuthFail; + private final Map imagesCache = new ConcurrentHashMap<>(); + + public PhotoDownloaderWithCacheEventEventBinder( + Context ctx, ImageDownloader imgDownloader, + OnEventClickListener listener, Runnable onAuthFail) { + logMethod(TAG, this); + this.ctx = ctx; + this.imageDownloader = imgDownloader; + this.listener = listener; + this.onAuthFail = onAuthFail; + } + + @Override + public void bind(EventViewHolder holder, EventDTO event) { + String photoPath = event.getPhotoPath(); + if (photoPath == null) { + holder.bind( + event.getName(), event.getDescription(), null, + (v) -> listener.onEventClick(event)); + return; + } + Bitmap circledPhotoCache = imagesCache.get(photoPath); + holder.bind( + event.getName(), event.getDescription(), circledPhotoCache, + (v) -> listener.onEventClick(event)); + if (circledPhotoCache != null) { + return; + } + imageDownloader.downloadEventImage( + photoPath, ctx, + photo -> { + Bitmap circled = circleImage(photo); + holder.updatePhoto(circled); + imagesCache.put(photoPath, circled); + Log.d(TAG, "PhotoDownloaderWithCacheEventEventBinder: event " + + "image downloaded for [" + photoPath + "], cache updated."); + }, + onAuthFail); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/ViewHolderEventBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/ViewHolderEventBinder.java new file mode 100644 index 0000000..528ab09 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/ViewHolderEventBinder.java @@ -0,0 +1,10 @@ +package com.tom.meeter.infrastructure.components.binder; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.context.network.dto.EventDTO; + +public interface ViewHolderEventBinder { + + void bind(T holder, EventDTO event); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/CardItemHolder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/CardItemHolder.java new file mode 100644 index 0000000..9a2d5c3 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/CardItemHolder.java @@ -0,0 +1,29 @@ +package com.tom.meeter.infrastructure.components.viewholder; + +import android.graphics.Bitmap; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.databinding.CardItemBinding; + +public class CardItemHolder extends RecyclerView.ViewHolder { + + private final CardItemBinding binding; + + public CardItemHolder(CardItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind( + String name, Bitmap photo, View.OnClickListener clickListener) { + binding.textView.setText(name); + binding.imageView.setImageBitmap(photo); + binding.card.setOnClickListener(clickListener); + } + + public void updatePhoto(Bitmap photo) { + binding.imageView.setImageBitmap(photo); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventViewHolder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/EventViewHolder.java similarity index 68% rename from AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventViewHolder.java rename to AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/EventViewHolder.java index 0d62b6c..7c09313 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/EventViewHolder.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/EventViewHolder.java @@ -1,4 +1,4 @@ -package com.tom.meeter.context.profile.adapter; +package com.tom.meeter.infrastructure.components.viewholder; import android.graphics.Bitmap; import android.view.View; @@ -8,6 +8,7 @@ import com.tom.meeter.databinding.EventViewBinding; public class EventViewHolder extends RecyclerView.ViewHolder { + private final EventViewBinding binding; public EventViewHolder(EventViewBinding binding) { @@ -15,10 +16,16 @@ public EventViewHolder(EventViewBinding binding) { this.binding = binding; } - public void bind(String name, String description, Bitmap photo, View.OnClickListener clickListener) { + public void bind( + String name, String description, Bitmap photo, + View.OnClickListener clickListener) { binding.eventNameCardView.setText(name); binding.eventDescriptionCardView.setText(description); binding.eventPhotoCardView.setImageBitmap(photo); binding.eventCardView.setOnClickListener(clickListener); } + + public void updatePhoto(Bitmap photo) { + binding.eventPhotoCardView.setImageBitmap(photo); + } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRestarterOnAuthFailure.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRestarterOnAuthFailure.java index 7e42a26..cfa13ea 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRestarterOnAuthFailure.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRestarterOnAuthFailure.java @@ -7,7 +7,8 @@ import retrofit2.Call; import retrofit2.Response; -public class ActivityRestarterOnAuthFailure extends DisconnectLogger { +public class ActivityRestarterOnAuthFailure extends HttpErrorLogger { + private static final String TAG = ActivityRestarterOnAuthFailure.class.getCanonicalName(); private final Fragment fragment; @@ -18,6 +19,7 @@ public ActivityRestarterOnAuthFailure(Fragment fragment) { @Override public void onResponse(Call call, Response response) { + super.onResponse(call, response); if (response.code() == HttpCodes.NOT_AUTHENTICATED) { InfrastructureHelper.restartActivityFromFragment(fragment); // TODO when restore from persisted state will be done, if necessary diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/DisconnectLogger.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/DisconnectLogger.java index 1f8c54e..bee1312 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/DisconnectLogger.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/DisconnectLogger.java @@ -6,6 +6,9 @@ import com.tom.meeter.R; +import java.net.ConnectException; +import java.net.SocketTimeoutException; + import retrofit2.Call; import retrofit2.Callback; @@ -21,9 +24,19 @@ public DisconnectLogger(Context ctx) { @Override public void onFailure(Call call, Throwable t) { - Toast.makeText(ctx, R.string.server_is_unreachable, Toast.LENGTH_SHORT).show(); - Log.i(TAG, "DisconnectLogger for " + ctx.getPackageName() - + " : " + ctx.getResources().getString(R.string.server_is_unreachable) - + ", error: " + t.getMessage()); + if (supportedErrorMapping(t)) { + Toast.makeText(ctx, R.string.server_is_unreachable, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "DisconnectLogger for " + ctx.getClass().getSimpleName() + + " : " + ctx.getResources().getString(R.string.server_is_unreachable) + + ", error: " + t.getMessage()); + } + } + + protected boolean supportedErrorMapping(Throwable t) { + if (t instanceof SocketTimeoutException + || t instanceof ConnectException) { + return true; + } + return false; } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ErrorLogger.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ErrorLogger.java new file mode 100644 index 0000000..2e8e51e --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ErrorLogger.java @@ -0,0 +1,32 @@ +package com.tom.meeter.infrastructure.http; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import retrofit2.Call; + +public abstract class ErrorLogger extends DisconnectLogger { + + private static final String TAG = ErrorLogger.class.getCanonicalName(); + + private final Context ctx; + + public ErrorLogger(Context ctx) { + super(ctx); + this.ctx = ctx; + } + + @Override + public void onFailure(Call call, Throwable t) { + if (super.supportedErrorMapping(t)) { + super.onFailure(call, t); + } else { + String message = t.getMessage(); + Toast.makeText(ctx, "ErrorLogger: " + message, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "ErrorLogger for " + ctx.getClass().getSimpleName() + + ", error: " + message); + } + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpClient.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpClient.java index cff713c..7a0dc9b 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpClient.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpClient.java @@ -1,61 +1,58 @@ package com.tom.meeter.infrastructure.http; -import android.util.Log; - -import org.jetbrains.annotations.NotNull; +import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; -import java.io.IOException; +import android.util.Log; import okhttp3.Call; -import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; -import okhttp3.Response; public class HttpClient { - private static final String TAG = HttpClient.class.getCanonicalName(); - private static final MediaType JSON = MediaType.get("application/json"); - private static final Callback CALLBACK_LOGGER = new Callback() { - @Override - public void onFailure(@NotNull Call call, @NotNull IOException e) { - Log.d(TAG, "IOException: ", e); - } + private final String serverUrl; - @Override - public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { - Log.d(TAG, "usersResp: " + response.body().string()); - } - }; + public HttpClient(String serverUrl) { + this.serverUrl = serverUrl; + } + + private static final String TAG = HttpClient.class.getCanonicalName(); + private static final MediaType JSON = MediaType.get("application/json"); private final OkHttpClient client = new OkHttpClient(); - public String post(String url, String json) throws IOException { - RequestBody body = RequestBody.create(JSON, json); - Request request = new Request.Builder() - .url(url) - .post(body) - .build(); - try (Response response = client.newCall(request).execute()) { - return response.body().string(); - } + public Call post(String json) { + return post(json, serverUrl); + } + + public Call post(String url, String json) { + return client.newCall( + new Request.Builder() + .url(url) + .post(RequestBody.create(json, JSON)) + .build()); } - public Call get(String url, Callback callback) { + public Call get(String url) { Log.d(TAG, "Making GET: '" + url + "'..."); - Request request = new Request.Builder() - .url(url) - .get() - .build(); - Call call = client.newCall(request); - call.enqueue(callback); - return call; + return client.newCall( + new Request.Builder() + .url(url) + .get() + .build()); } - public void get(String url) { - get(url, CALLBACK_LOGGER); + public Call patch(String json, String url, String authHeader) { + String fullPath = serverUrl + url; + Log.d(TAG, "Making PATCH: '" + fullPath + "' ..."); + return client.newCall( + new Request.Builder() + .url(fullPath) + .header(AUTH_HEADER, authHeader) + .patch(RequestBody.create(json, JSON)) + .build()); } //new HttpClient().get(initServerPath(getBaseContext()) + "/users"); diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpErrorLogger.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpErrorLogger.java new file mode 100644 index 0000000..95b3b08 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpErrorLogger.java @@ -0,0 +1,40 @@ +package com.tom.meeter.infrastructure.http; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; + +import android.content.Context; +import android.util.Log; + +import java.io.IOException; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + +public class HttpErrorLogger extends ErrorLogger { + + private static final String TAG = HttpErrorLogger.class.getCanonicalName(); + + private final Context ctx; + + public HttpErrorLogger(Context ctx) { + super(ctx); + this.ctx = ctx; + } + + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + return; + } + String errorMessage; + try (ResponseBody r = response.errorBody()) { + errorMessage = r.string(); + } catch (IOException e) { + errorMessage = "Unable to extract error body."; + } + int code = response.code(); + showMessage(ctx, code + "/" + errorMessage); + Log.e(TAG, "HTTP request error: " + code + "/" + errorMessage); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelKey.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelKey.java index 3c07fec..822e2ff 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelKey.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelKey.java @@ -12,6 +12,6 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @MapKey -@interface ViewModelKey { +public @interface ViewModelKey { Class value(); } \ No newline at end of file diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelModule.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelModule.java index 3080e89..56b3568 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelModule.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/injection/viewmodel/ViewModelModule.java @@ -4,12 +4,8 @@ import androidx.lifecycle.ViewModel; -import com.tom.meeter.context.event.viewmodel.EventViewModel; import com.tom.meeter.context.profile.viewmodel.ProfileEventsViewModel; import com.tom.meeter.context.profile.viewmodel.ProfileViewModel; -import com.tom.meeter.context.profile.viewmodel.UserEventsViewModel; -import com.tom.meeter.context.profile.viewmodel.UserProfileViewModel; -import com.tom.meeter.context.user.viewmodel.UserViewModel; import dagger.Binds; import dagger.Module; @@ -24,16 +20,6 @@ public ViewModelModule() { Log.d(TAG, "Configuring ViewModelModule..."); } - @Binds - @IntoMap - @ViewModelKey(UserProfileViewModel.class) - abstract ViewModel userProfileViewModel(UserProfileViewModel userProfileViewModel); - - @Binds - @IntoMap - @ViewModelKey(UserEventsViewModel.class) - abstract ViewModel userEventViewModel(UserEventsViewModel userEventsViewModel); - @Binds @IntoMap @ViewModelKey(ProfileViewModel.class) @@ -43,15 +29,4 @@ public ViewModelModule() { @IntoMap @ViewModelKey(ProfileEventsViewModel.class) abstract ViewModel profileEventsViewModel(ProfileEventsViewModel profileEventsViewModel); - - @Binds - @IntoMap - @ViewModelKey(UserViewModel.class) - abstract ViewModel userViewModel(UserViewModel userViewModel); - - @Binds - @IntoMap - @ViewModelKey(EventViewModel.class) - abstract ViewModel eventViewModel(EventViewModel eventViewModel); - } diff --git a/AndroidClient/src/main/res/drawable/bg_button_selected.xml b/AndroidClient/src/main/res/drawable/bg_button_selected.xml new file mode 100644 index 0000000..cecfec4 --- /dev/null +++ b/AndroidClient/src/main/res/drawable/bg_button_selected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AndroidClient/src/main/res/drawable/bg_button_unselected.xml b/AndroidClient/src/main/res/drawable/bg_button_unselected.xml new file mode 100644 index 0000000..215c9fe --- /dev/null +++ b/AndroidClient/src/main/res/drawable/bg_button_unselected.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/AndroidClient/src/main/res/layout/activity_event_editable.xml b/AndroidClient/src/main/res/layout/activity_event_editable.xml new file mode 100644 index 0000000..102f0f0 --- /dev/null +++ b/AndroidClient/src/main/res/layout/activity_event_editable.xml @@ -0,0 +1,272 @@ + + + + + + + + + +