diff --git a/android-app/.gitignore b/android-app/.gitignore new file mode 100755 index 0000000..aa724b7 --- /dev/null +++ b/android-app/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android-app/.idea/.gitignore b/android-app/.idea/.gitignore new file mode 100755 index 0000000..26d3352 --- /dev/null +++ b/android-app/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/android-app/.idea/.name b/android-app/.idea/.name new file mode 100755 index 0000000..0e0eebb --- /dev/null +++ b/android-app/.idea/.name @@ -0,0 +1 @@ +Birthday Countdown \ No newline at end of file diff --git a/android-app/.idea/compiler.xml b/android-app/.idea/compiler.xml new file mode 100755 index 0000000..fb7f4a8 --- /dev/null +++ b/android-app/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/.idea/deploymentTargetDropDown.xml b/android-app/.idea/deploymentTargetDropDown.xml new file mode 100755 index 0000000..9c68abe --- /dev/null +++ b/android-app/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/gradle.xml b/android-app/.idea/gradle.xml new file mode 100755 index 0000000..526b4c2 --- /dev/null +++ b/android-app/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/misc.xml b/android-app/.idea/misc.xml new file mode 100755 index 0000000..029ddbb --- /dev/null +++ b/android-app/.idea/misc.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/.gitignore b/android-app/app/.gitignore new file mode 100755 index 0000000..42afabf --- /dev/null +++ b/android-app/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android-app/app/build.gradle b/android-app/app/build.gradle new file mode 100755 index 0000000..1cbc4bd --- /dev/null +++ b/android-app/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 31 + + defaultConfig { + applicationId "de.drmaxnix.birthdaycountdown" + minSdk 24 + targetSdk 31 + versionCode 1 + versionName "1.0.0-a" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'com.google.android.material:material:1.6.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/android-app/app/proguard-rules.pro b/android-app/app/proguard-rules.pro new file mode 100755 index 0000000..481bb43 --- /dev/null +++ b/android-app/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android-app/app/release/output-metadata.json b/android-app/app/release/output-metadata.json new file mode 100755 index 0000000..824fec1 --- /dev/null +++ b/android-app/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "de.drmaxnix.kimsbirthdayapp", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0.1", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/android-app/app/src/androidTest/java/de/drmaxnix/birthdaycountdown/ExampleInstrumentedTest.java b/android-app/app/src/androidTest/java/de/drmaxnix/birthdaycountdown/ExampleInstrumentedTest.java new file mode 100755 index 0000000..a2e7ce2 --- /dev/null +++ b/android-app/app/src/androidTest/java/de/drmaxnix/birthdaycountdown/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package de.drmaxnix.birthdaycountdown; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("de.drmaxnix.kaysbirthdayapp", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..5fd066a --- /dev/null +++ b/android-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/icon-playstore.png b/android-app/app/src/main/icon-playstore.png new file mode 100755 index 0000000..48e5456 Binary files /dev/null and b/android-app/app/src/main/icon-playstore.png differ diff --git a/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/Birthday.java b/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/Birthday.java new file mode 100755 index 0000000..a3e67aa --- /dev/null +++ b/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/Birthday.java @@ -0,0 +1,80 @@ +package de.drmaxnix.birthdaycountdown; + +import android.icu.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +public class Birthday { + private final int age; + private final int[] date; + + /* + CONSTRUCTOR: Get new birthday object + */ + private Birthday(int age, int[] date){ + this.age = age; + this.date = date; + } + + /* + HELPER: Find next birthday and return new birthday object + */ + public static Birthday find_next(int[] birthdate){ + // FIND DATE // + final Calendar now = Calendar.getInstance(); + + // try this year + GregorianCalendar birthday_try = new GregorianCalendar(now.get(Calendar.YEAR), birthdate[1], birthdate[2]); + + // already in past? use next year! + if(birthday_try.getTime().getTime() < now.getTime().getTime()){ + birthday_try = new GregorianCalendar(now.get(Calendar.YEAR) + 1, birthdate[1], birthdate[2]); + } + + + // GET VALUES // + int year = birthday_try.get(Calendar.YEAR); + int month = birthday_try.get(Calendar.MONTH); + int day = birthday_try.get(Calendar.DAY_OF_MONTH); + + + // GET AGE // + int age = year - birthdate[0]; + + + // RETURN NEW BIRTHDAY OBJECT // + return new Birthday(age, new int[]{year, month, day}); + } + + /* + GETTER: Get age + */ + public int age(){ + return age; + } + + /* + GETTER: Get millis left till birthday + */ + public double millis_left(){ + // GET DATES // + // objects + Date date_now = new Date(); + Date date_birthday; + + // load birthday date + date_birthday = new GregorianCalendar(date[0], date[1], date[2]).getTime(); + + + // GET TIME DIFFERENCE // + // millis + double date_diff_millis = date_birthday.getTime() - date_now.getTime(); + + // make sure it's not negative + date_diff_millis = Math.max(date_diff_millis, 0); + + + // RETURN // + return date_diff_millis; + } +} diff --git a/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/MainActivity.java b/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/MainActivity.java new file mode 100755 index 0000000..a174135 --- /dev/null +++ b/android-app/app/src/main/java/de/drmaxnix/birthdaycountdown/MainActivity.java @@ -0,0 +1,500 @@ +package de.drmaxnix.birthdaycountdown; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import android.app.DatePickerDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.icu.util.Calendar; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TreeMap; + +public class MainActivity extends AppCompatActivity { + SharedPreferences settings_store; + + int[] birthdate = new int[3]; + + TextView countdown_days; + TextView countdown_hours; + TextView countdown_minutes; + + LinearLayout date_select_container; + TextView date_select_text; + + LinearLayout milestone_last_birthday_container; + LinearLayout milestone_350_container; + LinearLayout milestone_300_container; + LinearLayout milestone_250_container; + LinearLayout milestone_200_container; + LinearLayout milestone_150_container; + LinearLayout milestone_100_container; + LinearLayout milestone_75_container; + LinearLayout milestone_50_container; + LinearLayout milestone_30_container; + LinearLayout milestone_15_container; + LinearLayout milestone_7_container; + LinearLayout milestone_3_container; + LinearLayout milestone_1_container; + LinearLayout milestone_next_birthday_container; + + TextView milestone_last_birthday_countdown; + TextView milestone_350_countdown; + TextView milestone_300_countdown; + TextView milestone_250_countdown; + TextView milestone_200_countdown; + TextView milestone_150_countdown; + TextView milestone_100_countdown; + TextView milestone_75_countdown; + TextView milestone_50_countdown; + TextView milestone_30_countdown; + TextView milestone_15_countdown; + TextView milestone_7_countdown; + TextView milestone_3_countdown; + TextView milestone_1_countdown; + TextView milestone_next_birthday_countdown; + + TextView milestone_last_birthday_age; + TextView milestone_next_birthday_age; + + LinearLayout about_website_button; + + Handler update_handler = new Handler(); + + @Override + protected void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + + // OPEN SETTINGS STORE // + // get object + settings_store = this.getSharedPreferences("settings", 0); + + // load birthdate + birthdate[0] = settings_store.getInt("birthdate.year", -1); + birthdate[1] = settings_store.getInt("birthdate.month", -1); + birthdate[2] = settings_store.getInt("birthdate.day", -1); + + + // BIND VIEWS // + countdown_days = findViewById(R.id.countdown_days); + countdown_hours = findViewById(R.id.countdown_hours); + countdown_minutes = findViewById(R.id.countdown_minutes); + + date_select_container = findViewById(R.id.date_select_container); + date_select_text = findViewById(R.id.date_select_text); + + milestone_last_birthday_container = findViewById(R.id.milestone_last_birthday_container); + milestone_350_container = findViewById(R.id.milestone_350_container); + milestone_300_container = findViewById(R.id.milestone_300_container); + milestone_250_container = findViewById(R.id.milestone_250_container); + milestone_200_container = findViewById(R.id.milestone_200_container); + milestone_150_container = findViewById(R.id.milestone_150_container); + milestone_100_container = findViewById(R.id.milestone_100_container); + milestone_75_container = findViewById(R.id.milestone_75_container); + milestone_50_container = findViewById(R.id.milestone_50_container); + milestone_30_container = findViewById(R.id.milestone_30_container); + milestone_15_container = findViewById(R.id.milestone_15_container); + milestone_7_container = findViewById(R.id.milestone_7_container); + milestone_3_container = findViewById(R.id.milestone_3_container); + milestone_1_container = findViewById(R.id.milestone_1_container); + milestone_next_birthday_container = findViewById(R.id.milestone_next_birthday_container); + + milestone_last_birthday_countdown = findViewById(R.id.milestone_last_birthday_countdown); + milestone_350_countdown = findViewById(R.id.milestone_350_countdown); + milestone_300_countdown = findViewById(R.id.milestone_300_countdown); + milestone_250_countdown = findViewById(R.id.milestone_250_countdown); + milestone_200_countdown = findViewById(R.id.milestone_200_countdown); + milestone_150_countdown = findViewById(R.id.milestone_150_countdown); + milestone_100_countdown = findViewById(R.id.milestone_100_countdown); + milestone_75_countdown = findViewById(R.id.milestone_75_countdown); + milestone_50_countdown = findViewById(R.id.milestone_50_countdown); + milestone_30_countdown = findViewById(R.id.milestone_30_countdown); + milestone_15_countdown = findViewById(R.id.milestone_15_countdown); + milestone_7_countdown = findViewById(R.id.milestone_7_countdown); + milestone_3_countdown = findViewById(R.id.milestone_3_countdown); + milestone_1_countdown = findViewById(R.id.milestone_1_countdown); + milestone_next_birthday_countdown = findViewById(R.id.milestone_next_birthday_countdown); + + milestone_last_birthday_age = findViewById(R.id.milestone_last_birthday_age); + milestone_next_birthday_age = findViewById(R.id.milestone_next_birthday_age); + + about_website_button = findViewById(R.id.about_website_button); + + + // SET VERSION TEXT // + try { + // get version of the app + PackageInfo pInfo = this.getPackageManager().getPackageInfo(this.getPackageName(), 0); + String version = pInfo.versionName; + + // set text + TextView version_text = findViewById(R.id.version); + version_text.setText(version); + + } catch(PackageManager.NameNotFoundException e){ + e.printStackTrace(); + } + + + // INITIALIZE DATE SELECT // + // date picker dialog + DatePickerDialog.OnDateSetListener date_select_picker = (view, year, month, day) -> { + // UPDATE SETTINGS-STORE // + // get editor object + SharedPreferences.Editor editor = settings_store.edit(); + + // store + editor.putInt("birthdate.year", year); + editor.putInt("birthdate.month", month); + editor.putInt("birthdate.day", day); + + // apply changes + editor.apply(); + + + // RELOAD VALUES // + birthdate[0] = year; + birthdate[1] = month; + birthdate[2] = day; + + + // UPDATE UI // + // birth date + date_select_update(); + + // overview countdown + update_overview_countdown(); + + // milestones + update_milestones(); + }; + + // on-click listener + date_select_container.setOnClickListener(view -> { + // get default values for date picker + int default_year; + int default_month; + int default_day; + + if(birthdate[0] > -1 && birthdate[1] > -1 && birthdate[2] > -1){ + // use stored date + default_year = birthdate[0]; + default_month = birthdate[1]; + default_day = birthdate[2]; + + } else { + // no date set yet, use today + final Calendar now = Calendar.getInstance(); + default_year = now.get(Calendar.YEAR); + default_month = now.get(Calendar.MONTH); + default_day = now.get(Calendar.DAY_OF_MONTH); + } + + // open date picker dialog + new DatePickerDialog(MainActivity.this, date_select_picker, default_year, default_month, default_day).show(); + }); + + // load date into textview + date_select_update(); + + + // WEBSITE-BUTTON ONCLICK CALLBACK // + about_website_button.setOnClickListener(v -> this.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://birthdaycountdown.drmaxnix.de")))); + + + // START PAGE UPDATE HANDLER // + update_handler.post(update_runnable); + } + + + /* + HELPER: Update value of date select text + */ + private void date_select_update(){ + if(birthdate[0] > -1 && birthdate[1] > -1 && birthdate[2] > -1){ + // get date object (month starts with 0) + Date date = new GregorianCalendar(birthdate[0], birthdate[1], birthdate[2]).getTime(); + + // format date + SimpleDateFormat date_format = new SimpleDateFormat("dd MMM yyyy", Locale.US); + String date_text_formatted = date_format.format(date); + + // display + date_select_text.setText(date_text_formatted); + + } else { + // no date set yet, make it a button + date_select_text.setText(R.string.select_date_button); + } + } + + + /* + RUNNABLE: Update page + */ + Runnable update_runnable = new Runnable(){ + public void run(){ + // UPDATE STUFF // + // overview countdown + update_overview_countdown(); + + // milestones + update_milestones(); + + + // CALL AGAIN AFTER DELAY // + update_handler.postDelayed(this, 1000); + } + }; + + /* + HELPER: Update overview countdown + */ + private void update_overview_countdown(){ + // MAYBE DISPLAY PLACEHOLDER // + if(birthdate[0] <= -1 || birthdate[1] <= -1 || birthdate[2] <= -1){ + countdown_days.setText(R.string.countdown_days_placeholder); + countdown_hours.setText(R.string.countdown_hours_placeholder); + countdown_minutes.setText(R.string.countdown_minutes_placeholder); + return; + } + + + // GET COUNTDOWN // + // find next birthday + Birthday birthday = Birthday.find_next(birthdate); + + // format millis left + String[] countdown = format_countdown(birthday.millis_left()); + + // set texts + countdown_days.setText(countdown[0]); + countdown_hours.setText(countdown[1]); + countdown_minutes.setText(countdown[2]); + } + + /* + HELPER: Update milestones + */ + private void update_milestones(){ + // PRELOAD RESOURCES // + // colors + int color_theme = ContextCompat.getColor(this, R.color.theme); + int color_theme_dark = ContextCompat.getColor(this, R.color.theme_dark); + int color_gray_dark = ContextCompat.getColor(this, R.color.gray_dark); + + // mini-countdown format + String milestone_mini_countdown = getResources().getString(R.string.milestone_mini_countdown); + + // milestone list elements + TreeMap container_list = new TreeMap<>(); + TreeMap countdown_list = new TreeMap<>(); + container_list.put(350, milestone_350_container); countdown_list.put(350, milestone_350_countdown); + container_list.put(300, milestone_300_container); countdown_list.put(300, milestone_300_countdown); + container_list.put(250, milestone_250_container); countdown_list.put(250, milestone_250_countdown); + container_list.put(200, milestone_200_container); countdown_list.put(200, milestone_200_countdown); + container_list.put(150, milestone_150_container); countdown_list.put(150, milestone_150_countdown); + container_list.put(100, milestone_100_container); countdown_list.put(100, milestone_100_countdown); + container_list.put(75, milestone_75_container); countdown_list.put(75, milestone_75_countdown); + container_list.put(50, milestone_50_container); countdown_list.put(50, milestone_50_countdown); + container_list.put(30, milestone_30_container); countdown_list.put(30, milestone_30_countdown); + container_list.put(15, milestone_15_container); countdown_list.put(15, milestone_15_countdown); + container_list.put(7, milestone_7_container); countdown_list.put(7, milestone_7_countdown); + container_list.put(3, milestone_3_container); countdown_list.put(3, milestone_3_countdown); + container_list.put(1, milestone_1_container); countdown_list.put(1, milestone_1_countdown); + container_list.put(0, milestone_next_birthday_container); countdown_list.put(0, milestone_next_birthday_countdown); + + + // CLEAR IF BIRTH DATE NOT SET YET // + if(birthdate[0] <= -1 || birthdate[1] <= -1 || birthdate[2] <= -1){ + // last and next birthday + milestone_last_birthday_age.setText(format_ordinal(0)); + milestone_next_birthday_age.setText(format_ordinal(1)); + + // elements + for(Integer milestone : container_list.descendingKeySet()){ + // get views + LinearLayout container = container_list.get(milestone); + TextView countdown = countdown_list.get(milestone); + assert container != null; + assert countdown != null; + + // clear + container.setBackgroundColor(color_gray_dark); + countdown.setText(""); + } + + // ignore the rest + return; + } + + + // GET VALUES // + // find next birthday + Birthday birthday = Birthday.find_next(birthdate); + + // get millis left + double millis_left = birthday.millis_left(); + + // convert to days + int date_diff_days = (int)Math.floor(millis_left / DateUtils.DAY_IN_MILLIS); + + // make diff really big if you're unborn + if(birthday.age() <= 0){ + date_diff_days = 999; + } + + // get ages + int next_birthday_age = Math.max(1, birthday.age()); + int last_birthday_age = next_birthday_age - 1; + + + // FILL IN LAST AND NEXT BIRTHDAY // + // last + milestone_last_birthday_age.setText(format_ordinal(last_birthday_age)); + if(birthday.age() > 0){ + milestone_last_birthday_container.setBackgroundColor(color_theme); + milestone_last_birthday_countdown.setText(R.string.milestone_completed); + + } else { + milestone_last_birthday_container.setBackgroundColor(color_theme_dark); + milestone_last_birthday_countdown.setText(""); + } + + // next + milestone_next_birthday_age.setText(format_ordinal(next_birthday_age)); + + + // CHECK ALL // + boolean found_next = false; + for(Integer milestone : container_list.descendingKeySet()){ + // get views + LinearLayout container = container_list.get(milestone); + TextView countdown = countdown_list.get(milestone); + assert container != null; + assert countdown != null; + + // not born yet? + if(birthday.age() <= 0){ + container.setBackgroundColor(color_gray_dark); + countdown.setText(""); + continue; + } + + // completed? + if(date_diff_days < milestone){ + container.setBackgroundColor(color_theme); + countdown.setText(R.string.milestone_completed); + continue; + } + + // next to complete? + if(!found_next){ + container.setBackgroundColor(color_theme_dark); + + String[] countdown_string = format_countdown(millis_left - (DateUtils.DAY_IN_MILLIS * milestone), false); + String[] significant = get_most_significant_segment(countdown_string); + countdown.setText(String.format(milestone_mini_countdown, significant[0], significant[1])); + + found_next = true; + continue; + } + + // default + container.setBackgroundColor(color_gray_dark); + countdown.setText(""); + } + } + + /* + TOOL: Convert millis to displayable days, hours and minutes + */ + private String[] format_countdown(double millis_left){ + return format_countdown(millis_left, true); + } + private String[] format_countdown(double millis_left, boolean fill_up_with_zeroes){ + // GET VALUES // + // make up for the missing seconds display + millis_left += DateUtils.MINUTE_IN_MILLIS; + + // days + int date_diff_days = (int)Math.floor(millis_left / DateUtils.DAY_IN_MILLIS); + millis_left -= (date_diff_days * DateUtils.DAY_IN_MILLIS); + + // hours + int date_diff_hours = (int)Math.floor(millis_left / DateUtils.HOUR_IN_MILLIS); + millis_left -= (date_diff_hours * DateUtils.HOUR_IN_MILLIS); + + // minutes + int date_diff_minutes = (int)Math.floor(millis_left / DateUtils.MINUTE_IN_MILLIS); + + + // GET TEXTS // + // days + String countdown_segment_days = Integer.toString(date_diff_days); + + // hours + String countdown_segment_hours = ""; + if(fill_up_with_zeroes && date_diff_hours < 10){ + countdown_segment_hours += "0"; + } + countdown_segment_hours += Integer.toString(date_diff_hours); + + // minutes + String countdown_segment_minutes = ""; + if(fill_up_with_zeroes && date_diff_minutes < 10){ + countdown_segment_minutes += "0"; + } + countdown_segment_minutes += Integer.toString(date_diff_minutes); + + + // RETURN // + return new String[]{countdown_segment_days, countdown_segment_hours, countdown_segment_minutes}; + } + + /* + TOOL: Get most significant nonnull countdown segment + */ + private String[] get_most_significant_segment(String[] countdown){ + if(countdown[0].replace("0", "").length() > 0) { + return new String[]{countdown[0], "d"}; + } + if(countdown[1].replace("0", "").length() > 0){ + return new String[]{countdown[1], "h"}; + } + + return new String[]{countdown[2], "m"}; + } + + /* + TOOL: Format number as ordinal string + */ + private static String format_ordinal(int number){ + String[] suffix_list = new String[]{"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"}; + switch(number % 100){ + case 11: + case 12: + case 13: + return number + "th"; + default: + return number + suffix_list[number % 10]; + } + } +} \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..beea3fe --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,863 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/icon.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/icon.xml new file mode 100755 index 0000000..3a8ac85 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-anydpi-v26/icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/icon_round.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/icon_round.xml new file mode 100755 index 0000000..3a8ac85 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-anydpi-v26/icon_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-hdpi/icon.png b/android-app/app/src/main/res/mipmap-hdpi/icon.png new file mode 100755 index 0000000..bc9ed0f Binary files /dev/null and b/android-app/app/src/main/res/mipmap-hdpi/icon.png differ diff --git a/android-app/app/src/main/res/mipmap-hdpi/icon_foreground.png b/android-app/app/src/main/res/mipmap-hdpi/icon_foreground.png new file mode 100755 index 0000000..394c002 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-hdpi/icon_foreground.png differ diff --git a/android-app/app/src/main/res/mipmap-hdpi/icon_round.png b/android-app/app/src/main/res/mipmap-hdpi/icon_round.png new file mode 100755 index 0000000..c0ffd0e Binary files /dev/null and b/android-app/app/src/main/res/mipmap-hdpi/icon_round.png differ diff --git a/android-app/app/src/main/res/mipmap-mdpi/icon.png b/android-app/app/src/main/res/mipmap-mdpi/icon.png new file mode 100755 index 0000000..b9c220a Binary files /dev/null and b/android-app/app/src/main/res/mipmap-mdpi/icon.png differ diff --git a/android-app/app/src/main/res/mipmap-mdpi/icon_foreground.png b/android-app/app/src/main/res/mipmap-mdpi/icon_foreground.png new file mode 100755 index 0000000..41d69a2 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-mdpi/icon_foreground.png differ diff --git a/android-app/app/src/main/res/mipmap-mdpi/icon_round.png b/android-app/app/src/main/res/mipmap-mdpi/icon_round.png new file mode 100755 index 0000000..60cca4d Binary files /dev/null and b/android-app/app/src/main/res/mipmap-mdpi/icon_round.png differ diff --git a/android-app/app/src/main/res/mipmap-xhdpi/icon.png b/android-app/app/src/main/res/mipmap-xhdpi/icon.png new file mode 100755 index 0000000..e17d985 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xhdpi/icon.png differ diff --git a/android-app/app/src/main/res/mipmap-xhdpi/icon_foreground.png b/android-app/app/src/main/res/mipmap-xhdpi/icon_foreground.png new file mode 100755 index 0000000..f109409 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xhdpi/icon_foreground.png differ diff --git a/android-app/app/src/main/res/mipmap-xhdpi/icon_round.png b/android-app/app/src/main/res/mipmap-xhdpi/icon_round.png new file mode 100755 index 0000000..ce7c5ca Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xhdpi/icon_round.png differ diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/icon.png b/android-app/app/src/main/res/mipmap-xxhdpi/icon.png new file mode 100755 index 0000000..d5d11a6 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxhdpi/icon.png differ diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/icon_foreground.png b/android-app/app/src/main/res/mipmap-xxhdpi/icon_foreground.png new file mode 100755 index 0000000..5f24e17 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxhdpi/icon_foreground.png differ diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/icon_round.png b/android-app/app/src/main/res/mipmap-xxhdpi/icon_round.png new file mode 100755 index 0000000..2931e13 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxhdpi/icon_round.png differ diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/icon.png b/android-app/app/src/main/res/mipmap-xxxhdpi/icon.png new file mode 100755 index 0000000..42cad6b Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxxhdpi/icon.png differ diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/icon_foreground.png b/android-app/app/src/main/res/mipmap-xxxhdpi/icon_foreground.png new file mode 100755 index 0000000..3a5d5d9 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxxhdpi/icon_foreground.png differ diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/icon_round.png b/android-app/app/src/main/res/mipmap-xxxhdpi/icon_round.png new file mode 100755 index 0000000..d996bde Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxxhdpi/icon_round.png differ diff --git a/android-app/app/src/main/res/values/colors.xml b/android-app/app/src/main/res/values/colors.xml new file mode 100755 index 0000000..f01f681 --- /dev/null +++ b/android-app/app/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #F4AC64 + #D9822B + #87531F + #FF000000 + #333333 + #FFFFFFFF + \ No newline at end of file diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml new file mode 100755 index 0000000..0d86a6e --- /dev/null +++ b/android-app/app/src/main/res/values/strings.xml @@ -0,0 +1,36 @@ + + Birthday Countdown + App icon + > Select Date of Birth < + + 000 + d + 00 + h + 00 + m + + Milestones + birthday + days + day + in %1$s %2$s + completed + 350 + 300 + 250 + 200 + 150 + 100 + 75 + 50 + 30 + 15 + 7 + 3 + 1 + + About + Can\'t await your next birthday? Me neither! So I built this Countdown App for you ^^\n\n~ Kim <33 + Birthday Countdown Website + \ No newline at end of file diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml new file mode 100755 index 0000000..ce4c2d0 --- /dev/null +++ b/android-app/app/src/main/res/values/themes.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/test/java/de/drmaxnix/birthdaycountdown/ExampleUnitTest.java b/android-app/app/src/test/java/de/drmaxnix/birthdaycountdown/ExampleUnitTest.java new file mode 100755 index 0000000..af1c46b --- /dev/null +++ b/android-app/app/src/test/java/de/drmaxnix/birthdaycountdown/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package de.drmaxnix.birthdaycountdown; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/android-app/build.gradle b/android-app/build.gradle new file mode 100755 index 0000000..905d3bd --- /dev/null +++ b/android-app/build.gradle @@ -0,0 +1,9 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.1.3' apply false + id 'com.android.library' version '7.1.3' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/android-app/gradle.properties b/android-app/gradle.properties new file mode 100755 index 0000000..dab7c28 --- /dev/null +++ b/android-app/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/android-app/gradle/wrapper/gradle-wrapper.jar b/android-app/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..e708b1c Binary files /dev/null and b/android-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000..207661d --- /dev/null +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Feb 12 14:55:04 CET 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/android-app/gradlew b/android-app/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/android-app/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/android-app/gradlew.bat b/android-app/gradlew.bat new file mode 100755 index 0000000..ac1b06f --- /dev/null +++ b/android-app/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android-app/settings.gradle b/android-app/settings.gradle new file mode 100755 index 0000000..8551b94 --- /dev/null +++ b/android-app/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Birthday Countdown" +include ':app'