diff --git a/AndroidClient/build.gradle b/AndroidClient/build.gradle index 46f57fd..5d1a434 100644 --- a/AndroidClient/build.gradle +++ b/AndroidClient/build.gradle @@ -50,7 +50,9 @@ dependencies { implementation libs.lifecycle.extensions annotationProcessor libs.lifecycle.compiler - implementation libs.socketioclient + implementation(libs.socketioclient) { + exclude group: 'org.json', module: 'json' + } implementation libs.eventbus implementation libs.play.services.maps @@ -62,8 +64,6 @@ dependencies { // https://mvnrepository.com/artifact/com.mikepenz/google-material-typeface implementation("com.mikepenz:google-material-typeface:3.0.1.3.original") - implementation libs.gson - implementation libs.rxandroid implementation libs.rxjava @@ -100,7 +100,7 @@ dependencies { implementation libs.guava // https://mvnrepository.com/artifact/androidx.preference/preference - implementation 'androidx.preference:preference:1.2.1' + implementation libs.preference implementation libs.core @@ -110,7 +110,7 @@ dependencies { androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { exclude group: 'com.android.support', module: 'support-annotations' }) - androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' //androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.0.4" androidTestImplementation libs.testcore diff --git a/AndroidClient/lint-baseline.xml b/AndroidClient/lint-baseline.xml index 8bea0ee..e76a79a 100644 --- a/AndroidClient/lint-baseline.xml +++ b/AndroidClient/lint-baseline.xml @@ -1,53 +1,6 @@ - - - - - - - - - - - - - - - - - - - - + errorLine1=" private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + file="src/main/java/com/tom/meeter/infrastructure/common/DateHelper.java" + line="31" + column="52"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -198,96 +74,19 @@ errorLine2=" ~~~~~~~"> + message="A newer version of io.reactivex.rxjava2:rxjava than 2.2.6 is available: 2.2.9" + errorLine1="rxjava = "2.2.6"" + errorLine2=" ~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + column="10"/> @@ -308,7 +107,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -319,7 +118,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -329,8 +128,8 @@ errorLine1=" <string name="restricted_mode">Restricted Mode can help to hide videos with potentially mature content.No filter is 100% accurate, but it should help you to avoid most of this type of content.</string>" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -340,8 +139,8 @@ errorLine1=" <string name="restricted_mode">Restricted Mode can help to hide videos with potentially mature content.No filter is 100% accurate, but it should help you to avoid most of this type of content.</string>" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -390,25 +189,69 @@ + id="ObsoleteLayoutParam" + message="Invalid layout param in a `RelativeLayout`: `layout_weight`" + errorLine1=" android:layout_weight="1"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + + + + + id="Overdraw" + message="Possible overdraw: Root element paints background `@color/c0deef` with a theme that also paints a background (inferred theme is `@style/MyMaterialTheme`)" + errorLine1=" android:background="@color/c0deef">" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + file="src/main/res/layout/activity_user.xml" + line="6" + column="5"/> + + + + + + + + + + + + + message="The resource `R.layout.event_layout_editable` appears to be unused" + errorLine1="<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"" + errorLine2="^"> + + + + + file="src/main/res/layout/fragment_profile_old.xml" + line="2" + column="1"/> @@ -582,7 +440,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -593,18 +451,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - @@ -615,7 +462,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -626,106 +473,205 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.user_format` appears to be unused" + errorLine1=" <string name="user_format">%1$s %2$s, %3$s, %4$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.user_about_format` appears to be unused" + errorLine1=" <string name="user_about_format">About: %1$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.user_about` appears to be unused" + errorLine1=" <string name="user_about">About:</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.profile_user_id` appears to be unused" + errorLine1=" <string name="profile_user_id">ID:</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.profile_user_id_format` appears to be unused" + errorLine1=" <string name="profile_user_id_format">ID: %1$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.profile_user_name_format` appears to be unused" + errorLine1=" <string name="profile_user_name_format">User: %1$s %2$s.</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.profile_gender_format` appears to be unused" + errorLine1=" <string name="profile_gender_format">Gender: %1$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message="The resource `R.string.profile_info_format` appears to be unused" + errorLine1=" <string name="profile_info_format">Information: %1$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -736,7 +682,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -747,7 +693,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -758,7 +704,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -769,7 +715,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -780,7 +726,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -791,7 +737,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -802,7 +748,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -813,7 +759,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -824,7 +770,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -835,7 +781,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -846,7 +792,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -857,7 +803,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -868,7 +814,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -879,7 +825,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -890,7 +836,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -901,7 +847,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -912,7 +858,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -923,7 +869,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -934,7 +880,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -945,7 +891,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -956,7 +902,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -967,7 +913,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -978,7 +924,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -989,7 +935,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1000,7 +946,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1011,7 +957,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1022,7 +968,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1033,7 +979,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1044,7 +990,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1055,7 +1001,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1066,7 +1012,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1077,7 +1023,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1088,7 +1034,18 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + + + + @@ -1099,73 +1056,134 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + id="UnusedResources" + message="The resource `R.drawable.user_612x612` appears to be unused"> + file="src/main/res/drawable/user_612x612.jpg"/> + id="IconLocation" + message="Found bitmap drawable `res/drawable/event_512_512.png` in densityless folder"> + file="src/main/res/drawable/event_512_512.png"/> + id="IconLocation" + message="Found bitmap drawable `res/drawable/meeter_logo_1024.png` in densityless folder"> + file="src/main/res/drawable/meeter_logo_1024.png"/> + id="IconLocation" + message="Found bitmap drawable `res/drawable/meeter_logo_612.png` in densityless folder"> + file="src/main/res/drawable/meeter_logo_612.png"/> + message="Found bitmap drawable `res/drawable/nav_menu_header_bg.jpg` in densityless folder"> + file="src/main/res/drawable/nav_menu_header_bg.jpg"/> + message="Found bitmap drawable `res/drawable/random_1.jpg` in densityless folder"> + file="src/main/res/drawable/random_1.jpg"/> + message="Found bitmap drawable `res/drawable/random_10.jpg` in densityless folder"> + file="src/main/res/drawable/random_10.jpg"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1312,8 +1337,8 @@ errorLine1=" <EditText" errorLine2=" ~~~~~~~~"> @@ -1323,19 +1348,96 @@ errorLine1=" <EditText" errorLine2=" ~~~~~~~~"> + errorLine1=" <EditText" + errorLine2=" ~~~~~~~~"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1350,6 +1452,83 @@ column="10"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1456,7 +1679,7 @@ errorLine2=" ~~~~~~~~"> @@ -1577,7 +1800,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1588,7 +1811,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1599,30 +1822,41 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + errorLine1=" androidTestImplementation 'androidx.test.ext:junit:1.2.1'" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + line="116" + column="31"/> + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageView" + errorLine2=" ~~~~~~~~~"> + file="src/main/res/layout/activity_upload_image.xml" + line="8" + column="6"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidClient/src/main/AndroidManifest.xml b/AndroidClient/src/main/AndroidManifest.xml index b06c7d5..6c36365 100644 --- a/AndroidClient/src/main/AndroidManifest.xml +++ b/AndroidClient/src/main/AndroidManifest.xml @@ -5,8 +5,8 @@ - - + @@ -48,7 +48,6 @@ + android:name=".context.event.activity.EventDispatcherActivity" + android:noHistory="true" + android:theme="@style/Theme.Transparent.NoUI" /> + + + + + + + + onToken, Runnable onCancelledAuth, AccountManager am, Activity activity, TokenService tokenService) { @@ -84,6 +91,16 @@ public static void checkToken( }, null); return; } + checkTokenWithRetry(tokenService, token, am, activity, onToken, onCancelledAuth, 0); + } + + private static void simpleCheckToken( + TokenService tokenService, + String token, + AccountManager am, + Activity activity, + Consumer onToken, + Runnable onCancelledAuth) { tokenService.checkToken(Globals.getAuthHeader(token)).enqueue( new ErrorLogger<>(activity) { @Override @@ -98,6 +115,54 @@ public void onResponse(Call call, Response response) { }); } + private static void checkTokenWithRetry( + TokenService tokenService, + String token, + AccountManager am, + Activity activity, + Consumer onToken, + Runnable onCancelledAuth, + int attempt) { + tokenService.checkToken(Globals.getAuthHeader(token)) + .enqueue(new HttpErrorLogger<>(activity) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + onToken.accept(token); + return; + } + if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { + invalidateToken(am, activity, onToken, onCancelledAuth); + return; + } + } + + @Override + public void onFailure(Call call, Throwable t) { + super.onFailure(call, t); + if (supportedErrorMapping(t)) { + retryIfPossible(); + return; + } + } + + private void retryIfPossible() { + if (attempt < MAX_RETRIES) { + Log.w(TAG, "Повтор попытки " + (attempt + 1)); + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> checkTokenWithRetry( + tokenService, token, am, activity, onToken, + onCancelledAuth, attempt + 1), + RETRY_DELAY_MS); + } else { + Log.e(TAG, "Превышено количество попыток"); + } + } + }); + } + public static void invalidateToken( AccountManager am, Activity activity, Consumer onToken, Runnable onCancelledAuth) { 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 e834b05..74eda43 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,9 +2,11 @@ import android.app.Application; -import com.tom.meeter.context.event.activity.EventActivity; +import com.tom.meeter.context.event.activity.EventDispatcherActivity; import com.tom.meeter.context.event.activity.EventLocationMapActivity; import com.tom.meeter.context.event.activity.EventOnMapActivity; +import com.tom.meeter.context.event.activity.ProfileEventActivity; +import com.tom.meeter.context.event.activity.UserEventActivity; import com.tom.meeter.context.event.service.EventService; import com.tom.meeter.context.event.viewmodel.EventViewModelModule; import com.tom.meeter.context.image.ImageComponent; @@ -39,7 +41,11 @@ interface Builder { EventComponent build(); } - void inject(EventActivity eventActivity); + void inject(EventDispatcherActivity eventDispatcherActivity); + + void inject(ProfileEventActivity profileEventActivity); + + void inject(UserEventActivity userEventActivity); void inject(EventOnMapActivity eventOnMapActivity); 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 deleted file mode 100644 index 0d97e26..0000000 --- a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventActivity.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.tom.meeter.context.event.activity; - -import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; -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.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.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(); - - ViewBinding binding; - @Inject - TokenService tokenService; - @Inject - EventService eventService; - @Inject - ViewModelFactory viewModelFactory; - private EventViewModel eventViewModel; - 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(); - if (extras == null) { - Log.d(TAG, "Unable to create event activity without extras."); - finish(); - return; - } - 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()).getEventComponent().inject(this); - accountManager = AccountManager.get(this); - - //setToken(accountManager, Launcher.EXPIRED); - checkToken((token) -> onInit(token, eventId), this::finish, accountManager, this, tokenService); - } - - 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 -> { - 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( - @Nullable View parent, @NonNull String name, @NonNull Context ctx, - @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/EventDispatcherActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventDispatcherActivity.java new file mode 100644 index 0000000..f0c9aa1 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/EventDispatcherActivity.java @@ -0,0 +1,97 @@ +package com.tom.meeter.context.event.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.event.activity.ProfileEventActivity.dispatchToProfileEventActivity; +import static com.tom.meeter.context.event.activity.UserEventActivity.dispatchToUserEventActivity; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.appcompat.app.AppCompatActivity; + +import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.event.service.EventService; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class EventDispatcherActivity extends AppCompatActivity { + + public static final String EVENT_ID_KEY = "event_id"; + + private static final String TAG = EventDispatcherActivity.class.getCanonicalName(); + @Inject + TokenService tokenService; + @Inject + EventService eventService; + private AccountManager accountManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + Log.d(TAG, "Unable to create event activity without extras."); + finish(); + return; + } + 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()).getEventComponent().inject(this); + + accountManager = AccountManager.get(this); + + //setToken(accountManager, Launcher.EXPIRED); + checkToken((token) -> onInit(token, eventId), this::finish, + accountManager, this, tokenService); + } + + private void onInit(String token, String eventId) { + //TODO make isCreatedByMe + eventService.getEvent(Globals.getAuthHeader(token), eventId) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + EventDTO event = resp.body(); + if (AuthHelper.getUserUuid(accountManager).equals(event.getCreatorId())) { + dispatchToProfileEventActivity(EventDispatcherActivity.this, eventId); + } else { + dispatchToUserEventActivity(EventDispatcherActivity.this, eventId); + } + } + } + }); + } + + + 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, EventDispatcherActivity.class) + .putExtra(EventDispatcherActivity.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 index c83cc54..693dc12 100644 --- 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 @@ -101,8 +101,8 @@ protected void onCreate(Bundle savedInstanceState) { 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); + resultIntent.putExtra(ProfileEventActivity.EXTRA_LAT, position.latitude); + resultIntent.putExtra(ProfileEventActivity.EXTRA_LNG, position.longitude); setResult(RESULT_OK, resultIntent); finish(); } else { 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 index b253c25..b0a704d 100644 --- 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 @@ -97,8 +97,8 @@ public void onMapReady(GoogleMap googleMap) { public void onResponse(Call call, Response response) { if (response.code() == HttpCodes.OK) { EventDTO event = response.body(); - Double latitude = event.getLatitude(); - Double longitude = event.getLongitude(); + Float latitude = event.getLatitude(); + Float longitude = event.getLongitude(); if (latitude == null || longitude == null) { showMessage(EventOnMapActivity.this, "Event location is not set yet."); return; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/ProfileEventActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/ProfileEventActivity.java new file mode 100644 index 0000000..5cc2467 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/ProfileEventActivity.java @@ -0,0 +1,287 @@ +package com.tom.meeter.context.event.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.event.activity.EventDispatcherActivity.EVENT_ID_KEY; +import static com.tom.meeter.context.event.activity.EventLocationMapActivity.createEventLocationMapActivityIntent; +import static com.tom.meeter.context.event.utils.Utils.createUpdateEventRequest; +import static com.tom.meeter.context.image.activity.BaseUploadActivity.PHOTO_PATH_RESULT; +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.DateHelper.showDateTimePicker; +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.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; + +import com.tom.meeter.App; +import com.tom.meeter.R; +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.image.ImageDownloader; +import com.tom.meeter.context.image.activity.UploadEventImageActivity; +import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.profile.activity.ProfileActivity; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.databinding.ActivityEventEditableBinding; +import com.tom.meeter.infrastructure.common.Globals; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import java.util.Objects; + +import javax.inject.Inject; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + +public class ProfileEventActivity extends AppCompatActivity { + + public static final String EXTRA_LAT = "extra_lat"; + public static final String EXTRA_LNG = "extra_lng"; + + private static final String TAG = ProfileEventActivity.class.getCanonicalName(); + + @Inject + TokenService tokenService; + @Inject + EventService eventService; + @Inject + ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + + private ActivityEventEditableBinding binding; + private AccountManager accountManager; + private EventViewModel eventViewModel; + private ActivityResultLauncher mapResult; + + private EventDTO eventCache; + private ResponseBody photoCache; + private boolean isEditableModeEnabled = false; + + private final ActivityResultLauncher imageUploadLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { + return; + } + String photoPath = result.getData().getStringExtra(PHOTO_PATH_RESULT); + downloadAndUpdateLayoutPhoto(photoPath); + binding.photoPath.setText(photoPath); + }); + + void downloadAndUpdateLayoutPhoto(String photoPath) { + imgDownloader.downloadEventImage(photoPath, this, + this::updateLayoutPhoto, + this::recreate); + } + + private void updateLayoutPhoto(ResponseBody photo) { + photoCache = photo; + binding.photo.setImageBitmap(circleImage(photoCache, 600, 600)); + } + + private void switchEditMode() { + isEditableModeEnabled = !isEditableModeEnabled; + + binding.selectPhotoButton.setEnabled(isEditableModeEnabled); + binding.locationMapButton.setEnabled(isEditableModeEnabled); + binding.selectStartingDateButton.setEnabled(isEditableModeEnabled); + binding.selectEndingDateButton.setEnabled(isEditableModeEnabled); + + binding.name.setEnabled(isEditableModeEnabled); + binding.description.setEnabled(isEditableModeEnabled); + binding.latitude.setEnabled(isEditableModeEnabled); + binding.longitude.setEnabled(isEditableModeEnabled); + binding.starting.setEnabled(isEditableModeEnabled); + binding.ending.setEnabled(isEditableModeEnabled); + binding.city.setEnabled(isEditableModeEnabled); + binding.editSaveButton.setText(isEditableModeEnabled ? R.string.save : R.string.edit); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mapResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() != RESULT_OK || result.getData() == null) { + return; + } + double lat = result.getData().getDoubleExtra(EXTRA_LAT, 0.0); + double lng = result.getData().getDoubleExtra(EXTRA_LNG, 0.0); + binding.latitude.setText(String.valueOf(lat)); + binding.longitude.setText(String.valueOf(lng)); + }); + + logMethod(TAG, this); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + Log.d(TAG, "Unable to create event activity without extras."); + finish(); + return; + } + 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()).getEventComponent().inject(this); + + accountManager = AccountManager.get(this); + + //setToken(accountManager, Launcher.EXPIRED); + checkToken((token) -> onInit(token, eventId), this::finish, + accountManager, this, tokenService); + } + + 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 -> { + String userUuid = AuthHelper.getUserUuid(accountManager); + String eventCreatorId = event.getCreatorId(); + if (!userUuid.equals(eventCreatorId)) { + Log.e(TAG, "Profile event activity for non creator " + + userUuid + "/" + eventId + " : " + eventCreatorId); + finish(); + } + eventCache = event; + updateLayout(); + }); + eventViewModel.getEventPhotoLiveData() + .observe(this, this::updateLayoutPhoto); + initLayout(token); + } + + private void initLayout(String token) { + binding = ActivityEventEditableBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + binding.selectStartingDateButton.setOnClickListener( + v -> showDateTimePicker(this, binding.starting)); + binding.selectEndingDateButton.setOnClickListener( + v -> showDateTimePicker(this, binding.ending)); + binding.locationMapButton.setOnClickListener( + v -> mapResult.launch( + createEventLocationMapActivityIntent(this, eventCache.getId()))); + binding.deleteEventButton.setOnClickListener(v -> showAlertDialog()); + + binding.editSaveButton.setOnClickListener(v -> { + if (isEditableModeEnabled) { + UpdateEventRequest req = createUpdateEventRequest(eventCache, binding); + if (req.isEmpty()) { + showMessage(this, R.string.empty_update_request_is_not_sent); + switchEditMode(); + return; + } + eventService.updateEvent(Globals.getAuthHeader(token), eventCache.getId(), req) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + String oldPhotoPath = eventCache.getPhotoPath(); + eventCache = resp.body(); + if (!Objects.equals(oldPhotoPath, eventCache.getPhotoPath())) { + downloadAndUpdateLayoutPhoto(eventCache.getPhotoPath()); + } + showMessage(ProfileEventActivity.this, R.string.saved); + } + updateLayout(); + } + }); + } + switchEditMode(); + }); + binding.selectPhotoButton.setOnClickListener( + v -> imageUploadLauncher.launch( + new Intent(this, UploadEventImageActivity.class))); + } + + private void showAlertDialog() { + new AlertDialog.Builder(this) + .setTitle(R.string.delete_event) + .setMessage(R.string.are_you_sure_delete_event) + .setPositiveButton(R.string.delete, (dialog, which) -> { + eventService.deleteEvent(getAuthHeader(accountManager), eventCache.getId()) + .enqueue(new HttpErrorLogger<>(getApplicationContext()) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.isSuccessful()) { + showMessage(ProfileEventActivity.this, getString(R.string.deleted)); + startActivity(new Intent(getApplicationContext(), ProfileActivity.class)); + finish(); + } + } + }); + dialog.dismiss(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + } + + private void updateLayout() { + binding.photoPath.setText(eventCache.getPhotoPath()); + binding.name.setText(eventCache.getName()); + binding.eventCreated.setText(UI_DATE_TIME_FORMAT.format(eventCache.getCreated())); + + binding.description.setText(eventCache.getDescription()); + binding.latitude.setText(textOrNull(eventCache.getLatitude())); + binding.longitude.setText(textOrNull(eventCache.getLongitude())); + binding.starting.setText(dateOrNull(eventCache.getStarting())); + binding.ending.setText(dateOrNull(eventCache.getEnding())); + binding.city.setText(eventCache.getCity()); + } + + + @Nullable + @Override + public View onCreateView( + @Nullable View parent, @NonNull String name, @NonNull Context ctx, + @NonNull AttributeSet attrs) { + return super.onCreateView(parent, name, ctx, attrs); + } + + + public static void dispatchToProfileEventActivity(Context ctx, String eventId) { + ctx.startActivity(createProfileEventActivityIntent(ctx, eventId)); + } + + public static Intent createProfileEventActivityIntent(Context ctx, String eventId) { + return new Intent(ctx, ProfileEventActivity.class) + .putExtra(EventDispatcherActivity.EVENT_ID_KEY, eventId); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/UserEventActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/UserEventActivity.java new file mode 100644 index 0000000..634306c --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/event/activity/UserEventActivity.java @@ -0,0 +1,136 @@ +package com.tom.meeter.context.event.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.event.activity.EventOnMapActivity.dispatchToEventOnMapActivity; +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 android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; + +import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +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.databinding.ActivityEventReadableBinding; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import javax.inject.Inject; + +public class UserEventActivity extends AppCompatActivity { + + private static final String TAG = UserEventActivity.class.getCanonicalName(); + + ActivityEventReadableBinding binding; + @Inject + TokenService tokenService; + @Inject + EventService eventService; + @Inject + ViewModelFactory viewModelFactory; + private EventViewModel eventViewModel; + private AccountManager accountManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + Log.d(TAG, "Unable to create event activity without extras."); + finish(); + return; + } + String eventId = extras.getString(EventDispatcherActivity.EVENT_ID_KEY); + if (eventId == null) { + Log.d(TAG, "Unable to create event activity without 'event_id' provided."); + finish(); + return; + } + + ((App) getApplication()).getEventComponent().inject(this); + accountManager = AccountManager.get(this); + + //setToken(accountManager, Launcher.EXPIRED); + checkToken((token) -> onInit(token, eventId), + this::finish, accountManager, this, tokenService); + } + + 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 -> { + String userUuid = AuthHelper.getUserUuid(accountManager); + String eventCreatorId = event.getCreatorId(); + if (userUuid.equals(eventCreatorId)) { + Log.e(TAG, "User event activity for" + + " creator " + userUuid + "/" + eventId + " : " + eventCreatorId); + finish(); + } + initLayout(event); + eventViewModel.getEventPhotoLiveData() + .observe( + this, photo -> binding.eventPhoto.setImageBitmap( + circleImage(photo, 600, 600))); + }); + + } + + private void initLayout(EventDTO event) { + binding = ActivityEventReadableBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + binding.eventCreator.setOnClickListener( + v -> dispatchToUserActivity(this, event.getCreatorId())); + binding.locationMapButton.setOnClickListener( + v -> dispatchToEventOnMapActivity(this, event.getId())); + + binding.name.setText(event.getName()); + binding.eventCreated.setText(UI_DATE_TIME_FORMAT.format(event.getCreated())); + binding.description.setText(event.getDescription()); + binding.latitude.setText(textOrNull(event.getLatitude())); + binding.longitude.setText(textOrNull(event.getLongitude())); + binding.starting.setText(dateOrNull(event.getStarting())); + binding.ending.setText(dateOrNull(event.getEnding())); + binding.city.setText(event.getCity()); + } + + @Nullable + @Override + public View onCreateView( + @Nullable View parent, @NonNull String name, @NonNull Context ctx, + @NonNull AttributeSet attrs) { + return super.onCreateView(parent, name, ctx, attrs); + } + + + public static void dispatchToUserEventActivity(Context ctx, String eventId) { + ctx.startActivity(createUserEventActivityIntent(ctx, eventId)); + } + + public static Intent createUserEventActivityIntent(Context ctx, String eventId) { + return new Intent(ctx, UserEventActivity.class) + .putExtra(EventDispatcherActivity.EVENT_ID_KEY, eventId); + } +} 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 e8b107e..cc3308c 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 @@ -7,6 +7,7 @@ import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.PATCH; @@ -21,4 +22,7 @@ public interface EventService { Call updateEvent( @Header(AUTH_HEADER) String authHeader, @Path("id") String eventId, @Body UpdateEventRequest req); + + @DELETE("/event/{id}") + Call deleteEvent(@Header(AUTH_HEADER) String authHeader, @Path("id") String eventId); } 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 index 7f57b4c..b6ca0c2 100644 --- 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 @@ -16,40 +16,41 @@ private Utils() { } public static UpdateEventRequest createUpdateEventRequest( - EventDTO event, ActivityEventEditableBinding eBinding) { + EventDTO event, ActivityEventEditableBinding binding) { UpdateEventRequest req = new UpdateEventRequest(); - String eventNameChange = getStringOrNull(eBinding.eventName.getText()); + String eventNameChange = getStringOrNull(binding.name.getText()); if (!Objects.equals(event.getName(), eventNameChange)) { req.setName(eventNameChange); } - String eventDescrChange = getStringOrNull(eBinding.eventDescription.getText()); + String eventDescrChange = getStringOrNull(binding.description.getText()); if (!Objects.equals(event.getDescription(), eventDescrChange)) { req.setDescription(eventDescrChange); } - OffsetDateTime eventStartingChange = getOffsetDateTime(eBinding.eventStarting.getText()); + OffsetDateTime eventStartingChange = getOffsetDateTime(binding.starting.getText()); if (!Objects.equals(event.getStarting(), eventStartingChange)) { req.setStarting(eventStartingChange); } - OffsetDateTime eventEndingChange = getOffsetDateTime(eBinding.eventEnding.getText()); + OffsetDateTime eventEndingChange = getOffsetDateTime(binding.ending.getText()); if (!Objects.equals(event.getEnding(), eventEndingChange)) { req.setEnding(eventEndingChange); } - String eventCityChange = getStringOrNull(eBinding.eventCity.getText()); + String eventCityChange = getStringOrNull(binding.city.getText()); if (!Objects.equals(event.getCity(), eventCityChange)) { req.setCity(eventCityChange); } - //TODO FLOAT -> DOUBLE - Float eventLatitudeChange = getFloatOrNull(eBinding.eventLatitude.getText()); + Float eventLatitudeChange = getFloatOrNull(binding.latitude.getText()); if (!Objects.equals(event.getLatitude(), eventLatitudeChange)) { req.setLatitude(eventLatitudeChange); } - //TODO FLOAT -> DOUBLE - Float eventLongitudeChange = getFloatOrNull(eBinding.eventLongitude.getText()); + Float eventLongitudeChange = getFloatOrNull(binding.longitude.getText()); if (!Objects.equals(event.getLongitude(), eventLongitudeChange)) { req.setLongitude(eventLongitudeChange); } - //TODO: eventCache.getPhotoPath(); + String photoPathChange = getStringOrNull(binding.photoPath.getText()); + if (!Objects.equals(event.getPhotoPath(), photoPathChange)) { + req.setPhotoPath(photoPathChange); + } 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 c403dc6..ff37c3d 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 @@ -3,7 +3,6 @@ import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.app.Activity; -import android.util.Log; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -14,8 +13,8 @@ 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.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; import javax.inject.Inject; @@ -42,11 +41,12 @@ public EventViewModel(EventService eventService, ImageDownloader imageDownloader public void fetchEventInformation(String token, String eventId, Activity activity) { eventService.getEvent(Globals.getAuthHeader(token), eventId).enqueue( - new ErrorLogger<>(activity) { + new HttpErrorLogger<>(activity) { @Override - public void onResponse(Call call, Response response) { - EventDTO body = response.body(); - if (response.code() == HttpCodes.OK && body != null) { + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + EventDTO body = resp.body(); + if (resp.code() == HttpCodes.OK && body != null) { eventLiveData.setValue(body); String photoPath = body.getPhotoPath(); if (photoPath != null) { @@ -58,10 +58,9 @@ public void onResponse(Call call, Response response) { } return; } - if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { activity.recreate(); } - Log.i(TAG, "/event/{id}: " + response.code() + " : " + body); } } ); 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 index c4e103f..ac567e0 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageComponent.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageComponent.java @@ -2,6 +2,8 @@ import android.app.Application; +import com.tom.meeter.context.image.activity.BaseUploadActivity; + import javax.inject.Singleton; import dagger.BindsInstance; @@ -20,4 +22,6 @@ interface Builder { ImageComponent build(); } + + void inject(BaseUploadActivity baseUploadActivity); } 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 index d143032..a0093cd 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageDownloader.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/ImageDownloader.java @@ -5,11 +5,10 @@ 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 com.tom.meeter.infrastructure.http.HttpErrorLogger; import java.util.function.Consumer; @@ -34,19 +33,19 @@ public void downloadEventImage( String photoPath, Context ctx, Consumer onDownloaded, Runnable onNotAuthenticated) { imageService.downloadEventImage(getAuthHeader(AccountManager.get(ctx)), photoPath) - .enqueue(new ErrorLogger<>(ctx) { + .enqueue(new HttpErrorLogger<>(ctx) { @Override public void onResponse(Call call, Response response) { + super.onResponse(call, response); //Log.d(TAG, "/images/event" + photoPath + " downloaded..."); - try (ResponseBody body = response.body()) { - if (response.code() == HttpCodes.OK && body != null) { - onDownloaded.accept(response.body()); + if (response.code() == HttpCodes.OK) { + try (ResponseBody body = response.body()) { + onDownloaded.accept(body); return; } - if (response.code() == HttpCodes.NOT_AUTHENTICATED) { - onNotAuthenticated.run(); - } - Log.i(TAG, "/images/event/: " + response.code() + " : " + body); + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + onNotAuthenticated.run(); } } }); @@ -56,18 +55,18 @@ public void downloadUserImage( String photoPath, Context ctx, Consumer onDownloaded, Runnable onNotAuthenticated) { imageService.downloadUserImage(getAuthHeader(AccountManager.get(ctx)), photoPath) - .enqueue(new ErrorLogger<>(ctx) { + .enqueue(new HttpErrorLogger<>(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()); + super.onResponse(call, response); + if (response.code() == HttpCodes.OK) { + try (ResponseBody body = response.body()) { + onDownloaded.accept(body); return; } - if (response.code() == HttpCodes.NOT_AUTHENTICATED) { - onNotAuthenticated.run(); - } - Log.i(TAG, "/images/user/: " + response.code() + " : " + body); + } + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + onNotAuthenticated.run(); } } }); diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/Utils/FileUtils.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/Utils/FileUtils.java new file mode 100644 index 0000000..189987f --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/Utils/FileUtils.java @@ -0,0 +1,36 @@ +package com.tom.meeter.context.image.Utils; + +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +public class FileUtils { + public static File getFileFromUri(Context context, Uri uri) { + + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String fileName = UUID.randomUUID() + ".jpg"; + File tempFile = new File(context.getCacheDir(), fileName); + FileOutputStream outputStream = new FileOutputStream(tempFile); + copyStream(inputStream, outputStream); + outputStream.close(); + return tempFile; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static void copyStream(InputStream input, OutputStream output) throws IOException { + byte[] buffer = new byte[8192]; // 8 KB буфер + int bytesRead; + while ((bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + } +} \ No newline at end of file diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/BaseUploadActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/BaseUploadActivity.java new file mode 100644 index 0000000..41c245d --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/BaseUploadActivity.java @@ -0,0 +1,187 @@ +package com.tom.meeter.context.image.activity; + +import android.accounts.AccountManager; +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.view.View; +import android.webkit.MimeTypeMap; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.image.service.ImageService; +import com.tom.meeter.databinding.ActivityUploadImageBinding; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.inject.Inject; + +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; + +public abstract class BaseUploadActivity extends AppCompatActivity { + protected static final int REQUEST_PICK_IMAGE = 100; + public static final String PHOTO_PATH_RESULT = "photoPath"; + @Inject + ImageService imageService; + private ActivityUploadImageBinding binding; + private ActivityResultLauncher pickImageLauncher; + private AccountManager accountManager; + + protected Uri selectedImageUri; + + protected abstract String getUploadUrl(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityUploadImageBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + ((App) getApplication()).getImageComponent().inject(this); + + accountManager = AccountManager.get(this); + + pickImageLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + Uri selectedImageUri = result.getData().getData(); + binding.imagePreview.setImageURI(selectedImageUri); + binding.btnUploadImage.setEnabled(true); + this.selectedImageUri = selectedImageUri; + } + } + ); + + binding.btnSelectImage.setOnClickListener(v -> selectImage()); + binding.btnUploadImage.setOnClickListener(v -> uploadImage(this, selectedImageUri)); + } + + private void selectImage() { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + pickImageLauncher.launch(intent); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK && data != null) { + selectedImageUri = data.getData(); + binding.imagePreview.setImageURI(selectedImageUri); + binding.btnUploadImage.setEnabled(true); + } + } + + public void uploadImage(Context context, Uri uri) { + MultipartBody.Part imagePart; + try { + imagePart = createImagePartFromUri(context, uri); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(context, "Ошибка при обработке изображения", Toast.LENGTH_SHORT).show(); + return; + } + + String auth = AuthHelper.getAuthHeader(accountManager); + // Пример вызова API + imageService.uploadImage(auth, getUploadUrl(), imagePart).enqueue( + new HttpErrorLogger<>(this) { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + try { + String imagePath = response.body().string(); + handleUploadedImagePath(imagePath); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(context, "Ошибка при чтении ответа", Toast.LENGTH_SHORT).show(); + } + } + } + }); + } + + private void handleUploadedImagePath(String imagePath) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(PHOTO_PATH_RESULT, imagePath); + setResult(Activity.RESULT_OK, resultIntent); + finish(); + } + + public static MultipartBody.Part createImagePartFromUri(Context context, Uri uri) throws IOException { + ContentResolver contentResolver = context.getContentResolver(); + + // 1. Получаем MIME-тип + String mimeType = contentResolver.getType(uri); + if (mimeType == null) { + mimeType = "image/jpeg"; // по умолчанию + } + + // 2. Получаем расширение из MIME-типа + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extension == null) { + extension = "jpg"; // fallback + } + + // 3. Получаем имя файла + String fileName = getFileName(context, uri); + if (fileName == null || !fileName.contains(".")) { + fileName = "upload." + extension; + } + + // 4. Чтение содержимого файла + InputStream inputStream = contentResolver.openInputStream(uri); + if (inputStream == null) throw new IOException("Failed to open input stream"); + + byte[] bytes = readBytes(inputStream); + + // 5. Создаём RequestBody и MultipartBody.Part + RequestBody requestBody = RequestBody.create(bytes, MediaType.parse(mimeType)); + return MultipartBody.Part.createFormData("file", fileName, requestBody); + } + + public static String getFileName(Context context, Uri uri) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (cursor.moveToFirst()) { + String name = cursor.getString(nameIndex); + cursor.close(); + return name; + } + cursor.close(); + } + return "file.jpg"; + } + + public static byte[] readBytes(InputStream inputStream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[8192]; // 8 KB буфер + int nRead; + while ((nRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + return buffer.toByteArray(); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadEventImageActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadEventImageActivity.java new file mode 100644 index 0000000..9844fd5 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadEventImageActivity.java @@ -0,0 +1,8 @@ +package com.tom.meeter.context.image.activity; + +public class UploadEventImageActivity extends BaseUploadActivity { + @Override + protected String getUploadUrl() { + return "images/event"; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadUserImageActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadUserImageActivity.java new file mode 100644 index 0000000..28a9523 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/image/activity/UploadUserImageActivity.java @@ -0,0 +1,8 @@ +package com.tom.meeter.context.image.activity; + +public class UploadUserImageActivity extends BaseUploadActivity { + @Override + protected String getUploadUrl() { + return "images/user"; + } +} 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 index 03964a3..eed3419 100644 --- 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 @@ -2,11 +2,16 @@ import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; +import okhttp3.MultipartBody; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; import retrofit2.http.Path; +import retrofit2.http.Url; public interface ImageService { @@ -19,4 +24,20 @@ Call downloadEventImage( Call downloadUserImage( @Header(AUTH_HEADER) String authHeader, @Path(value = "imagePath", encoded = true) String imagePath); + + @Multipart + @POST + Call uploadImage( + @Header(AUTH_HEADER) String authHeader, + @Url String url, @Part MultipartBody.Part file); + + @Multipart + @POST("/images/user") + Call uploadUserImage( + @Header(AUTH_HEADER) String authHeader, @Part MultipartBody.Part file); + + @Multipart + @POST("/images/event") + Call uploadEventImage(@Url String url, @Part MultipartBody.Part file); + } 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 65cb544..dd71a17 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,6 +1,8 @@ package com.tom.meeter.context.network.dto; -import androidx.annotation.Nullable; +import static com.tom.meeter.infrastructure.common.JsonHelper.getFloatOrNull; +import static com.tom.meeter.infrastructure.common.JsonHelper.getOffsetDateTimeOrNull; +import static com.tom.meeter.infrastructure.common.JsonHelper.getStringOrNull; import com.fasterxml.jackson.annotation.JsonProperty; @@ -27,14 +29,19 @@ public class EventDTO { private static final String PHOTO_PATH_KEY = "photo_path"; private static final String CITY_KEY = "city"; + //Non nullable, cannot be changed private String id; - private String name; - private String description; - private Double latitude; - private Double longitude; @JsonProperty(value = CREATOR_ID_KEY) private String creatorId; private OffsetDateTime created; + + //Non nullable, can be changed + private String name; + + //Nullable + private String description; + private Float latitude; + private Float longitude; private OffsetDateTime starting; private OffsetDateTime ending; private String city; @@ -52,8 +59,8 @@ public static EventDTO encode(JSONObject json) { //Nullable. result.description = getStringOrNull(DESCRIPTION_KEY, json); - result.latitude = getDoubleOrNull(LATITUDE_KEY, json); - result.longitude = getDoubleOrNull(LONGITUDE_KEY, json); + result.latitude = getFloatOrNull(LATITUDE_KEY, json); + result.longitude = getFloatOrNull(LONGITUDE_KEY, json); result.starting = getOffsetDateTimeOrNull(STARTING_KEY, json); result.ending = getOffsetDateTimeOrNull(ENDING_KEY, json); result.photoPath = getStringOrNull(PHOTO_PATH_KEY, json); @@ -68,11 +75,11 @@ public void setName(String name) { this.name = name; } - public void setLatitude(Double latitude) { + public void setLatitude(Float latitude) { this.latitude = latitude; } - public void setLongitude(Double longitude) { + public void setLongitude(Float longitude) { this.longitude = longitude; } @@ -120,11 +127,11 @@ public String getDescription() { return description; } - public Double getLatitude() { + public Float getLatitude() { return latitude; } - public Double getLongitude() { + public Float getLongitude() { return longitude; } @@ -175,20 +182,4 @@ public int hashCode() { 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 index 695772a..01ab39c 100644 --- 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 @@ -1,6 +1,111 @@ package com.tom.meeter.context.network.dto; +import static com.tom.meeter.infrastructure.common.JsonHelper.getLocalDateOrNull; +import static com.tom.meeter.infrastructure.common.JsonHelper.getStringOrNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.time.LocalDate; -//TODO: make me public class UserDTO { + + private static final String USER_ID_KEY = "id"; + private static final String NAME_KEY = "name"; + private static final String GENDER_KEY = "gender"; + private static final String SURNAME_KEY = "surname"; + private static final String INFO_KEY = "info"; + private static final String BIRTHDAY_KEY = "birthday"; + private static final String PHOTO_PATH_KEY = "photo_path"; + + private String id; + private String name; + private UserGender gender; + private String surname; + private String info; + private LocalDate birthday; + @JsonProperty(PHOTO_PATH_KEY) + private String photoPath; + + public UserDTO() { + } + + public static UserDTO encode(JSONObject json) { + UserDTO result = new UserDTO(); + try { + //Non nullable. + result.id = json.getString(USER_ID_KEY); + result.name = json.getString(NAME_KEY); + result.gender = UserGender.fromString(json.getString(GENDER_KEY)); + + //Nullable. + result.surname = getStringOrNull(SURNAME_KEY, json); + result.info = getStringOrNull(INFO_KEY, json); + result.birthday = getLocalDateOrNull(BIRTHDAY_KEY, json); + result.photoPath = getStringOrNull(PHOTO_PATH_KEY, json); + } catch (JSONException e) { + throw new RuntimeException("Unable to encode UserDTO from jsonObject: ", e); + } + return result; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public UserGender getGender() { + return gender; + } + + public String getSurname() { + return surname; + } + + public String getInfo() { + return info; + } + + public LocalDate getBirthday() { + return birthday; + } + + public String getPhotoPath() { + return photoPath; + } + + private static final String MALE_VALUE = "male"; + private static final String FEMALE_VALUE = "female"; + + public enum UserGender { + + @JsonProperty(MALE_VALUE) + MALE(MALE_VALUE), + @JsonProperty(FEMALE_VALUE) + FEMALE(FEMALE_VALUE); + + private final String value; + + UserGender(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static UserGender fromString(String text) { + for (UserGender val : UserGender.values()) { + if (val.value.equalsIgnoreCase(text)) { + return val; + } + } + throw new IllegalArgumentException("No enum constant with string value " + text); + } + } } 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 e13b3c5..8cd3cb2 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 @@ -2,7 +2,9 @@ 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.network.utils.SocketIOCodes.NEW_SUBSCRIBER_CODE; import static com.tom.meeter.context.notification.NotificationHelper.sendNotificationEventCreated; +import static com.tom.meeter.context.notification.NotificationHelper.sendNotificationNewSubscriber; 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; @@ -20,6 +22,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.context.network.dto.UserDTO; import com.tom.meeter.infrastructure.common.Globals; import com.tom.meeter.infrastructure.common.JsonHelper; import com.tom.meeter.infrastructure.eventbus.events.FailureEventCreation; @@ -53,12 +56,16 @@ public class SocketIOService extends Service { 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 NEW_SUBSCRIBER_CHANNEL = "user:subscription:new"; private static final String CODE_KEY = "code"; private static final String ID_KEY = "id"; private static final int CREATED_CODE = 201; private static final int BAD_REQUEST = 400; private static final String UNAUTHORIZED = "401"; + private static final String MESSAGE_KEY = "message"; + private static final String USER_KEY = "user"; + private static final String EVENT_KEY = "event"; private AccountManager accountManager; public class ServiceBinder extends Binder { @@ -178,6 +185,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(NEW_SUBSCRIBER_CHANNEL, this::newSubscriberNotificationsChannel); socketClient.on(EVENTS_CREATE_CHANNEL, SocketIOService::eventsCreateHandler); socketClient.connect(); EventBus.getDefault().register(this); @@ -211,6 +219,7 @@ private void disconnect() { socketClient.off(GREETINGS_CHANNEL, SocketIOService::greetingsHandler); socketClient.off(EVENTS_SEARCH_CHANNEL, SocketIOService::eventsSearchHandler); socketClient.off(EVENTS_NOTIFICATIONS_CHANNEL, this::eventsNotificationsChannel); + socketClient.off(NEW_SUBSCRIBER_CHANNEL, this::newSubscriberNotificationsChannel); socketClient.off(EVENTS_CREATE_CHANNEL, SocketIOService::eventsCreateHandler); initialized = false; } @@ -264,9 +273,26 @@ 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); + if (response.getInt(CODE_KEY) == EVENT_CREATED_CODE) { + JSONObject message = response.getJSONObject(MESSAGE_KEY); + sendNotificationEventCreated( + this, + UserDTO.encode(message.getJSONObject(USER_KEY)), + EventDTO.encode(message.getJSONObject(EVENT_KEY))); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private void newSubscriberNotificationsChannel(Object... args) { + JSONObject response = getSimpleResponse(JSONObject.class, args); + Log.d(TAG, NEW_SUBSCRIBER_CHANNEL + " : " + response); + try { + if (response.getInt(CODE_KEY) == NEW_SUBSCRIBER_CODE) { + sendNotificationNewSubscriber( + this, + UserDTO.encode(response.getJSONObject(MESSAGE_KEY))); } } catch (JSONException e) { throw new RuntimeException(e); 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 index 733029f..f764552 100644 --- 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 @@ -3,6 +3,7 @@ public final class SocketIOCodes { public static int EVENT_CREATED_CODE = 100; + public static int NEW_SUBSCRIBER_CODE = 200; 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 index def249c..0a797e6 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/notification/NotificationHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/notification/NotificationHelper.java @@ -10,13 +10,16 @@ import android.content.pm.PackageManager; import android.os.Build; +import androidx.annotation.RequiresPermission; 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.event.activity.EventDispatcherActivity; import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.user.activity.UserActivity; public class NotificationHelper { @@ -38,7 +41,8 @@ public static void createNotificationChannel(Context ctx) { } } - public static void sendNotificationEventCreated(Context ctx, EventDTO event) { + public static void sendNotificationEventCreated( + Context ctx, UserDTO user, EventDTO event) { if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions @@ -50,18 +54,61 @@ public static void sendNotificationEventCreated(Context ctx, EventDTO event) { return; } NotificationManagerCompat mgr = NotificationManagerCompat.from(ctx); - mgr.notify(event.getId().hashCode(), getNotification(ctx, event)); + mgr.notify(event.getId().hashCode(), getNotificationEventCreated(ctx, user, event)); + notifySummary(ctx, mgr); + } + + @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) + private static void notifySummary(Context ctx, NotificationManagerCompat mgr) { mgr.notify(SUMMARY_ID, getSummaryNotification(ctx)); } - private static Notification getNotification(Context ctx, EventDTO event) { + public static void sendNotificationNewSubscriber(Context ctx, UserDTO user) { + 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(user.getId().hashCode(), getNotificationNewSubscriber(ctx, user)); + notifySummary(ctx, mgr); + } + + private static Notification getNotificationEventCreated( + Context ctx, UserDTO user, 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()) + .setContentTitle( + ctx.getString(R.string.notification_new_event) + ": " + event.getName() + " !") + .setContentText( + user.getName() + " " + user.getSurname() + " " + + ctx.getString(R.string.created_event)) .setStyle(getBigStyle(event.getDescription())) - .setContentIntent(createPendingIntent(ctx, event)) + .setContentIntent(createEventPendingIntent(ctx, event)) + .setGroup(ALL_EVENTS_GROUP) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .build(); + } + + private static Notification getNotificationNewSubscriber( + Context ctx, UserDTO user) { + return new NotificationCompat.Builder(ctx, EVENTS_NOTIFY) + .setSmallIcon(R.drawable.ic_meeter_lr) + .setContentTitle(ctx.getString(R.string.new_subscriber)) + .setContentText( + user.getName() + " " + user.getSurname() + " " + + ctx.getString(R.string.subscribed)) + .setStyle(getBigStyle( + user.getName() + " " + + user.getSurname() + ". " + + user.getInfo())) + .setContentIntent(createUserPendingIntent(ctx, user)) .setGroup(ALL_EVENTS_GROUP) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) @@ -80,12 +127,21 @@ 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()); + private static PendingIntent createEventPendingIntent(Context ctx, EventDTO event) { + Intent intent = EventDispatcherActivity.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); } + + private static PendingIntent createUserPendingIntent(Context ctx, UserDTO user) { + Intent intent = UserActivity.createUserActivityIntent(ctx, user.getId()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + return PendingIntent.getActivity( + ctx, user.getId().hashCode(), intent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + } } 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 bdd37cd..9066c2f 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,8 +26,8 @@ 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.ErrorLogger; import com.tom.meeter.infrastructure.http.HttpCodes; +import com.tom.meeter.infrastructure.http.HttpErrorLogger; import javax.inject.Inject; @@ -112,9 +112,10 @@ private void sendSavePrefs(int searchArea, boolean trackUser) { settingsService.createOrUpdateSettings( new SettingsCreateOrUpdate(searchArea, trackUser), Globals.getAuthHeader(AuthHelper.peekToken(accountManager))) - .enqueue(new ErrorLogger<>(this) { + .enqueue(new HttpErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { + super.onResponse(call, res); if (res.code() == HttpCodes.NOT_AUTHENTICATED) { invalidateToken( accountManager, SettingsActivity.this, @@ -123,9 +124,11 @@ public void onResponse(Call call, Response r Log.d(TAG, "SettingsActivity: canceled auth."); startActivity(new Intent(SettingsActivity.this, Launcher.class)); }); + return; } if (res.code() == HttpCodes.OK || res.code() == HttpCodes.CREATED) { Log.d(TAG, "SettingsActivity: created/updated server settings."); + return; } } }); @@ -135,14 +138,14 @@ private void sendSavePrefsRetry(String token, int searchArea, boolean trackUser) settingsService.createOrUpdateSettings( new SettingsCreateOrUpdate(searchArea, trackUser), Globals.getAuthHeader(token)) - .enqueue(new ErrorLogger<>(this) { + .enqueue(new HttpErrorLogger<>(this) { @Override public void onResponse(Call call, Response res) { + super.onResponse(call, res); if (res.code() == HttpCodes.OK || res.code() == HttpCodes.CREATED) { Log.d(TAG, "SettingsActivity: created/updated server settings on retry."); return; } - Log.d(TAG, "SettingsActivity: failed retry request. " + res.body()); } }); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscribersActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscribersActivity.java new file mode 100644 index 0000000..69d4949 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscribersActivity.java @@ -0,0 +1,163 @@ +package com.tom.meeter.context.profile.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.user.activity.UserActivity.dispatchToUserActivity; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.os.Bundle; +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.adapter.SubscribersAdapter; +import com.tom.meeter.context.profile.service.ProfileService; +import com.tom.meeter.context.profile.subscriber.Subscriber; +import com.tom.meeter.context.profile.viewmodel.ProfileSubscribersViewModel; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.context.user.service.UserService; +import com.tom.meeter.databinding.ActivityProfileSubscribersBinding; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheSubscriberBinder; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class SubscribersActivity extends AppCompatActivity { + + private static final String TAG = SubscribersActivity.class.getCanonicalName(); + + @Inject + ProfileService profileService; + @Inject + TokenService tokenService; + + ActivityProfileSubscribersBinding binding; + + private AccountManager accountManager; + + @Inject + ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + @Inject + UserService userService; + + private SubscribersAdapter adapter; + + private ProfileSubscribersViewModel profileSubscribersViewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + ((App) getApplication()).getComponent().inject(this); + accountManager = AccountManager.get(this); + + checkToken( + this::onInit, this::finish, + accountManager, this, tokenService); + } + + private void onInit(String token) { + logMethod(TAG, this); + + binding = ActivityProfileSubscribersBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + adapter = new SubscribersAdapter( + new PhotoDownloaderWithCacheSubscriberBinder( + this, imgDownloader, + (user) -> dispatchToUserActivity(this, user.getId()), + this::onSubUnsubClick, + this::recreate + )); + + profileSubscribersViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(ProfileSubscribersViewModel.class); + profileSubscribersViewModel.fetchProfileSubscribers(getAuthHeader(accountManager), this); + + profileSubscribersViewModel.getSubscribersLiveData() + .observe(this, subs -> adapter.setData(subs)); + binding.recyclerSubscribers.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerSubscribers.setAdapter(adapter); + } + + private void onSubUnsubClick(Subscriber sub, int position) { + if (sub.isAmISubscribedTo()) { + userService.unsubscribe(AuthHelper.getAuthHeader(accountManager), sub.getUser().getId()) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + sub.setAmISubscribedTo(false); + adapter.notifyItemChanged(position); + } + } + }); + } else { + userService.subscribe(AuthHelper.getAuthHeader(accountManager), sub.getUser().getId()) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + sub.setAmISubscribedTo(true); + adapter.notifyItemChanged(position); + } + } + }); + } + return; + } + + @Override + protected void onStart() { + logMethod(TAG, this); + super.onStart(); + } + + @Override + protected void onStop() { + logMethod(TAG, this); + super.onStop(); + } + + @Override + protected void onDestroy() { + logMethod(TAG, this); + super.onDestroy(); + } + + @Override + protected void onPause() { + logMethod(TAG, this); + super.onPause(); + } + + @Override + protected void onResume() { + logMethod(TAG, this); + super.onResume(); + } + + @Override + protected void onRestart() { + logMethod(TAG, this); + super.onRestart(); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscriptionsActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscriptionsActivity.java new file mode 100644 index 0000000..c11c7f9 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/activity/SubscriptionsActivity.java @@ -0,0 +1,154 @@ +package com.tom.meeter.context.profile.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.user.activity.UserActivity.dispatchToUserActivity; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.os.Bundle; +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.tom.meeter.App; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.adapter.SubscribersAdapter; +import com.tom.meeter.context.profile.subscriber.Subscriber; +import com.tom.meeter.context.profile.viewmodel.ProfileSubscriptionsViewModel; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.context.user.service.UserService; +import com.tom.meeter.databinding.ActivityProfileSubscriptionsBinding; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheSubscriberBinder; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class SubscriptionsActivity extends AppCompatActivity { + + private static final String TAG = SubscribersActivity.class.getCanonicalName(); + + @Inject + TokenService tokenService; + @Inject + ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + @Inject + UserService userService; + + private ActivityProfileSubscriptionsBinding binding; + + private AccountManager accountManager; + + private SubscribersAdapter adapter; + + private ProfileSubscriptionsViewModel profileSubscriptionsViewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + ((App) getApplication()).getComponent().inject(this); + accountManager = AccountManager.get(this); + + checkToken( + this::onInit, this::finish, + accountManager, this, tokenService); + } + + private void onInit(String token) { + logMethod(TAG, this); + + binding = ActivityProfileSubscriptionsBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + adapter = new SubscribersAdapter( + new PhotoDownloaderWithCacheSubscriberBinder( + this, imgDownloader, + (e) -> dispatchToUserActivity(this, e.getId()), + this::onSubUnsubClick, + this::recreate + )); + + profileSubscriptionsViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(ProfileSubscriptionsViewModel.class); + profileSubscriptionsViewModel.fetchProfileSubscriptions(getAuthHeader(accountManager), this); + + profileSubscriptionsViewModel.getSubscriptionsLiveData() + .observe(this, subs -> adapter.setData(subs)); + binding.recyclerSubscriptions.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerSubscriptions.setAdapter(adapter); + } + + private void onSubUnsubClick(Subscriber sub, int position) { + if (sub.isAmISubscribedTo()) { + userService.unsubscribe(AuthHelper.getAuthHeader(accountManager), sub.getUser().getId()) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response response) { + super.onResponse(call, response); + sub.setAmISubscribedTo(false); + adapter.notifyItemChanged(position); + } + }); + } else { + userService.subscribe(AuthHelper.getAuthHeader(accountManager), sub.getUser().getId()) + .enqueue(new ActivityRecreatorOnAuthFailure<>(this) { + @Override + public void onResponse(Call call, Response response) { + super.onResponse(call, response); + sub.setAmISubscribedTo(true); + adapter.notifyItemChanged(position); + } + }); + } + return; + } + + @Override + protected void onStart() { + logMethod(TAG, this); + super.onStart(); + } + + @Override + protected void onStop() { + logMethod(TAG, this); + super.onStop(); + } + + @Override + protected void onDestroy() { + logMethod(TAG, this); + super.onDestroy(); + } + + @Override + protected void onPause() { + logMethod(TAG, this); + super.onPause(); + } + + @Override + protected void onResume() { + logMethod(TAG, this); + super.onResume(); + } + + @Override + protected void onRestart() { + logMethod(TAG, this); + super.onRestart(); + } +} 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 index 9c563b8..285f994 100644 --- 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 @@ -7,7 +7,7 @@ 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.binder.EventBinder; import com.tom.meeter.infrastructure.components.viewholder.EventViewHolder; /** @@ -17,7 +17,7 @@ public class ActiveEventsAdapter extends BaseEventAdapter { private static final String TAG = ActiveEventsAdapter.class.getCanonicalName(); - public ActiveEventsAdapter(ViewHolderEventBinder binder) { + public ActiveEventsAdapter(EventBinder binder) { super(binder); } 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 index 444dc25..20d7108 100644 --- 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 @@ -9,7 +9,7 @@ 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.binder.EventBinder; import com.tom.meeter.infrastructure.components.viewholder.EventViewHolder; /** @@ -19,7 +19,7 @@ public class EventsAdapter extends BaseEventAdapter { private static final String TAG = EventsAdapter.class.getCanonicalName(); - public EventsAdapter(ViewHolderEventBinder binder) { + public EventsAdapter(EventBinder binder) { super(binder); logMethod(TAG, this); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/SubscribersAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/SubscribersAdapter.java new file mode 100644 index 0000000..7f2fc23 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/adapter/SubscribersAdapter.java @@ -0,0 +1,37 @@ +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.ActivityProfileSubscriberItemBinding; +import com.tom.meeter.infrastructure.components.adapter.BaseSubscriberAdapter; +import com.tom.meeter.infrastructure.components.binder.SubscriberBinder; +import com.tom.meeter.infrastructure.components.viewholder.SubscriberViewHolder; + +public class SubscribersAdapter extends BaseSubscriberAdapter { + + private static final String TAG = SubscribersAdapter.class.getCanonicalName(); + + public SubscribersAdapter(SubscriberBinder binder) { + super(binder); + logMethod(TAG, this); + } + + @Override + public SubscriberViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + logMethod(TAG, this); + return new SubscriberViewHolder( + ActivityProfileSubscriberItemBinding.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/domain/GMapEvent.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/domain/GMapEvent.java index 75cee25..7cde618 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 @@ -58,7 +58,7 @@ public void updateName(String name) { marker.setTitle(name); } - public void updatePosition(double latitude, double longitude) { + public void updatePosition(Float latitude, Float longitude) { event.setLatitude(latitude); event.setLongitude(longitude); marker.setPosition(new LatLng(latitude, longitude)); 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 40bc7a6..3a1e42a 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,7 +4,7 @@ * Created by Tom on 09.12.2016. */ -import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; +import static com.tom.meeter.context.event.activity.EventDispatcherActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.content.Context; @@ -24,7 +24,7 @@ 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.components.binder.PhotoDownloaderWithCacheEventBinder; import com.tom.meeter.infrastructure.eventbus.events.IncomeEvents; import org.greenrobot.eventbus.EventBus; @@ -59,7 +59,7 @@ public void onCreate(Bundle savedInstanceState) { Context ctx = getContext(); adapter = new EventsAdapter( - new PhotoDownloaderWithCacheEventEventBinder( + new PhotoDownloaderWithCacheEventBinder( ctx, imageDownloader, (e) -> dispatchToEventActivity(ctx, e.getId()), () -> InfrastructureHelper.restartActivityFromFragment(this))); 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 f74e018..058ecda 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,7 +1,7 @@ package com.tom.meeter.context.profile.fragment; import static android.content.Context.BIND_AUTO_CREATE; -import static com.tom.meeter.context.event.activity.EventActivity.dispatchToEventActivity; +import static com.tom.meeter.context.event.activity.EventDispatcherActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.ImagesHelper.circleImage; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java index 3f3ee8e..a3ad1f6 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/fragment/ProfileEventsFragment.java @@ -1,7 +1,7 @@ package com.tom.meeter.context.profile.fragment; 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.context.event.activity.EventDispatcherActivity.dispatchToEventActivity; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.accounts.AccountManager; @@ -23,7 +23,7 @@ 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.components.binder.PhotoDownloaderWithCacheEventBinder; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; import javax.inject.Inject; @@ -60,7 +60,7 @@ public void onCreate(Bundle savedInstanceState) { accountManager = AccountManager.get(ctx); adapter = new EventsAdapter( - new PhotoDownloaderWithCacheEventEventBinder( + new PhotoDownloaderWithCacheEventBinder( ctx, imageDownloader, (e) -> dispatchToEventActivity(ctx, e.getId()), () -> InfrastructureHelper.restartActivityFromFragment(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 42016ee..bfa502b 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,33 +1,44 @@ package com.tom.meeter.context.profile.fragment; 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.context.event.activity.EventDispatcherActivity.dispatchToEventActivity; +import static com.tom.meeter.context.image.activity.BaseUploadActivity.PHOTO_PATH_RESULT; +import static com.tom.meeter.infrastructure.common.CommonHelper.EMPTY_STR; 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.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.Activity; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.GridLayoutManager; import com.tom.meeter.App; import com.tom.meeter.R; import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.image.activity.UploadUserImageActivity; +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.profile.activity.SubscribersActivity; +import com.tom.meeter.context.profile.activity.SubscriptionsActivity; 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.databinding.FragmentProfileBinding; import com.tom.meeter.infrastructure.common.InfrastructureHelper; @@ -66,13 +77,24 @@ public class ProfileFragment extends Fragment { private AccountManager accountManager; private EventsCardAdapter adapter; - private User userCache; + private UserDTO userCache; private ResponseBody photoCache; public ProfileFragment() { logMethod(TAG, this); } + private final ActivityResultLauncher imageUploadLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + String photoPath = result.getData().getStringExtra(PHOTO_PATH_RESULT); + downloadAndUpdateLayoutPhoto(photoPath); + binding.photoPath.setText(photoPath); + } + }); + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,16 +130,19 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat profileViewModel.fetchProfile(authHeader, this); + LifecycleOwner owner = getViewLifecycleOwner(); profileViewModel.getProfileLiveData() .observe( - getViewLifecycleOwner(), + owner, user -> { userCache = user; updateLayoutValues(); }); profileViewModel.getProfileEventsLiveData() - .observe(getViewLifecycleOwner(), events -> adapter.setData(events)); + .observe(owner, events -> adapter.setData(events)); + profileViewModel.getProfilePhotoLiveData() + .observe(owner, this::updateLayoutPhoto); binding.events.setLayoutManager(new GridLayoutManager(getContext(), 2)); binding.events.setAdapter(adapter); @@ -126,19 +151,22 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat if (isEditableModeEnabled) { UpdateProfileRequest req = createUpdateProfileRequest(); if (req.isEmpty()) { - showMessage(this.getActivity(), "Empty update request is not sent."); - updateLayoutValues(); + showMessage(requireActivity(), R.string.empty_update_request_is_not_sent); switchEditMode(); return; } profileService.updateProfile(authHeader, req) .enqueue(new ActivityRestarterOnAuthFailure<>(this) { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call call, Response response) { super.onResponse(call, response); - if (response.code() == HttpCodes.OK && response.body() != null) { + if (response.code() == HttpCodes.OK) { + String oldPhotoPath = userCache.getPhotoPath(); userCache = response.body(); - showMessage(ProfileFragment.this.getActivity(), "Saved"); + if (!Objects.equals(oldPhotoPath, userCache.getPhotoPath())) { + downloadAndUpdateLayoutPhoto(userCache.getPhotoPath()); + } + showMessage(ProfileFragment.this.requireActivity(), R.string.saved); } updateLayoutValues(); } @@ -146,25 +174,51 @@ public void onResponse(Call call, Response response) { } switchEditMode(); }); + binding.subscribers.setOnClickListener( + v -> startActivity( + new Intent( + ProfileFragment.this.getContext(), SubscribersActivity.class))); + binding.subscriptions.setOnClickListener( + v -> startActivity( + new Intent( + ProfileFragment.this.getContext(), SubscriptionsActivity.class))); + binding.btnPhoto.setOnClickListener( + v -> imageUploadLauncher.launch( + new Intent(requireContext(), UploadUserImageActivity.class))); } private void updateLayoutValues() { /*binding.profileId.setText(userCache.getId());*/ + binding.photoPath.setText(userCache.getPhotoPath()); 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()))); + LocalDate birthday = userCache.getBirthday(); + binding.birthday.setText(birthday == null ? EMPTY_STR : birthday.toString()); + binding.age.setText(getString(R.string.profile_age_format, getAgeFromDate(birthday))); binding.info.setText(userCache.getInfo()); } + void downloadAndUpdateLayoutPhoto(String photoPath) { + imageDownloader.downloadUserImage(photoPath, requireContext(), + this::updateLayoutPhoto, + () -> InfrastructureHelper.restartActivityFromFragment(this)); + } + + private void updateLayoutPhoto(ResponseBody photo) { + photoCache = photo; + binding.photo.setImageBitmap(circleImage(photoCache, 600, 600)); + } + private void switchEditMode() { isEditableModeEnabled = !isEditableModeEnabled; + binding.btnPhoto.setEnabled(isEditableModeEnabled); binding.name.setEnabled(isEditableModeEnabled); binding.surname.setEnabled(isEditableModeEnabled); binding.birthday.setEnabled(isEditableModeEnabled); binding.info.setEnabled(isEditableModeEnabled); - binding.btnEdit.setText(isEditableModeEnabled ? "Save" : "Edit"); + binding.btnEdit.setText( + isEditableModeEnabled ? getString(R.string.save) : getString(R.string.edit)); } private UpdateProfileRequest createUpdateProfileRequest() { @@ -179,17 +233,17 @@ private UpdateProfileRequest createUpdateProfileRequest() { req.setSurname(surnameChange); } LocalDate birthdayChange = getLocalDateOrNull(binding.birthday.getText()); - //todo userCache.getBirthday() [String -> LocalDate] - if (!Objects.equals( - userCache.getBirthday(), - birthdayChange == null ? null : birthdayChange.toString())) { + if (!Objects.equals(userCache.getBirthday(), birthdayChange)) { req.setBirthday(birthdayChange); } String infoChange = getStringOrNull(binding.info.getText()); if (!Objects.equals(userCache.getInfo(), infoChange)) { req.setInfo(infoChange); } - //TODO: userCache.getPhotoPath(); + String photoPathChange = getStringOrNull(binding.photoPath.getText()); + if (!Objects.equals(userCache.getPhotoPath(), photoPathChange)) { + req.setPhotoPath(photoPathChange); + } return req; } 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 398c56e..848899b 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,8 +3,8 @@ import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.network.dto.UserDTO; import com.tom.meeter.context.profile.message.UpdateProfileRequest; -import com.tom.meeter.context.profile.user.domain.User; import java.util.List; @@ -16,12 +16,18 @@ public interface ProfileService { @GET("/profile") - Call getProfile(@Header(AUTH_HEADER) String authHeader); + Call getProfile(@Header(AUTH_HEADER) String authHeader); @PATCH("/profile/update") - Call updateProfile( + Call updateProfile( @Header(AUTH_HEADER) String authHeader, @Body UpdateProfileRequest req); @GET("/profile/events") Call> getProfileEvents(@Header(AUTH_HEADER) String authHeader); + + @GET("/profile/subscribers") + Call> getMySubscribers(@Header(AUTH_HEADER) String authHeader); + + @GET("/profile/subscriptions") + Call> getMySubscriptions(@Header(AUTH_HEADER) String authHeader); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/subscriber/Subscriber.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/subscriber/Subscriber.java new file mode 100644 index 0000000..11ccd3e --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/subscriber/Subscriber.java @@ -0,0 +1,26 @@ +package com.tom.meeter.context.profile.subscriber; + +import com.tom.meeter.context.network.dto.UserDTO; + +public class Subscriber { + + private UserDTO user; + private boolean amISubscribedTo; + + public Subscriber(UserDTO user, boolean amISubscribedTo) { + this.user = user; + this.amISubscribedTo = amISubscribedTo; + } + + public UserDTO getUser() { + return user; + } + + public boolean isAmISubscribedTo() { + return amISubscribedTo; + } + + public void setAmISubscribedTo(boolean amISubscribedTo) { + this.amISubscribedTo = amISubscribedTo; + } +} 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 d92a6b3..48f49fa 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,11 +4,7 @@ import androidx.room.Entity; import androidx.room.PrimaryKey; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - @Entity -@JsonIgnoreProperties(ignoreUnknown = true) -//login? public class User { @PrimaryKey @@ -20,10 +16,6 @@ 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 36b8560..d0015f6 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,10 +4,12 @@ import androidx.lifecycle.LiveData; +import com.tom.meeter.context.network.dto.UserDTO; 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.time.LocalDate; import java.util.concurrent.Executor; import io.reactivex.Completable; @@ -41,9 +43,16 @@ private void refreshUser(String id) { executor.execute(() -> userDao.load(id) .flatMap(user -> Maybe.empty(), Maybe::error, () -> Maybe.just(MARKER)) .flatMapCompletable(ign -> Completable.fromAction(() -> { - Response response = userService.getUser(id).execute(); + //TODO null + String header = null; + Response response = userService.getUser(header, id).execute(); if (response.isSuccessful()) { - userDao.save(response.body()); + UserDTO body = response.body(); + LocalDate birthday = body.getBirthday(); + userDao.save(new User( + body.getId(), body.getName(), body.getGender().getValue(), + body.getSurname(), body.getInfo(), + birthday != null ? birthday.toString() : null)); } else { Log.d(TAG, "Response is not succeed."); } 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 6469205..fc2c6d5 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 @@ -2,8 +2,6 @@ import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; -import android.util.Log; - import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -45,7 +43,6 @@ public void onResponse(Call> call, Response> respo profileEventsLiveData.setValue(response.body()); return; } - Log.i(TAG, "/profile/events: " + response.code() + " : " + response.body()); } } ); diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscribersViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscribersViewModel.java new file mode 100644 index 0000000..4d8498a --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscribersViewModel.java @@ -0,0 +1,88 @@ +package com.tom.meeter.context.profile.viewmodel; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Activity; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.profile.service.ProfileService; +import com.tom.meeter.context.profile.subscriber.Subscriber; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class ProfileSubscribersViewModel extends ViewModel { + + private static final String TAG = ProfileSubscribersViewModel.class.getCanonicalName(); + + private final MutableLiveData> subscribersLiveData = new MutableLiveData<>(); + + private final ProfileService profileService; + + @Inject + public ProfileSubscribersViewModel(ProfileService profileService) { + logMethod(TAG, this); + this.profileService = profileService; + } + + public void fetchProfileSubscribers(String auth, Activity activity) { + profileService.getMySubscriptions(auth).enqueue( + new ActivityRecreatorOnAuthFailure<>(activity) { + @Override + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + getSubscribers( + auth, + activity, + resp.body() + .stream() + .collect(Collectors.toMap( + UserDTO::getId, item -> item))); + return; + } + } + } + ); + } + + private void getSubscribers( + String auth, Activity activity, Map mySubscriptions) { + profileService.getMySubscribers(auth).enqueue( + new ActivityRecreatorOnAuthFailure<>(activity) { + @Override + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + List subscribers = new ArrayList<>(); + for (UserDTO subscriber : resp.body()) { + subscribers.add( + new Subscriber( + subscriber, + mySubscriptions.get(subscriber.getId()) != null)); + } + subscribersLiveData.setValue(subscribers); + return; + } + } + } + ); + } + + public LiveData> getSubscribersLiveData() { + return subscribersLiveData; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscriptionsViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscriptionsViewModel.java new file mode 100644 index 0000000..431818b --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/profile/viewmodel/ProfileSubscriptionsViewModel.java @@ -0,0 +1,61 @@ +package com.tom.meeter.context.profile.viewmodel; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Activity; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.profile.service.ProfileService; +import com.tom.meeter.context.profile.subscriber.Subscriber; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class ProfileSubscriptionsViewModel extends ViewModel { + + private static final String TAG = ProfileSubscribersViewModel.class.getCanonicalName(); + + private final MutableLiveData> subscriptionsLiveData = new MutableLiveData<>(); + + private final ProfileService profileService; + + @Inject + public ProfileSubscriptionsViewModel(ProfileService profileService) { + logMethod(TAG, this); + this.profileService = profileService; + } + + public void fetchProfileSubscriptions(String auth, Activity activity) { + profileService.getMySubscriptions(auth).enqueue( + new ActivityRecreatorOnAuthFailure<>(activity) { + @Override + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + List subscribers = new ArrayList<>(); + for (UserDTO subscription : resp.body()) { + subscribers.add(new Subscriber(subscription, true)); + } + subscriptionsLiveData.setValue(subscribers); + return; + } + } + } + ); + } + + public LiveData> getSubscriptionsLiveData() { + return subscriptionsLiveData; + } +} 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 3be0d01..e9de605 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 @@ -2,16 +2,16 @@ import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; -import android.util.Log; - import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.tom.meeter.context.image.ImageDownloader; import com.tom.meeter.context.network.dto.EventDTO; +import com.tom.meeter.context.network.dto.UserDTO; import com.tom.meeter.context.profile.service.ProfileService; -import com.tom.meeter.context.profile.user.domain.User; +import com.tom.meeter.infrastructure.common.InfrastructureHelper; import com.tom.meeter.infrastructure.http.ActivityRestarterOnAuthFailure; import com.tom.meeter.infrastructure.http.HttpCodes; @@ -19,6 +19,7 @@ import javax.inject.Inject; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Response; @@ -26,28 +27,40 @@ public class ProfileViewModel extends ViewModel { private static final String TAG = ProfileViewModel.class.getCanonicalName(); - private final MutableLiveData profileLiveData = new MutableLiveData<>(); + private final MutableLiveData profileLiveData = new MutableLiveData<>(); + private final MutableLiveData profilePhotoLiveData = new MutableLiveData<>(); private final MutableLiveData> profileEventsLiveData = new MutableLiveData<>(); private final ProfileService profileService; + private final ImageDownloader imageDownloader; @Inject - public ProfileViewModel(ProfileService profileService) { + public ProfileViewModel( + ProfileService profileService, ImageDownloader imageDownloader) { logMethod(TAG, this); this.profileService = profileService; + this.imageDownloader = imageDownloader; } public void fetchProfile(String auth, Fragment fragment) { profileService.getProfile(auth).enqueue( new ActivityRestarterOnAuthFailure<>(fragment) { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call call, Response response) { super.onResponse(call, response); - if (response.code() == HttpCodes.OK && response.body() != null) { - profileLiveData.setValue(response.body()); + if (response.code() == HttpCodes.OK) { + UserDTO user = response.body(); + profileLiveData.setValue(user); + String photoPath = user.getPhotoPath(); + if (photoPath == null) { + return; + } + imageDownloader.downloadUserImage( + photoPath, fragment.getContext(), + profilePhotoLiveData::setValue, + () -> InfrastructureHelper.restartActivityFromFragment(fragment)); return; } - Log.i(TAG, "/profile: " + response.code() + " : " + response.body()); } } ); @@ -60,7 +73,6 @@ public void onResponse(Call> call, Response> respo profileEventsLiveData.setValue(response.body()); return; } - Log.i(TAG, "/profile/events: " + response.code() + " : " + response.body()); } } ); @@ -72,11 +84,15 @@ protected void onCleared() { super.onCleared(); } - public LiveData getProfileLiveData() { + public LiveData getProfileLiveData() { return profileLiveData; } public LiveData> getProfileEventsLiveData() { return profileEventsLiveData; } + + public LiveData getProfilePhotoLiveData() { + return profilePhotoLiveData; + } } 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 index 3b8c309..43dfa64 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/context/user/UserComponent.java +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/UserComponent.java @@ -5,6 +5,9 @@ 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.activity.UserSubscribersActivity; +import com.tom.meeter.context.user.activity.UserSubscriptionsActivity; +import com.tom.meeter.context.user.service.UserService; import com.tom.meeter.context.user.viewmodel.UserViewModelModule; import dagger.BindsInstance; @@ -22,6 +25,8 @@ }) public interface UserComponent { + UserService provideUserService(); + @Component.Builder interface Builder { @BindsInstance @@ -35,4 +40,8 @@ interface Builder { } void inject(UserActivity userActivity); + + void inject(UserSubscribersActivity usa); + + void inject(UserSubscriptionsActivity usa); } 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 45cee42..77c2a17 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,13 @@ 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.context.event.activity.EventDispatcherActivity.dispatchToEventActivity; +import static com.tom.meeter.context.user.activity.UserSubscribersActivity.dispatchToUserSubscribersActivity; +import static com.tom.meeter.context.user.activity.UserSubscriptionsActivity.dispatchToUserSubscriptionsActivity; +import static com.tom.meeter.infrastructure.common.CommonHelper.EMPTY_STR; 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.ImagesHelper.circleImage; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import static com.tom.meeter.infrastructure.common.InfrastructureHelper.showMessage; @@ -23,7 +27,9 @@ import com.tom.meeter.App; import com.tom.meeter.R; +import com.tom.meeter.context.auth.infrastructure.AuthHelper; import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.profile.activity.ProfileActivity; import com.tom.meeter.context.token.service.TokenService; import com.tom.meeter.context.user.service.UserService; import com.tom.meeter.context.user.viewmodel.UserViewModel; @@ -35,6 +41,8 @@ import com.tom.meeter.infrastructure.http.HttpErrorLogger; import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; +import java.time.LocalDate; + import javax.inject.Inject; import retrofit2.Call; @@ -43,7 +51,7 @@ public class UserActivity extends AppCompatActivity { private static final String TAG = UserActivity.class.getCanonicalName(); - private static final String USER_ID_KEY = "user_id"; + public static final String USER_ID_KEY = "user_id"; @Inject TokenService tokenService; @@ -53,6 +61,7 @@ public class UserActivity extends AppCompatActivity { ViewModelFactory viewModelFactory; @Inject ImageDownloader imgDownloader; + private ActivityUserBinding binding; private UserViewModel userViewModel; private String userId; @@ -66,29 +75,35 @@ protected void onCreate(Bundle savedInstanceState) { logMethod(TAG, this); + validate(); + + ((App) getApplication()).getUserComponent().inject(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 validate() { Bundle extras = getIntent().getExtras(); if (extras == null) { Log.d(TAG, "Unable to create user activity without extras."); finish(); - return; } userId = extras.getString(USER_ID_KEY); if (userId == null) { Log.d(TAG, "Unable to create user activity without 'user_id' provided."); finish(); - return; } - - ((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); + if (userId.equals(AuthHelper.getUserUuid(accountManager))) { + startActivity(new Intent(this, ProfileActivity.class)); + finish(); + } } private void onInit(String token) { @@ -108,9 +123,8 @@ private void onInit(String token) { public void onResponse(Call call, Response resp) { super.onResponse(call, resp); if (resp.code() == HttpCodes.OK) { - amISubscriber = false; - updateSubscribeButtonText(); - showMessage(UserActivity.this, "Successfully unsubscribed."); + updateAmISubscriber(false); + showMessage(UserActivity.this, R.string.successfully_unsubscribed); return; } if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { @@ -125,9 +139,8 @@ public void onResponse(Call call, Response resp) { public void onResponse(Call call, Response resp) { super.onResponse(call, resp); if (resp.code() == HttpCodes.OK) { - amISubscriber = true; - updateSubscribeButtonText(); - showMessage(UserActivity.this, "Successfully subscribed."); + updateAmISubscriber(true); + showMessage(UserActivity.this, R.string.successfully_subscribed); return; } if (resp.code() == HttpCodes.NOT_AUTHENTICATED) { @@ -144,34 +157,35 @@ public void onResponse(Call call, Response resp) { userViewModel.getUserLiveData() .observe(this, user -> { 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.surname.setText(user.getSurname()); + LocalDate birthday = user.getBirthday(); + binding.birthday.setText(birthday == null ? EMPTY_STR : birthday.toString()); + binding.age.setText(getString(R.string.profile_age_format, getAgeFromDate(birthday))); binding.info.setText(user.getInfo()); - //binding.userPhoto.setImageBitmap(); }); userViewModel.getAmISubscriber() - .observe(this, val -> { - amISubscriber = val; - updateSubscribeButtonText(); - }); + .observe(this, this::updateAmISubscriber); userViewModel.getUserEventsLiveData() .observe(this, events -> adapter.setData(events)); + userViewModel.getUserPhotoLiveData() + .observe( + this, + photo -> binding.photo.setImageBitmap(circleImage(photo, 600, 600))); binding.events.setLayoutManager(new GridLayoutManager(this, 2)); binding.events.setAdapter(adapter); + + binding.subscribers.setOnClickListener( + v -> dispatchToUserSubscribersActivity(this, userId)); + binding.subscriptions.setOnClickListener( + v -> dispatchToUserSubscriptionsActivity(this, userId)); } - private void updateSubscribeButtonText() { - if (amISubscriber == null) { - binding.subscribeBtn.setText("..."); - } - if (amISubscriber) { - binding.subscribeBtn.setText("Unsubscribe"); - } else { - binding.subscribeBtn.setText("Subscribe"); - } + private void updateAmISubscriber(boolean value) { + amISubscriber = value; + binding.subscribeBtn.setText(amISubscriber ? R.string.unsubscribe : R.string.subscribe); } @Nullable @@ -186,7 +200,7 @@ public static void dispatchToUserActivity(Context ctx, String userId) { ctx.startActivity(createUserActivityIntent(ctx, userId)); } - private static Intent createUserActivityIntent(Context ctx, String userId) { + public 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/activity/UserSubscribersActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserSubscribersActivity.java new file mode 100644 index 0000000..38b1881 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserSubscribersActivity.java @@ -0,0 +1,150 @@ +package com.tom.meeter.context.user.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.user.activity.UserActivity.dispatchToUserActivity; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.tom.meeter.App; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.context.user.adapter.UsersAdapter; +import com.tom.meeter.context.user.viewmodel.UserSubscribersViewModel; +import com.tom.meeter.databinding.ActivityProfileSubscribersBinding; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheUserBinder; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import javax.inject.Inject; + +public class UserSubscribersActivity extends AppCompatActivity { + + private static final String TAG = UserSubscribersActivity.class.getCanonicalName(); + + @Inject + TokenService tokenService; + @Inject + ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + + private AccountManager accountManager; + private ActivityProfileSubscribersBinding binding; + private UserSubscribersViewModel userSubscribersViewModel; + private UsersAdapter adapter; + private String userId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + Log.d(TAG, "Unable to create user activity without extras."); + finish(); + return; + } + userId = extras.getString(UserActivity.USER_ID_KEY); + if (userId == null) { + Log.d(TAG, "Unable to create user activity without 'user_id' provided."); + finish(); + return; + } + + ((App) getApplication()).getUserComponent().inject(this); + accountManager = AccountManager.get(this); + + adapter = new UsersAdapter( + new PhotoDownloaderWithCacheUserBinder( + this, imgDownloader, + user -> dispatchToUserActivity(this, user.getId()), + this::recreate)); + + //setToken(accountManager, Launcher.EXPIRED); + checkToken(this::onInit, this::finish, accountManager, this, tokenService); + } + + private void onInit(String token) { + logMethod(TAG, this); + binding = ActivityProfileSubscribersBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + userSubscribersViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(UserSubscribersViewModel.class); + userSubscribersViewModel.fetchUserSubscribers(getAuthHeader(accountManager), userId, this); + + userSubscribersViewModel.getSubscribersLiveData() + .observe(this, subs -> adapter.setData(subs)); + binding.recyclerSubscribers.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerSubscribers.setAdapter(adapter); + } + + @Nullable + @Override + public View onCreateView( + @Nullable View parent, @NonNull String name, @NonNull Context ctx, + @NonNull AttributeSet attrs) { + return super.onCreateView(parent, name, ctx, attrs); + } + + @Override + protected void onStart() { + logMethod(TAG, this); + super.onStart(); + } + + @Override + protected void onStop() { + logMethod(TAG, this); + super.onStop(); + } + + @Override + protected void onDestroy() { + logMethod(TAG, this); + super.onDestroy(); + } + + @Override + protected void onPause() { + logMethod(TAG, this); + super.onPause(); + } + + @Override + protected void onResume() { + logMethod(TAG, this); + super.onResume(); + } + + @Override + protected void onRestart() { + logMethod(TAG, this); + super.onRestart(); + } + + public static void dispatchToUserSubscribersActivity(Context ctx, String userId) { + ctx.startActivity(createUserSubscribersActivityIntent(ctx, userId)); + } + + private static Intent createUserSubscribersActivityIntent(Context ctx, String userId) { + return new Intent(ctx, UserSubscribersActivity.class) + .putExtra(UserActivity.USER_ID_KEY, userId); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserSubscriptionsActivity.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserSubscriptionsActivity.java new file mode 100644 index 0000000..754d1e3 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/activity/UserSubscriptionsActivity.java @@ -0,0 +1,150 @@ +package com.tom.meeter.context.user.activity; + +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.checkToken; +import static com.tom.meeter.context.auth.infrastructure.AuthHelper.getAuthHeader; +import static com.tom.meeter.context.user.activity.UserActivity.dispatchToUserActivity; +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.tom.meeter.App; +import com.tom.meeter.context.image.ImageDownloader; +import com.tom.meeter.context.token.service.TokenService; +import com.tom.meeter.context.user.adapter.UsersAdapter; +import com.tom.meeter.context.user.viewmodel.UserSubscriptionsViewModel; +import com.tom.meeter.databinding.ActivityProfileSubscriptionsBinding; +import com.tom.meeter.infrastructure.components.binder.PhotoDownloaderWithCacheUserBinder; +import com.tom.meeter.infrastructure.injection.viewmodel.ViewModelFactory; + +import javax.inject.Inject; + +public class UserSubscriptionsActivity extends AppCompatActivity { + + private static final String TAG = UserSubscriptionsActivity.class.getCanonicalName(); + + @Inject + TokenService tokenService; + @Inject + ViewModelFactory viewModelFactory; + @Inject + ImageDownloader imgDownloader; + + private ActivityProfileSubscriptionsBinding binding; + private UserSubscriptionsViewModel userSubscriptionsViewModel; + private UsersAdapter adapter; + private AccountManager accountManager; + private String userId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + logMethod(TAG, this); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + Log.d(TAG, "Unable to create user activity without extras."); + finish(); + return; + } + userId = extras.getString(UserActivity.USER_ID_KEY); + if (userId == null) { + Log.d(TAG, "Unable to create user activity without 'user_id' provided."); + finish(); + return; + } + + ((App) getApplication()).getUserComponent().inject(this); + accountManager = AccountManager.get(this); + + adapter = new UsersAdapter( + new PhotoDownloaderWithCacheUserBinder( + this, imgDownloader, + user -> dispatchToUserActivity(this, user.getId()), + this::recreate)); + + //setToken(accountManager, Launcher.EXPIRED); + checkToken(this::onInit, this::finish, accountManager, this, tokenService); + } + + private void onInit(String token) { + logMethod(TAG, this); + binding = ActivityProfileSubscriptionsBinding.inflate(getLayoutInflater()); + View view = binding.getRoot(); + setContentView(view); + + userSubscriptionsViewModel = ViewModelProviders.of(this, viewModelFactory) + .get(UserSubscriptionsViewModel.class); + userSubscriptionsViewModel.fetchUserSubscriptions(getAuthHeader(accountManager), userId, this); + + userSubscriptionsViewModel.getSubscriptionsLiveData() + .observe(this, subs -> adapter.setData(subs)); + binding.recyclerSubscriptions.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerSubscriptions.setAdapter(adapter); + } + + @Nullable + @Override + public View onCreateView( + @Nullable View parent, @NonNull String name, @NonNull Context ctx, + @NonNull AttributeSet attrs) { + return super.onCreateView(parent, name, ctx, attrs); + } + + @Override + protected void onStart() { + logMethod(TAG, this); + super.onStart(); + } + + @Override + protected void onStop() { + logMethod(TAG, this); + super.onStop(); + } + + @Override + protected void onDestroy() { + logMethod(TAG, this); + super.onDestroy(); + } + + @Override + protected void onPause() { + logMethod(TAG, this); + super.onPause(); + } + + @Override + protected void onResume() { + logMethod(TAG, this); + super.onResume(); + } + + @Override + protected void onRestart() { + logMethod(TAG, this); + super.onRestart(); + } + + public static void dispatchToUserSubscriptionsActivity(Context ctx, String userId) { + ctx.startActivity(createUserSubscriptionsActivityIntent(ctx, userId)); + } + + private static Intent createUserSubscriptionsActivityIntent(Context ctx, String userId) { + return new Intent(ctx, UserSubscriptionsActivity.class) + .putExtra(UserActivity.USER_ID_KEY, userId); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/UsersAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/UsersAdapter.java new file mode 100644 index 0000000..b28129b --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/adapter/UsersAdapter.java @@ -0,0 +1,37 @@ +package com.tom.meeter.context.user.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.ActivityUserSubscriberItemBinding; +import com.tom.meeter.infrastructure.components.adapter.BaseUserAdapter; +import com.tom.meeter.infrastructure.components.binder.UserBinder; +import com.tom.meeter.infrastructure.components.viewholder.UserViewHolder; + +public class UsersAdapter extends BaseUserAdapter { + + private static final String TAG = UsersAdapter.class.getCanonicalName(); + + public UsersAdapter(UserBinder binder) { + super(binder); + logMethod(TAG, this); + } + + @Override + public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + logMethod(TAG, this); + return new UserViewHolder( + ActivityUserSubscriberItemBinding.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/user/service/UserService.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/service/UserService.java index 54270d5..fd15b87 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 @@ -3,7 +3,7 @@ import static com.tom.meeter.infrastructure.common.Globals.AUTH_HEADER; import com.tom.meeter.context.network.dto.EventDTO; -import com.tom.meeter.context.profile.user.domain.User; +import com.tom.meeter.context.network.dto.UserDTO; import java.util.List; @@ -13,13 +13,9 @@ import retrofit2.http.Path; public interface UserService { - //This will not even work, since server needs an auth for this requests. - @GET("/user/{id}") - @Deprecated - Call getUser(@Path("id") String userId); @GET("/user/{id}") - Call getUser(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + Call getUser(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); @GET("/user/{id}/events") Call> getUserEvents(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); @@ -32,4 +28,12 @@ public interface UserService { @GET("/user/{id}/unsubscribe") Call unsubscribe(@Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + + @GET("/user/{id}/subscribers") + Call> getSubscribers( + @Header(AUTH_HEADER) String authHeader, @Path("id") String userId); + + @GET("/user/{id}/subscriptions") + Call> getSubscriptions( + @Header(AUTH_HEADER) String authHeader, @Path("id") String userId); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscribersViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscribersViewModel.java new file mode 100644 index 0000000..c1befe8 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscribersViewModel.java @@ -0,0 +1,55 @@ +package com.tom.meeter.context.user.viewmodel; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Activity; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.user.service.UserService; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import java.util.List; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class UserSubscribersViewModel extends ViewModel { + + private static final String TAG = UserSubscribersViewModel.class.getCanonicalName(); + + private final MutableLiveData> subscribersLiveData = new MutableLiveData<>(); + + private final UserService userService; + + @Inject + public UserSubscribersViewModel(UserService userService) { + logMethod(TAG, this); + this.userService = userService; + } + + public void fetchUserSubscribers(String auth, String userId, Activity activity) { + userService.getSubscribers(auth, userId).enqueue( + new ActivityRecreatorOnAuthFailure<>(activity) { + @Override + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + subscribersLiveData.setValue(resp.body()); + return; + } + } + } + ); + } + + public LiveData> getSubscribersLiveData() { + return subscribersLiveData; + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscriptionsViewModel.java b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscriptionsViewModel.java new file mode 100644 index 0000000..b4c13b1 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/context/user/viewmodel/UserSubscriptionsViewModel.java @@ -0,0 +1,55 @@ +package com.tom.meeter.context.user.viewmodel; + +import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; + +import android.app.Activity; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.tom.meeter.context.network.dto.UserDTO; +import com.tom.meeter.context.user.service.UserService; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; +import com.tom.meeter.infrastructure.http.HttpCodes; + +import java.util.List; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Response; + +public class UserSubscriptionsViewModel extends ViewModel { + + private static final String TAG = UserSubscriptionsViewModel.class.getCanonicalName(); + + private final MutableLiveData> subscriptionsLiveData = new MutableLiveData<>(); + + private final UserService userService; + + @Inject + public UserSubscriptionsViewModel(UserService userService) { + logMethod(TAG, this); + this.userService = userService; + } + + public void fetchUserSubscriptions(String auth, String userId, Activity activity) { + userService.getSubscriptions(auth, userId).enqueue( + new ActivityRecreatorOnAuthFailure<>(activity) { + @Override + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + subscriptionsLiveData.setValue(resp.body()); + return; + } + } + } + ); + } + + public LiveData> getSubscriptionsLiveData() { + return subscriptionsLiveData; + } +} 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 8af6f68..704b17c 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 @@ -3,23 +3,24 @@ import static com.tom.meeter.infrastructure.common.InfrastructureHelper.logMethod; import android.app.Activity; -import android.util.Log; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.tom.meeter.context.image.ImageDownloader; import com.tom.meeter.context.network.dto.EventDTO; -import com.tom.meeter.context.profile.user.domain.User; +import com.tom.meeter.context.network.dto.UserDTO; import com.tom.meeter.context.user.service.UserService; import com.tom.meeter.infrastructure.common.Globals; -import com.tom.meeter.infrastructure.http.ErrorLogger; +import com.tom.meeter.infrastructure.http.ActivityRecreatorOnAuthFailure; import com.tom.meeter.infrastructure.http.HttpCodes; import java.util.List; import javax.inject.Inject; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Response; @@ -27,63 +28,67 @@ public class UserViewModel extends ViewModel { private static final String TAG = UserViewModel.class.getCanonicalName(); - private final MutableLiveData userLiveData = new MutableLiveData<>(); + private final MutableLiveData userLiveData = new MutableLiveData<>(); private final MutableLiveData amISubscriber = new MutableLiveData<>(); private final MutableLiveData> userEventsLiveData = new MutableLiveData<>(); + private final MutableLiveData userPhotoLiveData = new MutableLiveData<>(); private final UserService userService; + private final ImageDownloader imgDownloader; @Inject - public UserViewModel(UserService userService) { + public UserViewModel( + UserService userService, ImageDownloader imgDownloader) { logMethod(TAG, this); + this.imgDownloader = imgDownloader; this.userService = userService; } public void fetchUserInformation(String token, String userId, Activity activity) { userService.getUser(Globals.getAuthHeader(token), userId).enqueue( - new ErrorLogger<>(activity) { + new ActivityRecreatorOnAuthFailure<>(activity) { @Override - public void onResponse(Call call, Response response) { - if (response.code() == HttpCodes.OK && response.body() != null) { - userLiveData.setValue(response.body()); + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + UserDTO user = resp.body(); + userLiveData.setValue(user); + String photoPath = user.getPhotoPath(); + if (photoPath == null) { + return; + } + imgDownloader.downloadUserImage( + photoPath, activity.getApplicationContext(), + userPhotoLiveData::setValue, + activity::recreate); return; } - if (response.code() == HttpCodes.NOT_AUTHENTICATED) { - activity.recreate(); - } - Log.i(TAG, "/user/{id}: " + response.code() + " : " + response.body()); } } ); userService.amISubscribed(Globals.getAuthHeader(token), userId).enqueue( - new ErrorLogger<>(activity) { + new ActivityRecreatorOnAuthFailure<>(activity) { @Override - public void onResponse(Call call, Response response) { - if (response.code() == HttpCodes.OK && response.body() != null) { - amISubscriber.setValue(response.body()); + public void onResponse(Call call, Response resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + amISubscriber.setValue(resp.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 ErrorLogger<>(activity) { + new ActivityRecreatorOnAuthFailure<>(activity) { @Override - public void onResponse(Call> call, Response> response) { - if (response.code() == HttpCodes.OK && response.body() != null) { - userEventsLiveData.setValue(response.body()); + public void onResponse(Call> call, Response> resp) { + super.onResponse(call, resp); + if (resp.code() == HttpCodes.OK) { + userEventsLiveData.setValue(resp.body()); return; } - if (response.code() == HttpCodes.NOT_AUTHENTICATED) { - activity.recreate(); - } - Log.i(TAG, "/user/{id}/events: " + response.code() + " : " + response.body()); } }); } @@ -94,7 +99,7 @@ protected void onCleared() { super.onCleared(); } - public LiveData getUserLiveData() { + public LiveData getUserLiveData() { return userLiveData; } @@ -102,7 +107,11 @@ public LiveData> getUserEventsLiveData() { return userEventsLiveData; } - public MutableLiveData getAmISubscriber() { + public LiveData getAmISubscriber() { return amISubscriber; } + + public LiveData getUserPhotoLiveData() { + return userPhotoLiveData; + } } 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 index 92249c4..8110188 100644 --- 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 @@ -14,4 +14,14 @@ public abstract class UserViewModelModule { @IntoMap @ViewModelKey(UserViewModel.class) abstract ViewModel userViewModel(UserViewModel userViewModel); + + @Binds + @IntoMap + @ViewModelKey(UserSubscribersViewModel.class) + abstract ViewModel userSubscribersViewModel(UserSubscribersViewModel usvm); + + @Binds + @IntoMap + @ViewModelKey(UserSubscriptionsViewModel.class) + abstract ViewModel userSubscriptionsViewModel(UserSubscriptionsViewModel usvm); } 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 488418d..7f03d1f 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 @@ -1,10 +1,12 @@ package com.tom.meeter.infrastructure.common; import android.content.Context; +import android.util.Log; import androidx.annotation.Nullable; import com.tom.meeter.R; +import com.tom.meeter.context.network.dto.UserDTO; import java.time.LocalDate; import java.time.LocalDateTime; @@ -12,6 +14,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; public final class CommonHelper { @@ -26,10 +29,10 @@ private CommonHelper() { 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); - case "male" -> ctx.getString(R.string.male_gender); + public static String genderResolver(Context ctx, UserDTO.UserGender gender) { + return switch (gender) { + case FEMALE -> ctx.getString(R.string.female_gender); + case MALE -> ctx.getString(R.string.male_gender); default -> throw new IllegalArgumentException("#args " + gender); }; } @@ -44,6 +47,11 @@ public static CharSequence textOrNull(Double val) { return val == null ? null : val.toString(); } + @Nullable + public static CharSequence textOrNull(Float val) { + return val == null ? null : val.toString(); + } + public static String getStringOrNull(CharSequence input) { if (input == null || EMPTY_STR.contentEquals(input)) { return null; @@ -71,6 +79,11 @@ public static LocalDate getLocalDateOrNull(CharSequence input) { if (input == null || EMPTY_STR.contentEquals(input)) { return null; } - return LocalDate.parse(input, UI_DATE_FORMAT); + try { + return LocalDate.parse(input, UI_DATE_FORMAT); + } catch (DateTimeParseException e) { + Log.e("DateParser", "Ошибка парсинга даты: " + input, e); + return null; + } } } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/DateHelper.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/DateHelper.java index cbe066b..7a3f0a5 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/DateHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/DateHelper.java @@ -1,19 +1,39 @@ package com.tom.meeter.infrastructure.common; +import static com.tom.meeter.infrastructure.common.CommonHelper.EMPTY_STR; +import static com.tom.meeter.infrastructure.common.CommonHelper.UI_DATE_TIME_FORMAT; + +import android.app.DatePickerDialog; +import android.app.TimePickerDialog; +import android.content.Context; import android.util.Log; +import android.widget.EditText; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.format.DateTimeParseException; import java.util.Calendar; +import java.util.Locale; public final class DateHelper { + private static final String TAG = DateHelper.class.getCanonicalName(); private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private DateHelper() { } - public static String getAgeFromDate(String date) { + public static String getAgeFromDateOld(String date) { if (date == null) { return ""; } @@ -36,4 +56,140 @@ public static String getAgeFromDate(String date) { return String.valueOf(age); } + + + public static String getAgeFromDate(LocalDate birthDate) { + if (birthDate == null) { + return EMPTY_STR; + } + try { + int age = Period.between(birthDate, LocalDate.now()).getYears(); + return String.valueOf(age); + } catch (DateTimeParseException e) { + Log.e(TAG, "Error while parsing the date [" + birthDate + "] : " + e.getMessage(), e); + return null; + } + } + + + // Метод для отображения DatePickerDialog + public static void showDatePickerDialog(Context ctx, 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(ctx, + (view, selectedYear, selectedMonth, selectedDay) -> { + // Устанавливаем выбранную дату в EditText + String selectedDate = selectedDay + "/" + (selectedMonth + 1) + "/" + selectedYear; + targetEditText.setText(selectedDate); + }, year, month, day); + + // Показываем диалог + datePickerDialog.show(); + } + + // Метод для отображения Material DatePicker + public static void showMaterialDatePicker( + AppCompatActivity activity, 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(activity.getSupportFragmentManager(), datePicker.toString()); + } + + public static void showDateTimePickerOld(Context ctx, EditText target) { + final Calendar calendar = Calendar.getInstance(); + + DatePickerDialog datePickerDialog = new DatePickerDialog( + ctx, + (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( + ctx, + (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 showDateTimePicker(Context ctx, EditText target) { + LocalDate nowDate = LocalDate.now(); + LocalTime nowTime = LocalTime.now(); + + DatePickerDialog datePickerDialog = new DatePickerDialog( + ctx, + (view, year, month, dayOfMonth) -> { + LocalDate selectedDate = LocalDate.of(year, month + 1, dayOfMonth); + + TimePickerDialog timePickerDialog = new TimePickerDialog( + ctx, + (timeView, hourOfDay, minute) -> { + LocalTime selectedTime = LocalTime.of(hourOfDay, minute); + LocalDateTime dateTime = LocalDateTime.of(selectedDate, selectedTime); + + String formatted = dateTime.format(UI_DATE_TIME_FORMAT); + target.setText(formatted); + }, + nowTime.getHour(), + nowTime.getMinute(), + true + ); + + timePickerDialog.show(); + }, + nowDate.getYear(), + nowDate.getMonthValue() - 1, + nowDate.getDayOfMonth() + ); + + datePickerDialog.show(); + } } 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 a6fbe81..9cac81c 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 @@ -27,6 +27,11 @@ public static void showMessage(Activity activity, String msg) { () -> Toast.makeText(activity.getApplicationContext(), msg, Toast.LENGTH_SHORT).show()); } + public static void showMessage(Activity activity, int resId) { + activity.runOnUiThread( + () -> Toast.makeText(activity.getApplicationContext(), resId, Toast.LENGTH_SHORT).show()); + } + public static void showMessage(Context ctx, String msg) { if (TextUtils.isEmpty(msg)) return; diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/JsonHelper.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/JsonHelper.java index 2df6b52..1ec8502 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/JsonHelper.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/common/JsonHelper.java @@ -1,8 +1,15 @@ package com.tom.meeter.infrastructure.common; +import static com.tom.meeter.infrastructure.common.CommonHelper.UI_DATE_FORMAT; + +import androidx.annotation.Nullable; + import org.json.JSONException; import org.json.JSONObject; +import java.time.LocalDate; +import java.time.OffsetDateTime; + public class JsonHelper { private JsonHelper() { } @@ -22,4 +29,34 @@ public static String getString(JSONObject json, String key) { throw new RuntimeException(e); } } + + @Nullable + public static String getStringOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : json.getString(key); + } + + @Nullable + public static OffsetDateTime getOffsetDateTimeOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : OffsetDateTime.parse(json.getString(key)); + } + + @Nullable + public static LocalDate getLocalDateOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : LocalDate.parse(json.getString(key), UI_DATE_FORMAT); + } + + @Nullable + public static Double getDoubleOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : json.getDouble(key); + } + + @Nullable + public static Float getFloatOrNull( + String key, JSONObject json) throws JSONException { + return json.isNull(key) ? null : (float) json.getDouble(key); + } } 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 index 65e81c0..fd8fe7c 100644 --- 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 @@ -5,7 +5,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.tom.meeter.context.network.dto.EventDTO; -import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; +import com.tom.meeter.infrastructure.components.binder.EventBinder; import java.util.ArrayList; import java.util.List; @@ -13,10 +13,10 @@ public abstract class BaseEventAdapter extends RecyclerView.Adapter { - private final ViewHolderEventBinder binder; + private final EventBinder binder; private final List events = new ArrayList<>(); - protected BaseEventAdapter(ViewHolderEventBinder binder) { + protected BaseEventAdapter(EventBinder binder) { this.binder = binder; } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseOnClickListener.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseOnClickListener.java new file mode 100644 index 0000000..109cbad --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseOnClickListener.java @@ -0,0 +1,5 @@ +package com.tom.meeter.infrastructure.components.adapter; + +public interface BaseOnClickListener { + void onClick(T me); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseSubscriberAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseSubscriberAdapter.java new file mode 100644 index 0000000..b439362 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseSubscriberAdapter.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.profile.subscriber.Subscriber; +import com.tom.meeter.infrastructure.components.binder.SubscriberBinder; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseSubscriberAdapter + extends RecyclerView.Adapter { + + private final SubscriberBinder binder; + protected final List subs = new ArrayList<>(); + + protected BaseSubscriberAdapter(SubscriberBinder binder) { + this.binder = binder; + } + + public void setData(List newSubs) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( + new SubscriberDiffCallback(subs, newSubs)); + this.subs.clear(); + this.subs.addAll(newSubs); + diffResult.dispatchUpdatesTo(this); + } + + @Override + public void onBindViewHolder(@NonNull T holder, int position) { + binder.bind(holder, subs.get(position)); + } + + @Override + public int getItemCount() { + return subs.size(); + } + + static class SubscriberDiffCallback extends DiffUtil.Callback { + + private final List oldUsers, newUsers; + + public SubscriberDiffCallback(List oldUsers, List newUsers) { + this.oldUsers = oldUsers; + this.newUsers = newUsers; + } + + @Override + public int getOldListSize() { + return oldUsers.size(); + } + + @Override + public int getNewListSize() { + return newUsers.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition).getUser().getId() + .equals(newUsers.get(newItemPosition).getUser().getId()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition) + .equals(newUsers.get(newItemPosition)); + } + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseUserAdapter.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseUserAdapter.java new file mode 100644 index 0000000..7b5c6eb --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/BaseUserAdapter.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.UserDTO; +import com.tom.meeter.infrastructure.components.binder.UserBinder; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseUserAdapter + extends RecyclerView.Adapter { + + private final UserBinder binder; + private final List users = new ArrayList<>(); + + protected BaseUserAdapter(UserBinder binder) { + this.binder = binder; + } + + public void setData(List newUsers) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( + new UsersDiffCallback(users, newUsers)); + this.users.clear(); + this.users.addAll(newUsers); + diffResult.dispatchUpdatesTo(this); + } + + @Override + public void onBindViewHolder(@NonNull T holder, int position) { + binder.bind(holder, users.get(position)); + } + + @Override + public int getItemCount() { + return users.size(); + } + + static class UsersDiffCallback extends DiffUtil.Callback { + + private final List oldUsers, newUsers; + + public UsersDiffCallback(List oldUsers, List newUsers) { + this.oldUsers = oldUsers; + this.newUsers = newUsers; + } + + @Override + public int getOldListSize() { + return oldUsers.size(); + } + + @Override + public int getNewListSize() { + return newUsers.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition).getId() + .equals(newUsers.get(newItemPosition).getId()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition) + .equals(newUsers.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 index d9495b4..9190f10 100644 --- 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 @@ -6,14 +6,14 @@ import androidx.annotation.NonNull; import com.tom.meeter.databinding.CardItemBinding; -import com.tom.meeter.infrastructure.components.binder.ViewHolderEventBinder; +import com.tom.meeter.infrastructure.components.binder.EventBinder; 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) { + public EventsCardAdapter(EventBinder binder) { super(binder); } 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 index a17d354..6a58930 100644 --- 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 @@ -2,6 +2,6 @@ import com.tom.meeter.context.network.dto.EventDTO; -public interface OnEventClickListener { - void onEventClick(EventDTO event); +public interface OnEventClickListener extends BaseOnClickListener { + void onClick(EventDTO event); } diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnSubscribeUnsubscribeClickListener.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnSubscribeUnsubscribeClickListener.java new file mode 100644 index 0000000..d37fad7 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnSubscribeUnsubscribeClickListener.java @@ -0,0 +1,7 @@ +package com.tom.meeter.infrastructure.components.adapter; + +import com.tom.meeter.context.profile.subscriber.Subscriber; + +public interface OnSubscribeUnsubscribeClickListener { + void onSubUnsub(Subscriber sub, int position); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnUserClickListener.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnUserClickListener.java new file mode 100644 index 0000000..efafc7d --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/adapter/OnUserClickListener.java @@ -0,0 +1,7 @@ +package com.tom.meeter.infrastructure.components.adapter; + +import com.tom.meeter.context.network.dto.UserDTO; + +public interface OnUserClickListener extends BaseOnClickListener { + void onClick(UserDTO user); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/BaseViewHolderBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/BaseViewHolderBinder.java new file mode 100644 index 0000000..9569d99 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/BaseViewHolderBinder.java @@ -0,0 +1,6 @@ +package com.tom.meeter.infrastructure.components.binder; + +public interface BaseViewHolderBinder { + + void bind(VH holder, TARGET target); +} 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/EventBinder.java similarity index 64% rename from AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/ViewHolderEventBinder.java rename to AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/EventBinder.java index 528ab09..6dcef1a 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/ViewHolderEventBinder.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/EventBinder.java @@ -4,7 +4,7 @@ import com.tom.meeter.context.network.dto.EventDTO; -public interface ViewHolderEventBinder { - +public interface EventBinder + extends BaseViewHolderBinder { void bind(T holder, 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 index c1a6a17..43ac1dc 100644 --- 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 @@ -10,7 +10,7 @@ import com.tom.meeter.infrastructure.components.viewholder.CardItemHolder; public class PhotoDownloaderEventBinder - implements ViewHolderEventBinder { + implements EventBinder { private static final String TAG = PhotoDownloaderEventBinder.class.getCanonicalName(); @@ -30,7 +30,7 @@ public PhotoDownloaderEventBinder( @Override public void bind(CardItemHolder holder, EventDTO event) { - holder.bind(event.getName(), null, (view) -> listener.onEventClick(event)); + holder.bind(event.getName(), null, (view) -> listener.onClick(event)); String photoPath = event.getPhotoPath(); if (photoPath == null) { return; 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/PhotoDownloaderWithCacheEventBinder.java similarity index 84% rename from AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventEventBinder.java rename to AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventBinder.java index 1469321..d6f3b8a 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventEventBinder.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheEventBinder.java @@ -15,10 +15,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -public class PhotoDownloaderWithCacheEventEventBinder - implements ViewHolderEventBinder { +public class PhotoDownloaderWithCacheEventBinder + implements EventBinder { - private static final String TAG = PhotoDownloaderWithCacheEventEventBinder.class.getCanonicalName(); + private static final String TAG = PhotoDownloaderWithCacheEventBinder.class.getCanonicalName(); private final Context ctx; private final ImageDownloader imageDownloader; @@ -26,7 +26,7 @@ public class PhotoDownloaderWithCacheEventEventBinder private final Runnable onAuthFail; private final Map imagesCache = new ConcurrentHashMap<>(); - public PhotoDownloaderWithCacheEventEventBinder( + public PhotoDownloaderWithCacheEventBinder( Context ctx, ImageDownloader imgDownloader, OnEventClickListener listener, Runnable onAuthFail) { logMethod(TAG, this); @@ -42,13 +42,13 @@ public void bind(EventViewHolder holder, EventDTO event) { if (photoPath == null) { holder.bind( event.getName(), event.getDescription(), null, - (v) -> listener.onEventClick(event)); + (v) -> listener.onClick(event)); return; } Bitmap circledPhotoCache = imagesCache.get(photoPath); holder.bind( event.getName(), event.getDescription(), circledPhotoCache, - (v) -> listener.onEventClick(event)); + (v) -> listener.onClick(event)); if (circledPhotoCache != null) { return; } @@ -58,7 +58,7 @@ public void bind(EventViewHolder holder, EventDTO event) { Bitmap circled = circleImage(photo); holder.updatePhoto(circled); imagesCache.put(photoPath, circled); - Log.d(TAG, "PhotoDownloaderWithCacheEventEventBinder: event " + + Log.d(TAG, "PhotoDownloaderWithCacheEventBinder: event " + "image downloaded for [" + photoPath + "], cache updated."); }, onAuthFail); diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheSubscriberBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheSubscriberBinder.java new file mode 100644 index 0000000..80d6081 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheSubscriberBinder.java @@ -0,0 +1,76 @@ +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.UserDTO; +import com.tom.meeter.context.profile.subscriber.Subscriber; +import com.tom.meeter.infrastructure.components.adapter.OnSubscribeUnsubscribeClickListener; +import com.tom.meeter.infrastructure.components.adapter.OnUserClickListener; +import com.tom.meeter.infrastructure.components.viewholder.SubscriberViewHolder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PhotoDownloaderWithCacheSubscriberBinder implements SubscriberBinder { + + private static final String TAG = PhotoDownloaderWithCacheSubscriberBinder.class.getCanonicalName(); + + private final Context ctx; + private final ImageDownloader imageDownloader; + private final OnUserClickListener userClickListener; + private final OnSubscribeUnsubscribeClickListener subUnsubClickListener; + private final Runnable onAuthFail; + private final Map imagesCache = new ConcurrentHashMap<>(); + + public PhotoDownloaderWithCacheSubscriberBinder( + Context ctx, ImageDownloader imgDownloader, + OnUserClickListener listener, + OnSubscribeUnsubscribeClickListener subUnsubClickListener, + Runnable onAuthFail) { + logMethod(TAG, this); + this.ctx = ctx; + this.imageDownloader = imgDownloader; + this.userClickListener = listener; + this.subUnsubClickListener = subUnsubClickListener; + this.onAuthFail = onAuthFail; + } + + @Override + public void bind(SubscriberViewHolder holder, Subscriber target) { + UserDTO user = target.getUser(); + String photoPath = user.getPhotoPath(); + if (photoPath == null) { + holder.bind( + target.isAmISubscribedTo(), + user.getName(), user.getSurname(), null, + (v) -> userClickListener.onClick(user), + (v) -> subUnsubClickListener.onSubUnsub(target, holder.getBindingAdapterPosition())); + return; + } + Bitmap circledPhotoCache = imagesCache.get(photoPath); + holder.bind( + target.isAmISubscribedTo(), + user.getName(), user.getSurname(), circledPhotoCache, + (v) -> userClickListener.onClick(user), + (v) -> subUnsubClickListener.onSubUnsub(target, holder.getBindingAdapterPosition())); + if (circledPhotoCache != null) { + return; + } + imageDownloader.downloadUserImage( + photoPath, ctx, + photo -> { + Bitmap circled = circleImage(photo); + holder.updatePhoto(circled); + imagesCache.put(photoPath, circled); + Log.d(TAG, "PhotoDownloaderWithCacheUserBinder: user " + + "image downloaded for [" + photoPath + "], cache updated."); + }, + onAuthFail); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheUserBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheUserBinder.java new file mode 100644 index 0000000..7eb8fb0 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/PhotoDownloaderWithCacheUserBinder.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.UserDTO; +import com.tom.meeter.infrastructure.components.adapter.OnUserClickListener; +import com.tom.meeter.infrastructure.components.viewholder.UserViewHolder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PhotoDownloaderWithCacheUserBinder implements UserBinder { + + private static final String TAG = PhotoDownloaderWithCacheEventBinder.class.getCanonicalName(); + + private final Context ctx; + private final ImageDownloader imageDownloader; + private final OnUserClickListener userClickListener; + private final Runnable onAuthFail; + private final Map imagesCache = new ConcurrentHashMap<>(); + + public PhotoDownloaderWithCacheUserBinder( + Context ctx, ImageDownloader imgDownloader, + OnUserClickListener listener, + Runnable onAuthFail) { + logMethod(TAG, this); + this.ctx = ctx; + this.imageDownloader = imgDownloader; + this.userClickListener = listener; + this.onAuthFail = onAuthFail; + } + + @Override + public void bind(UserViewHolder holder, UserDTO user) { + String photoPath = user.getPhotoPath(); + if (photoPath == null) { + holder.bind( + user.getName(), user.getSurname(), null, + (v) -> userClickListener.onClick(user)); + return; + } + Bitmap circledPhotoCache = imagesCache.get(photoPath); + holder.bind( + user.getName(), user.getSurname(), circledPhotoCache, + (v) -> userClickListener.onClick(user)); + if (circledPhotoCache != null) { + return; + } + imageDownloader.downloadUserImage( + photoPath, ctx, + photo -> { + Bitmap circled = circleImage(photo); + holder.updatePhoto(circled); + imagesCache.put(photoPath, circled); + Log.d(TAG, "PhotoDownloaderWithCacheUserBinder: user " + + "image downloaded for [" + photoPath + "], cache updated."); + }, + onAuthFail); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/SubscriberBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/SubscriberBinder.java new file mode 100644 index 0000000..18d653d --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/SubscriberBinder.java @@ -0,0 +1,11 @@ +package com.tom.meeter.infrastructure.components.binder; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.context.profile.subscriber.Subscriber; + +public interface SubscriberBinder + extends BaseViewHolderBinder { + + void bind(T holder, Subscriber target); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/UserBinder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/UserBinder.java new file mode 100644 index 0000000..ed9d120 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/binder/UserBinder.java @@ -0,0 +1,11 @@ +package com.tom.meeter.infrastructure.components.binder; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.context.network.dto.UserDTO; + +public interface UserBinder + extends BaseViewHolderBinder { + + void bind(T holder, UserDTO target); +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/SubscriberViewHolder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/SubscriberViewHolder.java new file mode 100644 index 0000000..8684d3e --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/SubscriberViewHolder.java @@ -0,0 +1,37 @@ +package com.tom.meeter.infrastructure.components.viewholder; + +import android.graphics.Bitmap; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import com.tom.meeter.R; +import com.tom.meeter.databinding.ActivityProfileSubscriberItemBinding; + +public class SubscriberViewHolder extends RecyclerView.ViewHolder { + + private final ActivityProfileSubscriberItemBinding binding; + + public SubscriberViewHolder(ActivityProfileSubscriberItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind( + boolean isAmSubscribedTo, String name, String surname, Bitmap photo, + View.OnClickListener cardClickListener, + View.OnClickListener subUnsubClickListener) { + binding.subscriberName.setText(name + " " + surname); + if (photo != null) { + binding.photo.setImageBitmap(photo); + } + + binding.subUnsubBtn.setOnClickListener(subUnsubClickListener); + binding.subCard.setOnClickListener(cardClickListener); + binding.subUnsubBtn.setText(isAmSubscribedTo ? R.string.unsubscribe : R.string.subscribe); + } + + public void updatePhoto(Bitmap photo) { + binding.photo.setImageBitmap(photo); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/UserViewHolder.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/UserViewHolder.java new file mode 100644 index 0000000..cc664a9 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/components/viewholder/UserViewHolder.java @@ -0,0 +1,32 @@ +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.ActivityUserSubscriberItemBinding; + +public class UserViewHolder extends RecyclerView.ViewHolder { + + private final ActivityUserSubscriberItemBinding binding; + + public UserViewHolder(ActivityUserSubscriberItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind( + String name, String surname, Bitmap photo, + View.OnClickListener cardClickListener) { + binding.subscriberName.setText(name + " " + surname); + if (photo != null) { + binding.photo.setImageBitmap(photo); + } + binding.subCard.setOnClickListener(cardClickListener); + } + + public void updatePhoto(Bitmap photo) { + binding.photo.setImageBitmap(photo); + } +} diff --git a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRecreatorOnAuthFailure.java b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRecreatorOnAuthFailure.java new file mode 100644 index 0000000..7bd74b0 --- /dev/null +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/ActivityRecreatorOnAuthFailure.java @@ -0,0 +1,27 @@ +package com.tom.meeter.infrastructure.http; + +import android.app.Activity; +import android.util.Log; + +import retrofit2.Call; +import retrofit2.Response; + +public class ActivityRecreatorOnAuthFailure extends HttpErrorLogger { + + private static final String TAG = ActivityRecreatorOnAuthFailure.class.getCanonicalName(); + private final Activity activity; + + public ActivityRecreatorOnAuthFailure(Activity activity) { + super(activity.getApplicationContext()); + this.activity = activity; + } + + @Override + public void onResponse(Call call, Response response) { + super.onResponse(call, response); + if (response.code() == HttpCodes.NOT_AUTHENTICATED) { + Log.d(TAG, "Recreating: " + activity.getClass().getCanonicalName()); + activity.recreate(); + } + } +} 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 bee1312..7e96068 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 @@ -26,9 +26,9 @@ public DisconnectLogger(Context ctx) { public void onFailure(Call call, Throwable t) { 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()); + Log.e(TAG, "DisconnectLogger for [" + ctx.getClass().getCanonicalName() + + "]: " + ctx.getResources().getString(R.string.server_is_unreachable) + + " Error: " + t.getClass().getCanonicalName() + " - " + t.getMessage()); } } 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 index 95b3b08..8835884 100644 --- a/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpErrorLogger.java +++ b/AndroidClient/src/main/java/com/tom/meeter/infrastructure/http/HttpErrorLogger.java @@ -35,6 +35,7 @@ public void onResponse(Call call, Response response) { } int code = response.code(); showMessage(ctx, code + "/" + errorMessage); - Log.e(TAG, "HTTP request error: " + code + "/" + errorMessage); + Log.e(TAG, "HTTP request failed for [" + ctx.getClass().getCanonicalName() + + "] with http code [" + code + "] and body " + errorMessage); } } 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 56b3568..95f80d6 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 @@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModel; import com.tom.meeter.context.profile.viewmodel.ProfileEventsViewModel; +import com.tom.meeter.context.profile.viewmodel.ProfileSubscribersViewModel; +import com.tom.meeter.context.profile.viewmodel.ProfileSubscriptionsViewModel; import com.tom.meeter.context.profile.viewmodel.ProfileViewModel; import dagger.Binds; @@ -29,4 +31,14 @@ public ViewModelModule() { @IntoMap @ViewModelKey(ProfileEventsViewModel.class) abstract ViewModel profileEventsViewModel(ProfileEventsViewModel profileEventsViewModel); + + @Binds + @IntoMap + @ViewModelKey(ProfileSubscribersViewModel.class) + abstract ViewModel profileSubscribersViewModel(ProfileSubscribersViewModel psvm); + + @Binds + @IntoMap + @ViewModelKey(ProfileSubscriptionsViewModel.class) + abstract ViewModel profileSubscriptionsViewModel(ProfileSubscriptionsViewModel psvm); } diff --git a/AndroidClient/src/main/res/drawable/user_500x500_removebg.png b/AndroidClient/src/main/res/drawable/user_500x500_removebg.png new file mode 100644 index 0000000..8c6ccd1 Binary files /dev/null and b/AndroidClient/src/main/res/drawable/user_500x500_removebg.png differ diff --git a/AndroidClient/src/main/res/layout/activity_event_editable.xml b/AndroidClient/src/main/res/layout/activity_event_editable.xml index 102f0f0..edd4640 100644 --- a/AndroidClient/src/main/res/layout/activity_event_editable.xml +++ b/AndroidClient/src/main/res/layout/activity_event_editable.xml @@ -18,18 +18,27 @@ android:padding="16dp"> + +