Skip to content
This repository was archived by the owner on Jun 5, 2022. It is now read-only.

Commit 022d6c3

Browse files
committed
Split date/time input for watcher dates and pick date as range
- Split up the watcher begin/end/startafter/startbefore date and time inputs; selecting date is now separate from selecting the time. Also changed wording and location to make it more like other apps and easier to understand. - Use AS 4.0 desugaring to use java.time on older API levels, and use this to make the split inputs easier + smarter defaults for new watchers possible. - Select dates in ranges (start + end at once) using the new MaterialDatePicker. - Fix possible crash on recreation due to Chips triggering onCheckedChangeListeners when recreated.
1 parent 477b710 commit 022d6c3

File tree

7 files changed

+357
-90
lines changed

7 files changed

+357
-90
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ An Android front end application for interacting with a [Movie Notifier](https:/
66

77
## Building
88

9-
- *Requires* Android Studio 3.6 RC 2 or newer.
9+
- *Requires* Android Studio 4.0 Canary 9 or newer.
1010
- Add the `google-services.json` file from your Firebase project to the `app` module.
1111
- Make sure to add a `gradle.properties` file in the root of the project. An example of the file is [included](https://github.com/jpelgrom/Movie-Notifier-Android/blob/master/gradle.properties.example).

app/build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ android {
3737
files("$projectDir/schemas".toString())
3838
}
3939
compileOptions {
40+
coreLibraryDesugaringEnabled true
4041
sourceCompatibility JavaVersion.VERSION_1_8
4142
targetCompatibility JavaVersion.VERSION_1_8
4243
}
@@ -46,9 +47,12 @@ android {
4647
}
4748

4849
dependencies {
50+
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.4'
51+
4952
implementation fileTree(dir: 'libs', include: ['*.jar'])
5053

5154
implementation 'androidx.appcompat:appcompat:1.1.0'
55+
implementation "androidx.core:core:1.2.0"
5256
implementation 'androidx.recyclerview:recyclerview:1.1.0'
5357
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
5458
implementation 'androidx.emoji:emoji-appcompat:1.0.0'
@@ -63,8 +67,8 @@ dependencies {
6367

6468
implementation 'com.google.firebase:firebase-messaging:20.1.0'
6569

66-
implementation 'androidx.work:work-runtime:2.3.0'
67-
implementation 'androidx.work:work-gcm:2.3.0'
70+
implementation 'androidx.work:work-runtime:2.3.1'
71+
implementation 'androidx.work:work-gcm:2.3.1'
6872

6973
implementation 'org.apache.commons:commons-text:1.8'
7074

app/src/main/java/nl/jpelgrm/movienotifier/ui/WatcherActivity.java

Lines changed: 166 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package nl.jpelgrm.movienotifier.ui;
22

33
import android.Manifest;
4-
import android.app.DatePickerDialog;
4+
import android.annotation.SuppressLint;
55
import android.app.NotificationManager;
66
import android.app.TimePickerDialog;
77
import android.content.Context;
@@ -31,15 +31,28 @@
3131
import androidx.coordinatorlayout.widget.CoordinatorLayout;
3232
import androidx.core.app.ActivityCompat;
3333
import androidx.core.content.ContextCompat;
34+
import androidx.core.util.Pair;
3435
import androidx.core.view.ViewCompat;
3536

3637
import com.google.android.material.chip.Chip;
38+
import com.google.android.material.datepicker.CalendarConstraints;
39+
import com.google.android.material.datepicker.DateValidatorPointForward;
40+
import com.google.android.material.datepicker.MaterialDatePicker;
3741
import com.google.android.material.snackbar.Snackbar;
3842

3943
import org.apache.commons.text.WordUtils;
4044

4145
import java.text.DateFormat;
4246
import java.text.SimpleDateFormat;
47+
import java.time.DayOfWeek;
48+
import java.time.Instant;
49+
import java.time.LocalDate;
50+
import java.time.LocalDateTime;
51+
import java.time.LocalTime;
52+
import java.time.ZoneId;
53+
import java.time.ZonedDateTime;
54+
import java.time.temporal.ChronoField;
55+
import java.time.temporal.TemporalAdjusters;
4356
import java.util.ArrayList;
4457
import java.util.Arrays;
4558
import java.util.Calendar;
@@ -208,22 +221,45 @@ public void afterTextChanged(Editable editable) {
208221
}
209222
}
210223

211-
binding.begin.setOnClickListener(view -> {
224+
binding.watcherStart.setOnClickListener(view -> {
212225
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
213-
showDateTimePicker(true, true, watcher.getBegin());
226+
showDatePicker(false);
214227
});
215-
binding.end.setOnClickListener(view -> {
228+
binding.startAfterDate.setOnClickListener(view -> {
216229
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
217-
showDateTimePicker(true, false, watcher.getEnd());
230+
showDatePicker(false);
218231
});
219-
220-
binding.filterStartAfter.setOnClickListener(view -> {
232+
binding.startAfterTime.setOnClickListener(v -> {
233+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
234+
showTimePicker(false, true, watcher.getFilters().getStartAfter());
235+
});
236+
binding.startBeforeDate.setOnClickListener(view -> {
237+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
238+
showDatePicker(false);
239+
});
240+
binding.startBeforeTime.setOnClickListener(v -> {
241+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
242+
showTimePicker(false, false, watcher.getFilters().getStartBefore());
243+
});
244+
binding.active.setOnClickListener(view -> {
245+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
246+
showDatePicker(true);
247+
});
248+
binding.beginDate.setOnClickListener(view -> {
249+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
250+
showDatePicker(true);
251+
});
252+
binding.beginTime.setOnClickListener(v -> {
221253
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
222-
showDateTimePicker(false, true, watcher.getFilters().getStartAfter());
254+
showTimePicker(true, true, watcher.getBegin());
223255
});
224-
binding.filterStartBefore.setOnClickListener(view -> {
256+
binding.endDate.setOnClickListener(view -> {
225257
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
226-
showDateTimePicker(false, false, watcher.getFilters().getStartBefore());
258+
showDatePicker(true);
259+
});
260+
binding.endTime.setOnClickListener(v -> {
261+
InterfaceUtil.clearForcus(WatcherActivity.this); // Prevent scroll after popup close due to focusing again
262+
showTimePicker(true, false, watcher.getEnd());
227263
});
228264

229265
binding.filterRegularShowing.setOnCheckedChangeListener((buttonView, isChecked) -> validateAndUpdateExperiences());
@@ -278,7 +314,7 @@ public void afterTextChanged(Editable editable) {
278314
setupWatcher();
279315
});
280316

281-
// Al ready to go!
317+
// All ready to go!
282318
setupWatcher();
283319
}
284320

@@ -322,6 +358,7 @@ private void setupSharedInfo() {
322358
}
323359
}
324360

361+
@SuppressLint("NewApi")
325362
private void setupWatcher() {
326363
if(getIntent().getExtras() != null && !getIntent().getExtras().getString("id", "").equals("")) {
327364
id = getIntent().getExtras().getString("id");
@@ -340,11 +377,20 @@ private void setupWatcher() {
340377
if(sharedMovieID != null && sharedMovieID > 0) {
341378
watcher.setMovieID(sharedMovieID);
342379
}
380+
343381
watcher.setBegin(System.currentTimeMillis());
344-
watcher.setEnd(System.currentTimeMillis() + oneWeek);
382+
LocalDate nextMonday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));
383+
LocalTime ninePm = LocalTime.of(21, 0);
384+
watcher.setEnd(LocalDateTime.of(nextMonday, ninePm).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L);
385+
345386
watcher.getFilters().setCinemaID(settings.getInt("prefSelectedCinema", 0));
346-
watcher.getFilters().setStartAfter(System.currentTimeMillis() + oneWeek);
347-
watcher.getFilters().setStartBefore(System.currentTimeMillis() + oneWeek + oneWeek);
387+
388+
LocalDate nextWednesday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
389+
LocalDate nextNextWednesday = nextWednesday.with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
390+
LocalTime tenAm = LocalTime.of(10, 0);
391+
LocalTime elevenPm = LocalTime.of(23, 0);
392+
watcher.getFilters().setStartAfter(LocalDateTime.of(nextWednesday, tenAm).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L);
393+
watcher.getFilters().setStartBefore(LocalDateTime.of(nextNextWednesday, elevenPm).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L);
348394

349395
mode = Mode.EDITING;
350396

@@ -492,11 +538,20 @@ private void updateViews(boolean cinemaOnly) {
492538

493539
binding.autocompleteSuggestion.setVisibility((mode == Mode.EDITING && settings.getInt("prefAutocompleteLocation", -1) == -1) ? View.VISIBLE : View.GONE);
494540

495-
DateFormat format = SimpleDateFormat.getDateTimeInstance(java.text.DateFormat.MEDIUM, java.text.DateFormat.SHORT);
496-
binding.begin.setValue(format.format(new Date(watcher.getBegin())));
497-
binding.end.setValue(format.format(new Date(watcher.getEnd())));
498-
binding.filterStartAfter.setValue(format.format(new Date(watcher.getFilters().getStartAfter())));
499-
binding.filterStartBefore.setValue(format.format(new Date(watcher.getFilters().getStartBefore())));
541+
DateFormat dateFormat = SimpleDateFormat.getDateInstance(DateFormat.MEDIUM);
542+
DateFormat timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.SHORT);
543+
Date watcherBegin = new Date(watcher.getBegin());
544+
Date watcherEnd = new Date(watcher.getEnd());
545+
binding.beginDate.setText(dateFormat.format(watcherBegin));
546+
binding.beginTime.setText(timeFormat.format(watcherBegin));
547+
binding.endDate.setText(dateFormat.format(watcherEnd));
548+
binding.endTime.setText(timeFormat.format(watcherEnd));
549+
Date watcherStartAfter = new Date(watcher.getFilters().getStartAfter());
550+
Date watcherStartBefore = new Date(watcher.getFilters().getStartBefore());
551+
binding.startAfterDate.setText(dateFormat.format(watcherStartAfter));
552+
binding.startAfterTime.setText(timeFormat.format(watcherStartAfter));
553+
binding.startBeforeDate.setText(dateFormat.format(watcherStartBefore));
554+
binding.startBeforeTime.setText(timeFormat.format(watcherStartBefore));
500555

501556
updateViewsFilters();
502557

@@ -584,10 +639,16 @@ private void setFieldsEditable(boolean editable) {
584639
binding.watcherCinemaID.setFocusableInTouchMode(editable);
585640
binding.watcherCinemaID.setCursorVisible(editable);
586641

587-
binding.begin.setClickable(editable);
588-
binding.end.setClickable(editable);
589-
binding.filterStartAfter.setClickable(editable);
590-
binding.filterStartBefore.setClickable(editable);
642+
binding.watcherStart.setClickable(editable);
643+
binding.startAfterDate.setClickable(editable);
644+
binding.startAfterTime.setClickable(editable);
645+
binding.startBeforeDate.setClickable(editable);
646+
binding.startBeforeTime.setClickable(editable);
647+
binding.active.setClickable(editable);
648+
binding.beginDate.setClickable(editable);
649+
binding.beginTime.setClickable(editable);
650+
binding.endDate.setClickable(editable);
651+
binding.endTime.setClickable(editable);
591652

592653
binding.filterRegularShowing.setClickable(editable);
593654
binding.filterRegularShowing.setVisibility(editable ? View.VISIBLE : (binding.filterRegularShowing.isChecked() ? View.VISIBLE : View.GONE));
@@ -627,41 +688,91 @@ private void doneLoading() {
627688
binding.fab.show();
628689
}
629690

630-
private void showDateTimePicker(final boolean checkingValue, final boolean beginValue, long currentValue) {
631-
final Calendar current = Calendar.getInstance();
691+
@SuppressLint("NewApi")
692+
private void showDatePicker(boolean checkingValue) {
693+
MaterialDatePicker.Builder<Pair<Long, Long>> builder = MaterialDatePicker.Builder.dateRangePicker();
694+
Pair<Long, Long> selectedRange;
695+
if(checkingValue) {
696+
selectedRange = new Pair<>(watcher.getBegin(), watcher.getEnd());
697+
} else {
698+
selectedRange = new Pair<>(watcher.getFilters().getStartAfter(), watcher.getFilters().getStartBefore());
699+
}
700+
701+
long startOfFirstDateInMillis = ZonedDateTime.ofInstant(Instant.ofEpochSecond(selectedRange.first/1000), ZoneId.systemDefault())
702+
.with(ChronoField.HOUR_OF_DAY, 0).with(ChronoField.MINUTE_OF_DAY, 0).with(ChronoField.SECOND_OF_MINUTE, 0)
703+
.toEpochSecond() * 1000L;
704+
long startOfTodayInMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault())
705+
.toEpochSecond() * 1000L;
706+
707+
builder.setSelection(selectedRange);
708+
builder.setCalendarConstraints(new CalendarConstraints.Builder()
709+
.setStart(Math.min(System.currentTimeMillis() - 1000L, selectedRange.first))
710+
.setOpenAt(selectedRange.first)
711+
.setValidator(DateValidatorPointForward.from(Math.min(startOfFirstDateInMillis, startOfTodayInMillis)))
712+
.build());
713+
builder.setTitleText(checkingValue ? R.string.watcher_date_title_dialog : R.string.watcher_filter_title_startafter_dialog);
714+
MaterialDatePicker picker = builder.build();
715+
picker.addOnPositiveButtonClickListener(selection -> {
716+
LocalTime startTime, endTime;
717+
if(checkingValue) {
718+
startTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(watcher.getBegin()), ZoneId.systemDefault()).toLocalTime();
719+
endTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(watcher.getEnd()), ZoneId.systemDefault()).toLocalTime();
720+
} else {
721+
startTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(watcher.getFilters().getStartAfter()), ZoneId.systemDefault()).toLocalTime();
722+
endTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(watcher.getFilters().getStartBefore()), ZoneId.systemDefault()).toLocalTime();
723+
}
724+
725+
Pair<Long, Long> newDates = (Pair<Long, Long>) selection;
726+
LocalDate startDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(newDates.first), ZoneId.systemDefault()).toLocalDate();
727+
LocalDate endDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(newDates.second), ZoneId.systemDefault()).toLocalDate();
728+
729+
long newStart = LocalDateTime.of(startDate, startTime).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L;
730+
long newEnd = LocalDateTime.of(endDate, endTime).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L;
731+
732+
if(checkingValue) {
733+
watcher.setBegin(newStart);
734+
watcher.setEnd(newEnd);
735+
validateAndFixEnd();
736+
} else {
737+
watcher.getFilters().setStartAfter(newStart);
738+
watcher.getFilters().setStartBefore(newEnd);
739+
validateAndFixStartBefore();
740+
}
741+
742+
updateViews();
743+
});
744+
picker.show(getSupportFragmentManager(), checkingValue ? "watcherActivePicker" : "watcherStartPicker");
745+
}
746+
747+
private void showTimePicker(boolean checkingValue, boolean beginValue, long currentValue) {
748+
Calendar current = Calendar.getInstance();
632749
current.setTimeInMillis(currentValue);
633750

634-
DatePickerDialog datePickerDialog = new DatePickerDialog(this, (datePicker, year, month, day) -> {
635-
final int mYear = year;
636-
final int mMonth = month;
637-
final int mDay = day;
638-
TimePickerDialog timePickerDialog = new TimePickerDialog(WatcherActivity.this, (timePicker, hour, minute) -> {
639-
Calendar setTo = Calendar.getInstance();
640-
setTo.set(mYear, mMonth, mDay, hour, minute, 0);
641-
if(checkingValue) {
642-
if(beginValue) {
643-
watcher.setBegin(setTo.getTimeInMillis());
644-
validateAndFixEnd();
645-
} else {
646-
watcher.setEnd(setTo.getTimeInMillis());
647-
validateAndFixBegin();
648-
}
751+
TimePickerDialog picker = new TimePickerDialog(this, (view, hourOfDay, minute) -> {
752+
Calendar setTo = Calendar.getInstance();
753+
setTo.setTimeInMillis(currentValue);
754+
setTo.set(current.get(Calendar.YEAR), current.get(Calendar.MONTH), current.get(Calendar.DAY_OF_MONTH), hourOfDay, minute, 0);
755+
if(checkingValue) {
756+
if(beginValue) {
757+
watcher.setBegin(setTo.getTimeInMillis());
758+
validateAndFixEnd();
649759
} else {
650-
if(beginValue) {
651-
watcher.getFilters().setStartAfter(setTo.getTimeInMillis());
652-
validateAndFixStartBefore();
653-
} else {
654-
watcher.getFilters().setStartBefore(setTo.getTimeInMillis());
655-
validateAndFixStartAfter();
656-
}
760+
watcher.setEnd(setTo.getTimeInMillis());
761+
validateAndFixBegin();
762+
}
763+
} else {
764+
if(beginValue) {
765+
watcher.getFilters().setStartAfter(setTo.getTimeInMillis());
766+
validateAndFixStartBefore();
767+
} else {
768+
watcher.getFilters().setStartBefore(setTo.getTimeInMillis());
769+
validateAndFixStartAfter();
657770
}
771+
}
658772

659-
updateViews();
660-
}, current.get(Calendar.HOUR_OF_DAY), current.get(Calendar.MINUTE), android.text.format.DateFormat.is24HourFormat(WatcherActivity.this));
661-
timePickerDialog.show();
662-
}, current.get(Calendar.YEAR), current.get(Calendar.MONTH), current.get(Calendar.DAY_OF_MONTH));
663-
datePickerDialog.getDatePicker().setMinDate(System.currentTimeMillis() - 1000L);
664-
datePickerDialog.show();
773+
updateViews();
774+
}, current.get(Calendar.HOUR_OF_DAY), current.get(Calendar.MINUTE), android.text.format.DateFormat.is24HourFormat(this));
775+
picker.show();
665776
}
666777

667778
private boolean validateName(boolean forced) {
@@ -811,7 +922,7 @@ private void validateAndFixStartBefore() {
811922
}
812923

813924
private boolean validateAndUpdate3D() {
814-
if(updatingViews) {
925+
if(updatingViews || watcher == null) {
815926
return false;
816927
}
817928
if(watcher.getFilters() == null) {
@@ -828,7 +939,7 @@ private boolean validateAndUpdate3D() {
828939
}
829940

830941
private boolean validateAndUpdateExperiences() {
831-
if(updatingViews) {
942+
if(updatingViews || watcher == null) {
832943
return false;
833944
}
834945

0 commit comments

Comments
 (0)