├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── dubrowgn
│ │ └── microtimer
│ │ ├── AlarmReceiver.kt
│ │ ├── Dec6Duration.kt
│ │ ├── MainActivity.kt
│ │ ├── RoTimeControl.kt
│ │ ├── TimerControl.kt
│ │ ├── Vibration.kt
│ │ ├── db
│ │ ├── Alarm.kt
│ │ ├── AlarmDao.kt
│ │ ├── Database.kt
│ │ └── Dec6DurationConverter.kt
│ │ └── ui
│ │ └── ext
│ │ ├── ImageView.kt
│ │ ├── MarginLayout.kt
│ │ ├── TextView.kt
│ │ └── View.kt
│ └── res
│ ├── drawable
│ ├── btn_background.xml
│ ├── btn_selector.xml
│ ├── ic_launcher_background.xml
│ ├── ic_launcher_foreground.xml
│ ├── ico_backspace.xml
│ ├── ico_clear.xml
│ ├── ico_delete.xml
│ ├── ico_pause.xml
│ ├── ico_play.xml
│ └── ico_play_28.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── resources.xml
│ ├── strings.xml
│ └── themes.xml
├── build.gradle.kts
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 1.txt
│ ├── 2.txt
│ ├── 3.txt
│ ├── 4.txt
│ ├── 5.txt
│ ├── 6.txt
│ ├── 7.txt
│ ├── 8.txt
│ └── 9.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ └── main.png
│ └── short_description.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── readme
└── main.png
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.apk
2 | *.iml
3 |
4 | .DS_Store
5 | .gradle
6 | .idea
7 |
8 | /app/debug
9 | /app/release
10 | /build
11 | /captures
12 | .externalNativeBuild
13 | .cxx
14 | local.properties
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dustin Brown
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Micro Timer
2 |
3 | *Micro Timer* is a tiny, microwave-inspired timer app for Android. Instead of inventing yet another clever, pretty, tedious method of inputing time, *Micro Timer* uses the tried-and-true 10-key pad commonly found on microwave ovens.
4 |
5 | The idea was born when I realized I tended to use the kitchen microwave timer because the stock Android timer was such a pain to use. Not to mention it always seemed to change/break for no aparent reason between Android versions. I thought to myself, "Wouldn't it be nice if I could have my microwave timer with me wherever I went without having to drag around a microwave?"
6 |
7 | [
](https://f-droid.org/packages/dubrowgn.microtimer/)
10 |
11 | Or download the latest APK from the [Releases Section](https://github.com/dubrowgn/micro-timer/releases/latest).
12 |
13 | 
14 |
15 | ## Requirements
16 |
17 | * Android 9.0+
18 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("com.google.devtools.ksp")
4 | id("org.jetbrains.kotlin.android")
5 | }
6 |
7 | android {
8 | compileSdk = 34
9 |
10 | defaultConfig {
11 | applicationId = "dubrowgn.microtimer"
12 | minSdk = 28 // Handler.postDelayed()
13 | // work around unused library resources
14 | resourceConfigurations.addAll(listOf("anydpi", "en"))
15 | targetSdk = 34
16 | versionCode = 9
17 | versionName = "1.9"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | isMinifyEnabled = true
23 | isShrinkResources = true
24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility = JavaVersion.VERSION_17
29 | targetCompatibility = JavaVersion.VERSION_17
30 | }
31 | kotlinOptions {
32 | jvmTarget = "17"
33 | }
34 | namespace = "dubrowgn.microtimer"
35 | }
36 |
37 | dependencies {
38 | val roomVersion = "2.6.1"
39 | implementation("androidx.room:room-runtime:$roomVersion")
40 | annotationProcessor("androidx.room:room-compiler:$roomVersion")
41 | ksp("androidx.room:room-compiler:$roomVersion")
42 |
43 | implementation("androidx.core:core-ktx:1.12.0")
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dubrowgn/micro-timer/9d05e0cbddb737706377311e27560e6e6905d2cb/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/dubrowgn/microtimer/AlarmReceiver.kt:
--------------------------------------------------------------------------------
1 | package dubrowgn.microtimer
2 |
3 | import android.app.Notification
4 | import android.app.NotificationManager
5 | import android.app.PendingIntent
6 | import android.content.BroadcastReceiver
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.graphics.Color
10 | import android.util.Log
11 |
12 | class AlarmReceiver : BroadcastReceiver() {
13 | private fun debug(msg: String) {
14 | Log.d(this::class.java.name, msg)
15 | }
16 |
17 | private fun error(msg: String) {
18 | Log.e(this::class.java.name, msg)
19 | }
20 |
21 | override fun onReceive(context: Context?, intent: Intent?) {
22 | debug("onReceive()")
23 |
24 | if (context == null) {
25 | error("context is null")
26 | return
27 | }
28 |
29 | if (intent == null) {
30 | error("intent is null")
31 | return
32 | }
33 |
34 | Vibration.start(context)
35 |
36 | val noteMgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
37 | val id = intent.getLongExtra("id", -1)
38 | val name = intent.getStringExtra("name")
39 |
40 | val noteIntent = PendingIntent.getActivity(
41 | context,
42 | 0,
43 | Intent(context, MainActivity::class.java).apply {
44 | flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
45 | },
46 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
47 | )
48 |
49 | val builder = Notification.Builder(context, alarmNoteChannel)
50 | .setAutoCancel(true)
51 | .setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
52 | .setContentTitle(context.getString(R.string.app_name))
53 | .setContentText("Timer for $name Expired!")
54 | .setCategory(Notification.CATEGORY_ALARM)
55 | .setVisibility(Notification.VISIBILITY_PUBLIC)
56 | .setColor(Color.RED)
57 | .setFullScreenIntent(noteIntent, true)
58 |
59 | noteMgr.notify(id.toInt(), builder.build())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/dubrowgn/microtimer/Dec6Duration.kt:
--------------------------------------------------------------------------------
1 | package dubrowgn.microtimer
2 |
3 | class Dec6Duration(var digits: UInt) {
4 |
5 | constructor() : this(0u)
6 |
7 | val isZero: Boolean
8 | get() = digits == 0u
9 |
10 | val seconds: UInt
11 | get() = digits % 100u
12 |
13 | val minutes: UInt
14 | get() = digits / 100u % 100u
15 |
16 | val hours: UInt
17 | get() = digits / 10000u % 100u
18 |
19 | val totalSeconds: UInt
20 | get() = hours * 60u * 60u + minutes * 60u + seconds
21 |
22 | fun clone(): Dec6Duration {
23 | return Dec6Duration(digits)
24 | }
25 |
26 | fun popDigit(): Dec6Duration {
27 | digits /= 10u;
28 | return this
29 | }
30 |
31 | fun pushDigit(digit: UInt): Dec6Duration {
32 | digits *= 10u
33 | digits += digit
34 | digits %= 1000000u
35 |
36 | return this
37 | }
38 |
39 | fun subtractSeconds(rSeconds: UInt): Dec6Duration {
40 | if (rSeconds >= totalSeconds) {
41 | digits = 0u
42 | return this
43 | }
44 |
45 | val ls = seconds
46 | val lm = minutes
47 | val lh = hours
48 |
49 | val rs = rSeconds % 60u
50 | val rm = rSeconds % 3600u / 60u
51 | val rh = rSeconds / 3600u
52 |
53 | val bm = if (rs > ls) 1u else 0u
54 | val s = bm * 60u + ls - rs
55 |
56 | val bh = if (rm + bm > lm) 1u else 0u
57 | val m = bh * 60u + lm - bm - rm
58 |
59 | val h = lh - bh - rh
60 |
61 | digits = h * 10000u + m * 100u + s
62 |
63 | return this
64 | }
65 |
66 | override fun toString(): String {
67 | val padPart = { part: UInt -> part.toString().padStart(2, '0') }
68 |
69 | return "${padPart(hours)}:${padPart(minutes)}:${padPart(seconds)}"
70 | }
71 |
72 | fun zero(): Dec6Duration {
73 | digits = 0u
74 | return this
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/dubrowgn/microtimer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dubrowgn.microtimer
2 |
3 | import android.Manifest.permission
4 | import android.app.Activity
5 | import android.app.AlarmManager
6 | import android.app.KeyguardManager
7 | import android.app.NotificationChannel
8 | import android.app.NotificationManager
9 | import android.app.PendingIntent
10 | import android.content.Context
11 | import android.content.Intent
12 | import android.content.pm.PackageManager
13 | import android.net.Uri
14 | import android.os.Build
15 | import android.os.Bundle
16 | import android.os.Handler
17 | import android.os.Looper
18 | import android.os.PowerManager
19 | import android.os.SystemClock
20 | import android.provider.Settings
21 | import android.util.Log
22 | import android.widget.Button
23 | import android.widget.LinearLayout
24 | import androidx.core.view.children
25 | import androidx.room.Room
26 | import dubrowgn.microtimer.db.Alarm
27 | import dubrowgn.microtimer.db.AlarmDao
28 | import dubrowgn.microtimer.db.Database
29 |
30 | const val alarmNoteChannel = "micro-timer.alarm"
31 |
32 | class MainActivity : Activity() {
33 | private lateinit var alarmDao: AlarmDao
34 |
35 | private var duration: Dec6Duration = Dec6Duration()
36 | private val tickHandler: Handler = Handler(Looper.getMainLooper())
37 | private var expiredCount: Int = 0
38 |
39 | private lateinit var layoutAlarms: LinearLayout
40 | private lateinit var lblValue: RoTimeControl
41 |
42 | private val alarmMgr
43 | get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
44 |
45 | private val noteMgr
46 | get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
47 |
48 | private val powerMgr
49 | get() = getSystemService(Context.POWER_SERVICE) as PowerManager
50 |
51 | private fun debug(msg: String) {
52 | Log.d(this::class.java.name, msg)
53 | }
54 | private fun error(msg: String) {
55 | Log.e(this::class.java.name, msg)
56 | }
57 |
58 | override fun onCreate(savedInstanceState: Bundle?) {
59 | debug("onCreate()")
60 |
61 | super.onCreate(savedInstanceState)
62 |
63 | initDb()
64 | initPerms()
65 | initNotes()
66 | initUi()
67 |
68 | allowFullscreen()
69 |
70 | loadAlarms()
71 | }
72 |
73 | override fun onDestroy() {
74 | debug("onDestroy()")
75 |
76 | super.onDestroy()
77 | }
78 |
79 | private fun initDb() {
80 | alarmDao = Room.databaseBuilder(applicationContext, Database::class.java, "app-data")
81 | .allowMainThreadQueries()
82 | .build()
83 | .AlarmDao()
84 | }
85 |
86 | private fun allowFullscreen() {
87 | setShowWhenLocked(true)
88 | setTurnScreenOn(true)
89 |
90 | val keyguardMgr = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
91 | keyguardMgr.requestDismissKeyguard(this, null)
92 | }
93 |
94 | private fun initNotes() {
95 | noteMgr.createNotificationChannel(
96 | NotificationChannel(
97 | alarmNoteChannel,
98 | "Alarm Expired",
99 | NotificationManager.IMPORTANCE_HIGH
100 | ).apply {
101 | description = "Sound and visual indicator"
102 | }
103 | )
104 | }
105 |
106 | private fun initPerms() {
107 | val pkgUri = Uri.parse("package:$packageName")
108 |
109 | if(Build.VERSION.SDK_INT < 33) {
110 | debug("not applicable: POST_NOTIFICATIONS")
111 | } else if (checkSelfPermission(permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
112 | debug("granted: POST_NOTIFICATIONS")
113 | } else {
114 | debug("requesting: POST_NOTIFICATIONS")
115 | requestPermissions(arrayOf(permission.POST_NOTIFICATIONS), 0)
116 | }
117 |
118 | if (!powerMgr.isIgnoringBatteryOptimizations(packageName)) {
119 | debug("requesting: IGNORE_BATTERY_OPTIMIZATIONS")
120 | startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, pkgUri))
121 | } else {
122 | debug("granted: IGNORE_BATTERY_OPTIMIZATIONS")
123 | }
124 |
125 | if (Build.VERSION.SDK_INT < 31) {
126 | debug("not applicable: SCHEDULE_EXACT_ALARM")
127 | } else if (alarmMgr.canScheduleExactAlarms()) {
128 | debug("granted: SCHEDULE_EXACT_ALARM")
129 | } else {
130 | debug("requesting: SCHEDULE_EXACT_ALARM")
131 | startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, pkgUri))
132 | }
133 |
134 | if (Build.VERSION.SDK_INT < 34) {
135 | debug("not applicable: USE_FULL_SCREEN_INTENT")
136 | } else if (noteMgr.canUseFullScreenIntent()) {
137 | debug("granted: USE_FULL_SCREEN_INTENT")
138 | } else {
139 | debug("requesting: USE_FULL_SCREEN_INTENT")
140 | startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, pkgUri))
141 | }
142 | }
143 |
144 | private fun initUi() {
145 | setContentView(R.layout.activity_main)
146 |
147 | layoutAlarms = findViewById(R.id.layoutAlarms)
148 | lblValue = findViewById(R.id.lblInput)
149 |
150 | findViewById