77 Commits
Author SHA1 Message Date
me f0cd128025 main, take some suggestions given by Android Studio. 2026-11-06 14:31:07 -07:00
me b7a419eeab main, fix some silliness of agent using full class path. 2026-11-06 14:30:34 -07:00
me 4ede74e7a2 Agent's attempt to ensure the sound gets played every interval (adding Looper to Handler and moving release() from playSound to onDestroy) as well as ensuring it's not delayed (using play() rather than start()). 2026-11-06 13:35:08 -07:00
me 124e1bc211 Fix some of the agent's code, mediaplayer not defined in TimerForegroundService so maybe it was intended for Main, and PowerManager's WakeLock tag wanted to know the app name with a colon for logging. 2026-11-03 13:34:31 -07:00
me 1b9f40c002 Update plugin versions. 2026-11-03 13:32:39 -07:00
me 262aa439db Model's attempt to fix: 1) Only the first itnerval making a sound, all subsequent countdowns being silent. 2) App opening up directly to the Alarms & Reminders screen to get permissions approved without any prompting the user on what is going on. 2026-11-03 13:16:32 -07:00
me 9f1bfe8940 gradle, add but comment changes recommended by local model. 2026-11-01 06:27:18 -07:00
me 1099dec4e6 main, fix NullPointerException. 2026-11-01 06:22:48 -07:00
me 54418801c5 main, add missing import. 2026-10-28 17:36:01 -07:00
me 875d143076 More fixes trying to get the new changes to work. 2026-10-23 08:37:53 -07:00
me 595cffce78 main, add missing or deleted import. 2026-10-21 03:00:15 -07:00
me 3965aa6d88 Attempt to get app to work properly when in the background in newer versions of Android, with assistance from qwen3.6:35b. Testing TBD. 2026-10-20 18:39:07 -07:00
me 1786f03577 changelog, add header for 1.1.2. 2026-10-02 07:20:41 -07:00
me 5b991a5225 main, fix ending bracket. 2026-10-02 07:20:29 -07:00
me 54a0c53c1b Add CHANGELOG. 2026-10-02 07:02:19 -07:00
me 72a77eeef2 alert, begin adding changes to have a foreground service. 2026-10-02 06:54:52 -07:00
me 4e474f3fce Merge pull request '1.1.2' (#5) from dev into main
Reviewed-on: #5
2026-09-28 17:50:35 -07:00
me c40f6c0166 icon, change legacy to be a circle again. 2026-09-28 17:15:40 -07:00
me edb38612b5 Take all the yellow highlight suggestions regarding versions, besides sticking with SDK 36. 2026-09-28 17:15:26 -07:00
me 38eda443dc icon, change from webp to png to support legacy AOSP version. 2026-09-28 17:01:24 -07:00
me 51f8e67799 AGP Upgrade Assistant 2026-09-28 17:00:50 -07:00
me 26bcc3e496 manifest, update release file size enhancers. 2026-09-28 16:45:52 -07:00
me 92173b1cae manifest, increment version. 2026-09-28 16:41:56 -07:00
me bb78aa0a9a icon, update so that the background is white on non-monochrome rather than green grid. 2026-09-28 16:36:38 -07:00
me e4ab9c91c8 Save monochrome art changes. 2026-09-28 16:32:26 -07:00
me fa7926ca6e manifest, use the new icon. 2026-09-28 16:27:52 -07:00
me deb6369f37 Import the icon art. 2026-09-28 16:27:40 -07:00
me d6e952eae9 Update icon art. 2026-09-28 16:27:20 -07:00
me 398f9d8729 README, remove update notes, do them on the releases page. 2026-09-28 16:06:39 -07:00
me ef0292bdd9 fdroid, update email to be specific to infinite timer, 2026-09-28 15:45:31 -07:00
me 904aaba2b5 Add upcoming change to README while also refactoring. 2026-08-29 15:07:44 -07:00
me d4a5329bf6 Move the app-specific links below the app details. 2026-07-04 14:55:09 -07:00
me 251b3cdc0c fdroid metadata, CurrentVersionCode is fine with no value, so try removing the field entirely. 2026-07-04 12:51:19 -07:00
me cf1c40f5ce fdroid metadata, see what happens if we remove the version code. 2026-07-04 12:48:40 -07:00
me a4b69958e0 fdroid manifest requires an integer for the CurrentVersionCode. 2026-07-04 12:41:46 -07:00
me 596b13c7fd Test what happens when fdroid CurrentVersionCode is not the provided value. 2026-07-04 12:35:51 -07:00
me 6f7f85b90f Add trailing slashes back to urls in fdroid manifest. 2026-07-04 12:32:28 -07:00
me b629473129 Only provide the username for Liberapay. 2026-07-04 12:09:07 -07:00
me d36ceaa19f Fix Liberapay URL. 2026-07-04 11:55:04 -07:00
me ab746003b8 Add additional metadata for F-Droid. 2026-07-04 11:41:21 -07:00
me 27ed2afbf0 Update issue tracker to be proper HTTP link, use contact page. Change website page to go directly to the app. 2026-07-04 10:47:15 -07:00
me bd312edf83 Merge pull request 'Add manifest file for fdroid server.' (#4) from dev into main
Reviewed-on: #4
2026-07-04 10:19:02 -07:00
me b2167423db Add manifest file for fdroid server. 2026-07-03 17:34:45 -07:00
me 7e6c0b90e2 Merge pull request 'Volume Seekbar Fixes' (#3) from dev into main
Reviewed-on: #3
2025-08-17 08:49:15 -07:00
me 84c75f6be7 No need to call something in onCreate which is called in onResume. 2025-08-17 08:41:29 -07:00
me 1b3ca8c8af Only change the seekbar if it differs so that the volume sidebar does not show up on every open. 2025-08-17 08:40:56 -07:00
me f83a8b41e4 Ensure volume bar is accurate if the app is minimized and the volume is modified before the app is reopened. 2025-08-17 08:35:13 -07:00
me bc433b4185 Correctly add a space between "Volume" and the status. 2025-08-17 08:33:00 -07:00
me 34b13473c9 Show the article URL for proper accreditation. 2025-07-19 15:54:00 -07:00
me a642b51532 Increment version for future development. 2025-07-19 15:52:02 -07:00
me a5490b98c7 Hide release files (APKs and JSON) from Git. 2025-07-19 15:51:05 -07:00
me 140350012b Fix URL. 2025-07-19 15:50:40 -07:00
me d610bfd40b Merge pull request 'v1.1.0 - Update Targeted Version + Enhancements + Bugfixes' (#1) from dev into main
# Enhancements
- Target Android 36 and update theme so that the app may be used properly on current and future versions of Android.
- Volume slider for easy viewing and changing of current settings.
- Add a default sound for devices lacking notification sounds.
- Add a rounded icon.

# Bugfixes
- App opening with Start already pressed.
- Large intervals (hundreds of hours) causing odd behavior.
2025-07-19 15:25:33 -07:00
me b1c2dfaa6f Add a Spanish string resource. 2025-07-19 15:00:47 -07:00
me e79ee82193 Add textview for volume control. 2025-07-19 15:00:26 -07:00
me 4f64ca49b7 Add a seek bar tied to media volume. 2025-07-13 19:31:15 -07:00
me 25969dd6d6 Fix the issue of a device not having any sounds to play by including a basic backup chime. 2025-07-13 18:38:17 -07:00
me 1fe3035050 Add a rounded icon and move icons to mipmap from drawable. 2025-07-13 18:37:09 -07:00
me 219ef6d64e Add more debug output regarding the prevention of large numbers. Reinstate missing assignment of longLoopInterval. 2025-07-13 12:47:27 -07:00
me 854f5df595 Set the versions to the newest SDK. Dock the minumum by 1 to match Carb Up. 2025-07-13 10:32:50 -07:00
me dbb1af03c4 Finish fixing the large numbers bug by making 99:59:59 the maximum value allowed. Enhance the logging around resetting the StartStop key after a crash. 2025-07-13 10:32:19 -07:00
me 37e7df98af Update all debug log text. Fix bugs related to Start/Stop button. Start working on TODO for large value bug. 2025-07-12 15:43:47 -07:00
me 9bb729d5c2 Remove test from version name. 2025-07-12 15:31:07 -07:00
me 03380cf023 Putting things into place to fix a bug when the app is destroyed while the timer was running. 2025-07-12 13:18:52 -07:00
me 8da02333df Fix minute section displayin the hour's worth of minutes. 2025-07-12 12:39:11 -07:00
me 926fe17919 Remove unused imports. 2025-07-12 12:38:20 -07:00
me fb007debad Change theme to DayNight rather than hardcoded to Light. Doesn't change much right now. 2025-07-12 12:38:08 -07:00
me 23772cedcb Remove the temporary app name header and add a margin for the true action bar. 2025-07-12 12:30:44 -07:00
me e02abc19f8 Remove the temporary action bar hide. 2025-07-12 12:30:12 -07:00
me fc6ff2e5cc Begin working on UI fixes. 2025-07-11 14:53:09 -07:00
me 4cd8641ec2 Update version. 2025-07-11 14:52:47 -07:00
me 15ff7fa1cf Clean up extra lines. 2025-07-11 14:52:37 -07:00
me a3fe0d4a2e Delete deleteme's. 2025-07-11 14:30:40 -07:00
me f055efed02 Project is now compiling and running on a test device successfully. 2025-07-11 14:30:15 -07:00
me 9446f427c3 Add link to a helpful upgrading article. 2025-07-11 13:38:53 -07:00
me c57cdd0a35 Update gradle and android:gradle configurations so that the project builds successfully. 2025-07-11 13:38:27 -07:00
me 058e854606 Ensure app can be installed by future Android versions. 2025-07-11 13:13:04 -07:00
63 changed files with 908 additions and 171 deletions

No files matched your search

+2
View File
@@ -33,3 +33,5 @@ google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
# Release Files
app/release/*
+42
View File
@@ -0,0 +1,42 @@
# Changelog
## 1.2.0 (In Development)
**Add Foreground Service**
- Allow the app to stay in the foreground so that it does not get killed while running.
## 1.1.2 (Sep 28, 2026)
**Fix App Icon**
- Fix and enhance the icon for all versions of Android. Tested to work properly from SDK 16 to 36.
- Changed a few compile time options to help optimize the file size.
- Updated Gradle, AGP, and a few other internals.
## 1.1.1 (Aug 17, 2025)
**Volume Seekbar Fixes**
- Correctly add a space via Java and not Strings.xml.
- Ensure volume seekbar is accurate after app is resumed.
## 1.1.0 (Jul 19, 2025)
**Enhancements**
- Target Android 36 and update theme so that the app may be used properly on current and future versions of Android.
- Volume slider for easy viewing and changing of current settings.
- Add a default sound for devices lacking notification sounds.
- Add a rounded icon.
**Bugfixes**
- App opening with Start already pressed.
- Large intervals (hundreds of hours) causing odd behavior.
## 1.0.6 (Jan 4, 2025)
**Final Production Release**
The release APK available from the Play Store and my website for the last few years.
+13 -1
View File
@@ -1,3 +1,15 @@
# android-infinite-timer # Infinite Timer
Timer which loops over the specified interval, playing your default notification sound each time it hits 0. Timer which loops over the specified interval, playing your default notification sound each time it hits 0.
## Notes
### 2025-07-11
This article was very helpful in getting this app updated from SDK 25 to 36.
- https://sijus.medium.com/resurrecting-a-5-year-old-android-app-a-developers-journey-59d8f5689e5b
## Licenses
Chime sound byte came free from here:
- https://pixabay.com/sound-effects/chime-sound-7143/
+10 -9
View File
@@ -1,20 +1,21 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 28 namespace "com.hyperling.apps.infinitetimer"
buildToolsVersion '28.0.3' compileSdkVersion 37
defaultConfig { defaultConfig {
applicationId "com.hyperling.apps.infinitetimer" applicationId "com.hyperling.apps.infinitetimer"
minSdkVersion 15 minSdkVersion 14
targetSdkVersion 28 targetSdkVersion 37
versionCode 7 versionCode 9
versionName "1.06" versionName "1.1.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
} }
@@ -25,7 +26,7 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
}) })
// https://mvnrepository.com/artifact/com.android.support/appcompat-v7 // https://mvnrepository.com/artifact/com.android.support/appcompat-v7
implementation group: 'com.android.support', name: 'appcompat-v7', version: '28.0.0' implementation "com.android.support:appcompat-v7:28.0.0"
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.2'
} }
@@ -1,26 +0,0 @@
package com.hyperling.apps.infinitetimer.deleteme;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("infinitetimer.apps.hyperling.com.infinitetimer", appContext.getPackageName());
}
}
+23 -6
View File
@@ -1,21 +1,38 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.hyperling.apps.infinitetimer">
<!-- 🔊 Required to play sound while backgrounded & bypass Doze -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 requires declaring foreground service types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- ⏰ Android 12+ exact alarm permission (users can deny, code handles it gracefully) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/icon_512" android:icon="@mipmap/ic_launcher"
android:label="@string/appName" android:label="@string/appName"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/InfiniteTimer" android:theme="@style/InfiniteTimer"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"> android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize">
<activity android:name=".MainActivity">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application>
<!-- 🔔 Register the receiver that triggers the alarm response -->
<receiver android:name=".TimerReceiver"
android:exported="false" />
<!-- 🎵 Transient service to play audio & hold WakeLock -->
<service android:name=".TimerForegroundService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
</application>
</manifest> </manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,85 @@
package com.hyperling.apps.infinitetimer;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Settings;
import android.util.Log;
public class Alert {
private static final String TAG = "Alert";
public static final String ACTION_TIMER = "com.hyperling.apps.infinitetimer.ACTION_TIMER";
/**
* Sets a persistent alarm that triggers the TimerReceiver.
* Uses setExactAndAllowWhileIdle to bypass Android Doze mode.
*/
public static void start(Context context, long intervalMs) {
Intent intent = new Intent(context, TimerReceiver.class);
intent.setAction(ACTION_TIMER);
// FLAG_IMMUTABLE required for Android 12+ (API 31+)
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_CANCEL_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
long triggerTime = System.currentTimeMillis() + intervalMs;
// Exact alarm ensures it triggers even in Doze mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
}
Log.d(TAG, "Alarm set for " + intervalMs + "ms at " + triggerTime);
} else {
Log.e(TAG, "Could not get AlarmManager service");
}
}
/**
* Cancels the timer. If the app is killed, alarms are dropped automatically.
*/
public static void stop(Context context) {
Intent intent = new Intent(context, TimerReceiver.class);
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_CANCEL_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null) {
alarmManager.cancel(pendingIntent);
Log.d(TAG, "Alarm cancelled");
}
}
/**
* Checks/Requests permission for exact alarms on Android 12+
*/
public static boolean isExactAlarmAllowed(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
return am != null && am.canScheduleExactAlarms();
}
return true; // Android 11 and below allow exact alarms by default
}
/**
* Helper to request exact alarm permission if missing
*/
public static void requestExactAlarmPermission(android.app.Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isExactAlarmAllowed(activity)) {
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
activity.startActivity(intent);
}
}
}
@@ -1,20 +1,29 @@
package com.hyperling.apps.infinitetimer; package com.hyperling.apps.infinitetimer;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.media.AudioManager;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.media.RingtoneManager; import android.media.Ringtone;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.ViewManager;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.TableRow; import android.widget.TableRow;
import android.widget.TextView; import android.widget.TextView;
@@ -22,7 +31,7 @@ import java.util.Map;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
String TAG, String TAG = "MainActivity.",
keySharedPreferences, keyDebug, keyChronometerTime, keyLoopInterval, keySharedPreferences, keyDebug, keyChronometerTime, keyLoopInterval,
keyStartStop, keyAppOpen, keyServiceRunning, keyStartStop, keyAppOpen, keyServiceRunning,
stringChronometerDefault, stringChronometerTime, stringLoopInterval; stringChronometerDefault, stringChronometerTime, stringLoopInterval;
@@ -45,10 +54,23 @@ public class MainActivity extends AppCompatActivity {
TextView tvDot; TextView tvDot;
Uri ringtoneUri; Uri ringtoneUri;
// RingtoneManager reference for consistent notification sound playback across loops
private android.media.Ringtone notificationRingtone;
// For proper sound lifecycle management across timer loops
private Ringtone ringtonePlayer;
MediaPlayer mediaPlayer; MediaPlayer mediaPlayer;
SeekBar seekBar;
AudioManager audioManager;
TextView tvSeekBar;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
String tag = "onCreate";
if (DEBUG) Log.d(tag, "Starting");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
@@ -56,7 +78,6 @@ public class MainActivity extends AppCompatActivity {
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
/***** Get Strings *****/ /***** Get Strings *****/
TAG = getString(R.string.TAG);
keySharedPreferences = getString(R.string.keySharedPreferences); keySharedPreferences = getString(R.string.keySharedPreferences);
keyDebug = getString(R.string.keyDebug); keyDebug = getString(R.string.keyDebug);
keyChronometerTime = getString(R.string.keyChronometerTime); keyChronometerTime = getString(R.string.keyChronometerTime);
@@ -67,36 +88,54 @@ public class MainActivity extends AppCompatActivity {
stringChronometerDefault = getString(R.string.chronometerDefault); stringChronometerDefault = getString(R.string.chronometerDefault);
if (DEBUG) Log.d(tag, "Got strings.");
/***** Shared Preferences *****/ /***** Shared Preferences *****/
sharedPreferences = getSharedPreferences(keySharedPreferences, MODE_PRIVATE); sharedPreferences = getSharedPreferences(keySharedPreferences, MODE_PRIVATE);
// TODO: Comment these lines! DEBUG = sharedPreferences.getBoolean(keyDebug, true);
//sharedPreferences.edit().putBoolean(keyDebug, true).apply();
//sharedPreferences.edit().putBoolean(keyStartStop, false).apply();
//sharedPreferences.edit().putString(keyLoopInterval, stringChronometerDefault).apply();
//sharedPreferences.edit().putString(keyChronometerTime, stringChronometerDefault).apply();
// TODO: Keep these!
DEBUG = sharedPreferences.getBoolean(keyDebug, false);
appOpen = sharedPreferences.getBoolean(keyAppOpen, false); appOpen = sharedPreferences.getBoolean(keyAppOpen, false);
serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false); serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false);
/* if (DEBUG) Log.d(tag, "Got preferences.");
if (!appOpen && !serviceRunning) {
sharedPreferences.edit().putBoolean(keyStartStop, false).apply();
}
*/
// Sound // Sound
ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); // Check exact alarm permission for Android 12+
mediaPlayer = MediaPlayer.create(this, ringtoneUri); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !Alert.isExactAlarmAllowed(this)) {
if (DEBUG) Log.w(tag, "Exact alarm permission not granted, requesting...");
if (DEBUG) Log.d(TAG, "MainActivity.onCreate! Got strings."); new AlertDialog.Builder(MainActivity.this)
.setTitle("Alarm Permission Required")
.setMessage("To ensure the timer alerts you reliably, please grant 'Alarms & Reminders' permission in system settings.")
.setPositiveButton("Grant Permission", (d, w) -> {
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
startActivity(intent);
})
.setNegativeButton("Not Now", (d, w) -> d.dismiss())
.setCancelable(false)
.show();
}
if (DEBUG){ if (DEBUG){
// Print all preferences // Print all preferences
Map<String, ?> sharedPreferencesAll = sharedPreferences.getAll(); Map<String, ?> sharedPreferencesAll = sharedPreferences.getAll();
for (String key : sharedPreferencesAll.keySet()){ for (String key : sharedPreferencesAll.keySet()){
Log.d(TAG, "MainActivity.onCreate! key=" + key + " value=" + sharedPreferencesAll.get(key)); String value = sharedPreferencesAll.get(key).toString();
Log.d(tag, "key=" + key + " value=" + value);
// If app crashed, ensure buttons are correct.
if (key.equals(keyStartStop) && value.equals("true")) {
if (DEBUG) Log.d(tag, "Shutting off improper START value");
// Ensure START is always off if we are creating (not resuming).
int count = 0;
while (!sharedPreferences.edit().putBoolean(keyStartStop, false).commit() && count < 50) {
if (DEBUG) Log.d(tag, "Commit failed, trying again. count=" + count);
count++;
}
boolean checkStartStop = sharedPreferences.getBoolean(keyStartStop, false);;
Log.d(tag, "key=" + key + " value=" + checkStartStop);
}
} }
} }
@@ -150,21 +189,49 @@ public class MainActivity extends AppCompatActivity {
etMillis.setVisibility(View.GONE); etMillis.setVisibility(View.GONE);
//(ViewManager) etMillis.getParent().remove //(ViewManager) etMillis.getParent().remove
//sharedPreferences.edit().putBoolean(keyStartStop, false).apply(); seekBar = findViewById(R.id.seekBar);
recoverScreen(); setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (DEBUG) Log.d(TAG, "MainActivity.onCreate! Finished."); audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
seekBar.setMax(maxVolume);
tvSeekBar = findViewById(R.id.tvSeekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, i, AudioManager.FLAG_SHOW_UI);
syncVolume();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
syncVolume();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
syncVolume();
}
});
if (DEBUG) Log.d(tag, "Finished");
} }
private void doTimeChooser(){ private void doTimeChooser(){
if (DEBUG) Log.d(TAG, "MainActivity.openTimeChooser!"); String tag = TAG + "doTimeChooser";
if (DEBUG) Log.d(tag, "Starting");
//stringLoopInterval = "00:00:07"; //stringLoopInterval = "00:00:07";
//setEditTexts(stringLoopInterval); //setEditTexts(stringLoopInterval);
if (DEBUG) Log.d(tag, "Finished");
} }
private void setEditTexts(String interval){ private void setEditTexts(String interval){
if (DEBUG) Log.d(TAG, "MainActivity.setEditTexts! interval=" + interval); String tag = TAG + "setEditTexts";
if (DEBUG) Log.d(tag, "Starting, interval=" + interval);
if (!interval.equals(stringChronometerDefault)) { if (!interval.equals(stringChronometerDefault)) {
String[] time = interval.split(":"); String[] time = interval.split(":");
@@ -188,29 +255,67 @@ public class MainActivity extends AppCompatActivity {
} }
setLoopInterval(); setLoopInterval();
if (DEBUG) Log.d(tag, "Finished");
} }
private void setLoopInterval(){ private void setLoopInterval(){
if (DEBUG) Log.d(TAG, "MainActivity.setLoopInterval!"); String tag = TAG + "setLoopInterval";
if (DEBUG) Log.d(tag, "Starting");
int hours = Integer.parseInt("0"+etHours.getText().toString()); int hours = 0, minutes = 0, seconds = 0;
int minutes = Integer.parseInt("0"+etMinutes.getText().toString()); try {
int seconds = Integer.parseInt("0"+etSeconds.getText().toString()); hours = Integer.parseInt("0" + etHours.getText().toString());
} catch (Exception e) {
if (DEBUG) Log.d(tag, "Hours had an exception, using highest value");
hours = 99;
}
try {
minutes = Integer.parseInt("0" + etMinutes.getText().toString());
} catch (Exception e) {
if (DEBUG) Log.d(tag, "Minutes had an exception, using highest value");
minutes = 59;
}
try {
seconds = Integer.parseInt("0" + etSeconds.getText().toString());
} catch (Exception e) {
if (DEBUG) Log.d(tag, "Seconds had an exception, using highest value");
seconds = 59;
}
if ((hours + (minutes/60) + (seconds/60/60)) > 99) {
if (DEBUG) Log.d(tag, "Sum is over 99 hours, setting to 99:59:59");
hours = 99;
minutes = 59;
seconds = 59;
}
if (DEBUG) Log.d(tag, "hours=" + hours);
if (DEBUG) Log.d(tag, "minutes=" + minutes);
if (DEBUG) Log.d(tag, "seconds=" + seconds);
if (hours > 0) etHours.setText("" + hours);
if (minutes > 0) etMinutes.setText("" + minutes);
if (seconds > 0) etSeconds.setText("" + seconds);
longLoopInterval = (((hours*60*60) + (minutes*60) + seconds) * 1000); longLoopInterval = (((hours*60*60) + (minutes*60) + seconds) * 1000);
if (DEBUG) Log.d(TAG, "MainActivity.setLoopInterval! longLoopInterval=" + longLoopInterval); if (DEBUG) Log.d(tag, "longLoopInterval=" + longLoopInterval);
setChronometer(longLoopInterval); setChronometer(longLoopInterval);
if (DEBUG) Log.d(tag, "Finished");
} }
private void setChronometer(long milliseconds){ private void setChronometer(long milliseconds){
if (DEBUG) Log.d(TAG, "MainActivity.setChronometer! milliseconds=" + milliseconds); String tag = TAG + "setChronometer";
if (DEBUG) Log.d(tag, "Starting, milliseconds=" + milliseconds);
//chronometer = (TextView) findViewById(R.id.chronometer); //chronometer = (TextView) findViewById(R.id.chronometer);
String[] time = {Long.toString(milliseconds/(1000*60*60)), String[] time = {
Long.toString((milliseconds/(1000*60)%(60*60))), Long.toString(milliseconds/(1000*60*60)),
Long.toString((milliseconds/(1000))%60)}; Long.toString((milliseconds/(1000*60))%60),
Long.toString((milliseconds/(1000))%60)
};
for (int i = 0; i < time.length; i++){ for (int i = 0; i < time.length; i++){
while (time[i].length() < 2){ while (time[i].length() < 2){
@@ -229,16 +334,18 @@ public class MainActivity extends AppCompatActivity {
doTimeChooser(); doTimeChooser();
} }
});*/ });*/
if (DEBUG) Log.d(TAG, "MainActivity.setChronometer! display=" + display); if (DEBUG) Log.d(tag, "display=" + display);
sharedPreferences.edit().putString(keyChronometerTime, display).apply(); sharedPreferences.edit().putString(keyChronometerTime, display).apply();
if (DEBUG) Log.d(TAG, "MainActivity.setChronometer! Before=" + chronometer.getText()); if (DEBUG) Log.d(tag, "Before=" + chronometer.getText());
chronometer.setText(display); chronometer.setText(display);
if (DEBUG) Log.d(TAG, "MainActivity.setChronometer! After=" + chronometer.getText()); if (DEBUG) Log.d(tag, "After=" + chronometer.getText());
if (DEBUG) Log.d(tag, "Finished");
} }
private void startStopChronometer(String buttonText){ private void startStopChronometer(String buttonText){
if (DEBUG) Log.d(TAG, "MainActivity.startStopChronometer!"); String tag = TAG + "startStopChronometer";
if (DEBUG) Log.d(tag, "Starting");
start = !start; start = !start;
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = sharedPreferences.edit();
@@ -255,6 +362,20 @@ public class MainActivity extends AppCompatActivity {
setLoopInterval(); setLoopInterval();
resetTimeLeft(); resetTimeLeft();
setStartTime(); setStartTime();
// Start the background alert service using Alert manager
long interval = longLoopInterval > 1000 ? longLoopInterval : 60000; // Minimum 1 minute interval for alarms
if (Alert.isExactAlarmAllowed(this)) {
Alert.start(this, interval);
} else {
if (DEBUG) Log.w(tag, "Cannot start alert: Exact alarm permission denied.");
new AlertDialog.Builder(MainActivity.this)
.setTitle("Permission Required")
.setMessage("Please grant 'All files access' or 'Alarm' permissions in system settings for this feature to work.")
.setPositiveButton("OK", (d, w) -> d.dismiss())
.show();
}
} }
else if (buttonText.equals(getString(R.string.btnPause))){ else if (buttonText.equals(getString(R.string.btnPause))){
btnStartStop.setText(getString(R.string.btnResume)); btnStartStop.setText(getString(R.string.btnResume));
@@ -274,10 +395,13 @@ public class MainActivity extends AppCompatActivity {
flipStartStopButton(); flipStartStopButton();
flipResetPauseButton(); flipResetPauseButton();
if (DEBUG) Log.d(tag, "Finished");
} }
private void recoverScreen(){ private void recoverScreen(){
if (DEBUG) Log.d(TAG, "MainActivity.recoverScreen!"); String tag = TAG + "recoverScreen";
if (DEBUG) Log.d(tag, "Starting");
/***** Retrieve the last settings *****/ /***** Retrieve the last settings *****/
// Chronometer // Chronometer
@@ -290,6 +414,7 @@ public class MainActivity extends AppCompatActivity {
// Start/Stop Button // Start/Stop Button
start = sharedPreferences.getBoolean(keyStartStop, false); start = sharedPreferences.getBoolean(keyStartStop, false);
if (DEBUG) Log.d(tag, "start=" + start);
flipStartStopButton(); flipStartStopButton();
flipResetPauseButton(); flipResetPauseButton();
@@ -301,10 +426,13 @@ public class MainActivity extends AppCompatActivity {
//startStopChronometer(stringChronometerTime); //startStopChronometer(stringChronometerTime);
//incrementTime.sendEmptyMessage(0); //incrementTime.sendEmptyMessage(0);
if (DEBUG) Log.d(tag, "Finished");
} }
private void flipStartStopButton(){ private void flipStartStopButton(){
if (DEBUG) Log.d(TAG, "MainActivity.flipStartStopButton!"); String tag = TAG + "flipStartStopButton";
if (DEBUG) Log.d(tag, "Starting");
start = sharedPreferences.getBoolean(keyStartStop, false); start = sharedPreferences.getBoolean(keyStartStop, false);
serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false); serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false);
@@ -319,10 +447,13 @@ public class MainActivity extends AppCompatActivity {
} }
btnStartStop.setBackgroundColor(Color.GREEN); btnStartStop.setBackgroundColor(Color.GREEN);
} }
if (DEBUG) Log.d(tag, "Finished");
} }
private void resetPauseChronometer(){ private void resetPauseChronometer(){
if (DEBUG) Log.d(TAG, "MainActivity.resetPauseChronometer!"); String tag = TAG + "resetPauseChronometer";
if (DEBUG) Log.d(tag, "Starting");
start = sharedPreferences.getBoolean(keyStartStop, false); start = sharedPreferences.getBoolean(keyStartStop, false);
serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false); serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false);
@@ -333,10 +464,13 @@ public class MainActivity extends AppCompatActivity {
else{ else{
setEditTexts(stringChronometerDefault); setEditTexts(stringChronometerDefault);
} }
if (DEBUG) Log.d(tag, "Finished");
} }
private void flipResetPauseButton(){ private void flipResetPauseButton(){
if (DEBUG) Log.d(TAG, "MainActivity.flipResetPauseButton!"); String tag = TAG + "flipResetPauseButton";
if (DEBUG) Log.d(tag, "Starting");
start = sharedPreferences.getBoolean(keyStartStop, false); start = sharedPreferences.getBoolean(keyStartStop, false);
serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false); serviceRunning = sharedPreferences.getBoolean(keyServiceRunning, false);
@@ -349,13 +483,17 @@ public class MainActivity extends AppCompatActivity {
btnResetPause.setText(getString(R.string.btnReset)); btnResetPause.setText(getString(R.string.btnReset));
btnResetPause.setBackgroundColor(Color.RED); btnResetPause.setBackgroundColor(Color.RED);
} }
if (DEBUG) Log.d(tag, "Finished");
} }
private Handler incrementTime = new Handler(){ private final Handler incrementTime = new Handler(Looper.getMainLooper()){
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(@NonNull Message msg) {
String tag = TAG + "handleMessage";
if (DEBUG) Log.d(tag, "Starting");
super.handleMessage(msg); super.handleMessage(msg);
if (DEBUG) Log.d(TAG, "MainActivity.incrementTime.handleMessage!");
appOpen = sharedPreferences.getBoolean(keyAppOpen, true); appOpen = sharedPreferences.getBoolean(keyAppOpen, true);
if (appOpen) { if (appOpen) {
@@ -372,7 +510,7 @@ public class MainActivity extends AppCompatActivity {
// Calculate new time and text // Calculate new time and text
setTimeLeft(); setTimeLeft();
if (DEBUG) Log.d(TAG, "MainActivity.incrementTime.handleMessage! longTimeLeft=" + longTimeLeft); if (DEBUG) Log.d(tag, "longTimeLeft=" + longTimeLeft);
// Check if we need to beep // Check if we need to beep
if (longTimeLeft < 1000){ if (longTimeLeft < 1000){
@@ -381,12 +519,12 @@ public class MainActivity extends AppCompatActivity {
setStartTime(); setStartTime();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C longLoopInterval=" + longLoopInterval); Log.d(tag, "longLoopInterval=" + longLoopInterval);
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C longStartTime=" + longStartTime); Log.d(tag, "longStartTime=" + longStartTime);
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C longEndTime=" + longEndTime); Log.d(tag, "longEndTime=" + longEndTime);
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C longTimeLeft=" + longTimeLeft); Log.d(tag, "longTimeLeft=" + longTimeLeft);
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C waitInterval=" + waitInterval); Log.d(tag, "waitInterval=" + waitInterval);
Log.d(TAG, "MainActivity.incrementTime.handleMessage!C start=" + start); Log.d(tag, "start=" + start);
//chronometer.setText("Test"); //chronometer.setText("Test");
//setChronometer(200); //setChronometer(200);
/* /*
@@ -397,81 +535,200 @@ public class MainActivity extends AppCompatActivity {
} }
// Wait // Wait
Runnable task = new Runnable() { Thread t = wait(tag);
@Override
public void run() {
synchronized (this){
try{
wait(waitInterval);
if (DEBUG) Log.d(TAG, "MainActivity.incrementTime.handleMessage! Done waiting.");
}
catch (Exception e){
e.printStackTrace();
if (DEBUG) Log.d(TAG, "MainActivity.incrementTime.handleMessage! Failed to wait.");
}
}
//sharedPreferences.edit().putBoolean(keyStartStop, true).apply();
incrementTime.sendEmptyMessage(0);
//startStopChronometer("Start");
}
};
Thread t = new Thread(task);
t.start(); t.start();
} }
if (DEBUG) Log.d(tag, "Finished");
}
@NonNull
private Thread wait(String tag) {
Runnable task = new Runnable() {
@Override
public void run() {
synchronized (this){
try{
wait(waitInterval);
if (DEBUG) Log.d(tag, "Done waiting.");
}
catch (Exception e){
e.printStackTrace();
if (DEBUG) Log.d(tag, "Failed to wait.");
}
}
//sharedPreferences.edit().putBoolean(keyStartStop, true).apply();
incrementTime.sendEmptyMessage(0);
//startStopChronometer("Start");
}
};
Thread t = new Thread(task);
return t;
} }
}; };
private void setStartTime(){ private void setStartTime(){
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime!"); String tag = TAG + "setStartTime";
if (DEBUG) Log.d(tag, "Starting");
if (DEBUG) Log.d(tag, "System.currentTimeMillis()=" + System.currentTimeMillis());
if (longTimeLeft == 0) { if (longTimeLeft == 0) {
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longTimeLeft==" + longTimeLeft); if (DEBUG) Log.d(tag, "longTimeLeft=" + longTimeLeft);
// Add extra 1000 so timer starts on interval and beeps after 1 // Add extra 1000 so timer starts on interval and beeps after 1
longStartTime = System.currentTimeMillis() + 1000 - waitInterval; longStartTime = System.currentTimeMillis() + 1000 - waitInterval;
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longStartTime=" + longStartTime); if (DEBUG) Log.d(tag, "longStartTime=" + longStartTime);
longEndTime = longStartTime + longLoopInterval; longEndTime = longStartTime + longLoopInterval;
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longEndTime=" + longEndTime); if (DEBUG) Log.d(tag, "longEndTime=" + longEndTime);
} } else {
else{ if (DEBUG) Log.d(tag, "longTimeLeft=" + longTimeLeft);
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longTimeLeft==" + longTimeLeft);
longStartTime = System.currentTimeMillis() - waitInterval; longStartTime = System.currentTimeMillis() - waitInterval;
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longStartTime=" + longStartTime); if (DEBUG) Log.d(tag, "longStartTime=" + longStartTime);
longEndTime = longStartTime + longTimeLeft; longEndTime = longStartTime + longTimeLeft;
if (DEBUG) Log.d(TAG, "MainActivity.setStartTime! longEndTime=" + longEndTime); if (DEBUG) Log.d(tag, "longEndTime=" + longEndTime);
} }
if (DEBUG) Log.d(tag, "Finished");
} }
private void setTimeLeft(){ private void setTimeLeft(){
if (DEBUG) Log.d(TAG, "MainActivity.setTimeLeft!"); String tag = TAG + "setTimeLeft";
if (DEBUG) Log.d(tag, "Starting");
longTimeLeft = longEndTime - System.currentTimeMillis() + waitInterval; longTimeLeft = longEndTime - System.currentTimeMillis() + waitInterval;
if (DEBUG) Log.d(TAG, "MainActivity.setTimeLeft! longTimeLeft=" + longTimeLeft); if (DEBUG) Log.d(tag, "longTimeLeft=" + longTimeLeft);
setChronometer(longTimeLeft); setChronometer(longTimeLeft);
if (DEBUG) Log.d(tag, "Finished");
} }
private void resetTimeLeft(){ private void resetTimeLeft(){
if (DEBUG) Log.d(TAG, "MainActivity.resetTimeLeft!"); String tag = TAG + "resetTimeLeft";
if (DEBUG) Log.d(tag, "Starting");
longTimeLeft = 0; longTimeLeft = 0;
setChronometer(longLoopInterval); setChronometer(longLoopInterval);
if (DEBUG) Log.d(tag, "Finished");
} }
private void playSound(){ private void playSound(){
if (DEBUG) Log.d(TAG, "MainActivity.playSound!"); String tag = TAG + "playSound";
if (DEBUG) Log.d(tag, "Starting");
/* Original code, did not work on Nexus 7 with CM 12.1-20151117 (Android 5.1.1) try {
Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri); // Stop any existing notification ringtone first to prevent overlap
ringtone.play(); if (notificationRingtone != null && notificationRingtone.isPlaying()) {
*/ notificationRingtone.stop();
}
//mediaPlayer.reset(); // Use RingtoneManager which handles async playback lifecycle properly
mediaPlayer.start(); Uri uri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION);
notificationRingtone = android.media.RingtoneManager.getRingtone(this, uri);
if (notificationRingtone != null) {
// Ringtone.play() is asynchronous and handles its own lifecycle,
// avoiding the MediaPlayer.create() + immediate release bug that
// caused silence on subsequent loops
notificationRingtone.play();
} else {
// Fallback: if getRingtone fails, try direct MediaPlayer with proper lifecycle
MediaPlayer localPlayer = MediaPlayer.create(this, uri);
if (localPlayer != null) {
localPlayer.start();
final MediaPlayer fp = localPlayer;
// Release after playback completes (use delay as a safety net)
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(
new Runnable() { @Override public void run() {
if (fp.isPlaying()) fp.stop();
fp.release();
}}, 2000
);
}
}
} catch (Exception e) {
sharedPreferences.edit().putBoolean(keyStartStop, false).apply();
String message = "Failed to play a notification sound.";
message += " Please screenshot this error and send it to me@hyperling.com:\n\n";
message += e.toString();
new AlertDialog.Builder(MainActivity.this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("Exit", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
})
.setCancelable(false)
.show()
;
}
if (DEBUG) Log.d(tag, "Finished");
}
private void flipInputTexts() {
String tag = TAG + "flipInputTexts";
if (DEBUG) Log.d(tag, "Starting");
etHours.setEnabled(!start);
etMinutes.setEnabled(!start);
etSeconds.setEnabled(!start);
if (DEBUG) Log.d(tag, "Finished");
}
private void resetAll() {
String tag = TAG + "resetAll";
if (DEBUG) Log.d(tag, "Starting");
// Stop the background alert service when resetting
Alert.stop(this);
if (DEBUG) Log.d(tag, "Resetting screen values...");
setChronometer(0);
setEditTexts("");
setLoopInterval();
if (DEBUG) Log.d(tag, "Values reset");
if (DEBUG) Log.d(tag, "Resetting SharedPreferences...");
start = false;
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(keyStartStop, start);
editor.putBoolean(keyAppOpen, start);
editor.putBoolean(keyServiceRunning, start);
int count = 0;
while (!editor.commit() && count < 50) {
Log.i(tag, "Failed! Reattempting commit... count=" + count);
count++;
};
if (DEBUG) Log.d(tag, "SharedPreferences reset");
if (DEBUG) Log.d(tag, "Finished");
}
public void syncVolume() {
String tag = TAG + "syncVolume";
if (DEBUG) Log.d(tag, "Starting");
int currVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (currVolume != seekBar.getProgress()) {
seekBar.setProgress(currVolume);
}
String volume = getString(R.string.tvSeekBar) + " " + seekBar.getProgress() + "/" + seekBar.getMax();
tvSeekBar.setText(volume);
if (DEBUG) Log.d(tag, "Finished");
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); String tag = TAG + "onPause";
if (DEBUG) Log.d(TAG, "MainActivity.onPause!"); if (DEBUG) Log.d(tag, "Starting");
stringLoopInterval = stringLoopInterval =
etHours.getText().toString() + ":" + etHours.getText().toString() + ":" +
@@ -483,22 +740,59 @@ public class MainActivity extends AppCompatActivity {
editor.putString(keyLoopInterval, stringLoopInterval); editor.putString(keyLoopInterval, stringLoopInterval);
editor.putBoolean(keyAppOpen, false); editor.putBoolean(keyAppOpen, false);
editor.apply(); editor.apply();
if (DEBUG) Log.d(tag, "Done with local code, calling super.");
super.onPause();
if (DEBUG) Log.d(tag, "Finished");
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (DEBUG) Log.d(TAG, "MainActivity.onResume!");
String tag = TAG + "onResume";
if (DEBUG) Log.d(tag, "Starting");
sharedPreferences.edit().putBoolean(keyServiceRunning, false).apply(); sharedPreferences.edit().putBoolean(keyServiceRunning, false).apply();
recoverScreen(); recoverScreen();
syncVolume();
if (DEBUG) Log.d(tag, "Finished");
} }
private void flipInputTexts() { @Override
etHours.setEnabled(!start); protected void onDestroy() {
etMinutes.setEnabled(!start); String tag = TAG + "onDestroy";
etSeconds.setEnabled(!start); if (DEBUG) Log.d(tag, "Starting");
// Stop the background alert service when destroying activity
Alert.stop(this);
if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; }
// Clean up ringtone player to prevent resource leak across config changes
if (notificationRingtone != null && notificationRingtone.isPlaying()) {
notificationRingtone.stop();
}
if (DEBUG) Log.d(tag, "Done with local code, calling super..");
super.onDestroy();
if (DEBUG) Log.d(tag, "Finished");
} }
// TODO: This is somehow always 1 action behind the actual volume when pressing volume buttons.
// Like when doing down+up+down, the bar does nothing+down+up.
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
boolean bool = super.onKeyDown(keyCode, event);
syncVolume();
return bool;
}
return super.onKeyDown(keyCode, event);
}
} }
@@ -0,0 +1,95 @@
package com.hyperling.apps.infinitetimer;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
public class TimerForegroundService extends Service {
private static final String TAG = "TimerFGService";
private static final String CHANNEL_ID = "timer_notification_channel";
private static final int NOTIFICATION_ID = 1001;
private PowerManager.WakeLock wakeLock;
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
// Android 8+ requires foreground service to play sound reliably while backgrounded
startForeground(NOTIFICATION_ID, buildNotification());
// Acquire a wake lock briefly to ensure the CPU stays awake during playback
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "InfiniteTimer:TimerWakeLock");
wakeLock.acquire(60 * 1000L);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
playSound();
return START_NOT_STICKY; // Don't restart immediately if killed
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Timer Notifications", NotificationManager.IMPORTANCE_HIGH);
// Disable default sound to avoid double-sound on some OEMs
channel.setSound(null, null);
channel.enableVibration(false);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) manager.createNotificationChannel(channel);
}
}
private android.app.Notification buildNotification() {
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Infinite Timer")
.setContentText("Timer is active...")
// Use system alarm icon temporarily. Add your own ic_stat_notification if desired
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setOngoing(true)
.build();
}
private void playSound() {
try {
Uri uri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION);
Ringtone r = RingtoneManager.getRingtone(this, uri);
if (r != null) {
r.play();
// Allow time for sound to buffer and play before stopping
new android.os.Handler().postDelayed(new Runnable() {
@Override
public void run() {
stopSelf();
}
}, 1000);
} else {
stopSelf();
}
} catch (Exception e) {
Log.e(TAG, "Failed to play sound", e);
stopSelf();
}
}
@Override
public IBinder onBind(Intent intent) { return null; }
@Override
public void onDestroy() {
super.onDestroy();
if (wakeLock.isHeld()) wakeLock.release();
}
}
@@ -0,0 +1,20 @@
package com.hyperling.apps.infinitetimer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class TimerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() != null && Alert.ACTION_TIMER.equals(intent.getAction())) {
// Android 8+ requires foreground service to play sound reliably while backgrounded
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(new Intent(context, TimerForegroundService.class));
} else {
context.startService(new Intent(context, TimerForegroundService.class));
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
+24
View File
@@ -4,6 +4,7 @@
android:id="@+id/activity_main" android:id="@+id/activity_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="?android:actionBarSize"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
@@ -157,4 +158,27 @@
</TableLayout> </TableLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tblButtons"
android:layout_alignParentBottom="true">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/activity_vertical_margin"
android:layout_centerInParent="true"
android:indeterminate="false"/>
<TextView
android:id="@+id/tvSeekbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/seekBar"
android:layout_centerHorizontal="true"
android:text="@string/tvSeekBar"
android:textSize="14sp" />
</RelativeLayout>
</RelativeLayout> </RelativeLayout>
+1 -3
View File
@@ -5,20 +5,18 @@
android:id="@+id/action_milis" android:id="@+id/action_milis"
android:orderInCategory="100" android:orderInCategory="100"
android:title="@string/action_millis" android:title="@string/action_millis"
app:showAsAction="never"
android:checkable="true" /> android:checkable="true" />
<item <item
android:id="@+id/action_enable_ads" android:id="@+id/action_enable_ads"
android:orderInCategory="150" android:orderInCategory="150"
android:title="@string/action_enable_ads" android:title="@string/action_enable_ads"
app:showAsAction="never"
android:checkable="true" /> android:checkable="true" />
<item <item
android:id="@+id/action_exit" android:id="@+id/action_exit"
android:orderInCategory="500" android:orderInCategory="500"
android:title="@string/action_exit" android:title="@string/action_exit"
app:showAsAction="never" /> />
</menu> </menu>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
<resources>
<!-- App details -->
<string name="appName">Infinite Timer</string>
<string name="appVersion">v20250711</string>
<string name="TAG">com.hyperling.apps.infinitetimer</string>
<!-- Keys -->
<string name="keySharedPreferences">InfiniteTimer</string>
<string name="keyDebug">DEBUG</string>
<string name="keyChronometerTime">ChronometerTime</string>
<string name="keyLoopInterval">LoopInterval</string>
<string name="keyStartStop">StartStop</string>
<string name="keyAppOpen">AppOpen</string>
<string name="keyServiceRunning">ServiceRunning</string>
<!-- UI Text -->
<string name="btnStart">Empezar</string>
<string name="btnStop">Terminar</string>
<string name="btnChooseTime">Elige un Tiempo</string>
<string name="btnReset">Reiniciar</string>
<string name="btnPause">Pausa</string>
<string name="btnResume">Seguir</string>
<string name="chronometerDefault">00:00:00</string>
<string name="tvHoursHint">Horas</string>
<string name="tvMinutesHint">Minutos</string>
<string name="tvSecondsHint">Segundos</string>
<string name="tvMillisHint">Millis</string>
<string name="tvDot">.</string>
<string name="tvUpperLabel"><b>Tiempo entre notificaciones:</b></string>
<string name="tvLowerLabel"><b>Cuenta regresiva hasta notificación:</b></string>
<string name="action_millis">Enable Millisecs</string>
<string name="action_enable_ads">Enable Ads</string>
<string name="action_exit">Exit</string>
<string name="tvSeekBar">Volumen: </string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
+3 -1
View File
@@ -1,7 +1,7 @@
<resources> <resources>
<!-- App details --> <!-- App details -->
<string name="appName">Infinite Timer</string> <string name="appName">Infinite Timer</string>
<string name="appVersion">Test v201610291024</string> <string name="appVersion">v20250711</string>
<string name="TAG">com.hyperling.apps.infinitetimer</string> <string name="TAG">com.hyperling.apps.infinitetimer</string>
<!-- Keys --> <!-- Keys -->
@@ -35,4 +35,6 @@
<string name="action_millis">Enable Millisecs</string> <string name="action_millis">Enable Millisecs</string>
<string name="action_enable_ads">Enable Ads</string> <string name="action_enable_ads">Enable Ads</string>
<string name="action_exit">Exit</string> <string name="action_exit">Exit</string>
<string name="tvSeekBar">Media Volume:</string>
</resources> </resources>
+2 -2
View File
@@ -1,7 +1,7 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
@@ -9,7 +9,7 @@
</style> </style>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="InfiniteTimer" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="InfiniteTimer" parent="Theme.AppCompat.DayNight.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
@@ -1,17 +0,0 @@
package com.hyperling.apps.infinitetimer.deleteme;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
+7 -4
View File
@@ -2,7 +2,10 @@
buildscript { buildscript {
repositories { repositories {
jcenter() mavenCentral()
maven {
url 'https://jitpack.io'
}
google() google()
maven { maven {
url 'https://maven.google.com/' url 'https://maven.google.com/'
@@ -10,7 +13,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.1' classpath 'com.android.tools.build:gradle:9.5.0'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@@ -19,11 +22,11 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
jcenter() mavenCentral()
google() google()
} }
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
+16
View File
@@ -0,0 +1,16 @@
AuthorName: 'Chad Michael "Hyperling" Greenwood'
AuthorEmail: 'me+infinite@hyperling.com'
AuthorWebSite: 'https://hyperling.com/about/'
Donate: 'https://hyperling.com/donate/'
Liberapay: 'HyperVegan'
Bitcoin: 'bc1qsfe8dkvry3d34kztz449gkq67wq8fu2nkgfkh0'
Litecoin: 'ltc1qavmpu5d6ljntxsd6jj548m4yys83zwscl0dzkx'
IssueTracker: 'https://hyperling.com/contact/'
Categories:
- fdroid
- hyperling
- utilities
Name: Infinite Timer
Summary: 'Play a notification at your chosen interval.'
SourceCode: 'https://git.hyperling.com/me/android-infinite-timer/'
WebSite: 'https://hyperling.com/apps/#timer'
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+24 -1
View File
@@ -9,9 +9,32 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.builtInKotlin=false
android.defaults.buildfeatures.resvalues=true
android.dependency.useConstraints=true
android.enableAppCompileTimeRClass=false
android.newDsl=false
android.r8.optimizedResourceShrinking=false
android.r8.strictFullModeForKeepRules=false
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.uniquePackageNames=false
android.useAndroidX=false
android.usesSdkInManifest.disallowed=false
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx1536m
### Changes recommended by qwen3.6:35b when DNS was failing to foojay.
##org.gradle.jvmargs=-Xmx1536m -Dorg.gradle.jvm.download.auto.install=false
##org.gradle.java.home=/usr/lib/jvm/java-21-openjdk
##
### Toolchain & Vendor Workarounds for missing foojay.io DNS resolution
##org.gradle.java.installations.paths=/usr/lib/jvm/java-21-openjdk
##kotlin.compiler.ignoreJdkVendor=true
### Suppress AGP deprecation warnings
##android.sync.suppressAgpWarnings=UNSUPPORTED_PROJECT_OPTION_USE
##android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
##android.sync.suppressAgpWarnings=LIBRARY_CONSTRAINTS_SHOULD_BE_DISABLED
##kotlin.compiler.execution.strategy=in-process
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec-decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
+13
View File
@@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Jan 10 06:37:42 CST 2019 #Fri Jul 11 12:55:34 MST 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+3
View File
@@ -1 +1,4 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
include ':app' include ':app'