Skip to content

Commit 6b03a2c

Browse files
authored
OboeTester: Use AlarmManager to schedule timeouts so they work in deep sleep (#2290)
1 parent 6e85089 commit 6b03a2c

9 files changed

Lines changed: 169 additions & 50 deletions

File tree

apps/OboeTester/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
implementation "androidx.core:core-ktx:1.9.0"
3939
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
4040
implementation 'androidx.appcompat:appcompat:1.6.1'
41+
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
4142

4243
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
4344
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

apps/OboeTester/app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
2929
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
3030
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
31+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
3132

3233
<application
3334
android:icon="@mipmap/ic_launcher"
@@ -177,6 +178,9 @@
177178
android:name="android.support.FILE_PROVIDER_PATHS"
178179
android:resource="@xml/provider_paths" />
179180
</provider>
181+
182+
<receiver android:name=".TestTimeoutReceiver" android:exported="false"/>
183+
180184
</application>
181185

182186
</manifest>

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -490,25 +490,16 @@ public void startTestUsingBundle() {
490490
mDrawAlwaysBox.setChecked(mDrawChartAlways);
491491
});
492492

493-
int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent);
494-
if (durationSeconds > 0) {
495-
// Schedule the end of the test.
496-
Handler handler = new Handler(Looper.getMainLooper()); // UI thread
497-
handler.postDelayed(new Runnable() {
498-
@Override
499-
public void run() {
500-
stopAutomaticTest();
501-
}
502-
}, durationSeconds * 1000);
503-
}
493+
504494
} catch (Exception e) {
505495
showErrorToast(e.getMessage());
506496
} finally {
507497
mBundleFromIntent = null;
508498
}
509499
}
510500

511-
void stopAutomaticTest() {
501+
@Override
502+
public void stopAutomaticTest() {
512503
String report = getCommonTestReport();
513504
AudioStreamBase outputStream =mAudioOutTester.getCurrentAudioStream();
514505
report += "out.xruns = " + outputStream.getXRunCount() + "\n";

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -199,23 +199,12 @@ public void startAudioTest() throws IOException {
199199
@Override
200200
public void startTestUsingBundle() {
201201
configureStreamsFromBundle(mBundleFromIntent);
202-
203-
int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent);
204202
int numBursts = mBundleFromIntent.getInt(KEY_BUFFER_BURSTS, VALUE_DEFAULT_BUFFER_BURSTS);
205203

206204
try {
207205
openStartAudioTestUI();
208206
int sizeFrames = mAudioOutTester.getCurrentAudioStream().getFramesPerBurst() * numBursts;
209207
mAudioOutTester.getCurrentAudioStream().setBufferSizeInFrames(sizeFrames);
210-
211-
// Schedule the end of the test.
212-
Handler handler = new Handler(Looper.getMainLooper()); // UI thread
213-
handler.postDelayed(new Runnable() {
214-
@Override
215-
public void run() {
216-
stopAutomaticTest();
217-
}
218-
}, durationSeconds * 1000);
219208
} catch (IOException e) {
220209
String report = "Open failed: " + e.getMessage();
221210
maybeWriteTestResult(report);
@@ -226,7 +215,8 @@ public void run() {
226215

227216
}
228217

229-
void stopAutomaticTest() {
218+
@Override
219+
public void stopAutomaticTest() {
230220
String report = getCommonTestReport()
231221
+ String.format(Locale.getDefault(), "tolerance = %5.3f\n", mTolerance)
232222
+ mLastGlitchReport;

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import static com.mobileer.oboetester.IntentBasedTestSupport.KEY_RESTART_STREAM_IF_CLOSED;
2222
import static com.mobileer.oboetester.StreamConfiguration.convertErrorToText;
2323

24+
import android.content.BroadcastReceiver;
2425
import android.content.Context;
2526
import android.content.Intent;
27+
import android.content.IntentFilter;
2628
import android.content.pm.PackageInfo;
2729
import android.content.pm.PackageManager;
2830
import android.content.pm.ServiceInfo;
@@ -44,6 +46,7 @@
4446

4547
import androidx.annotation.NonNull;
4648
import androidx.appcompat.app.AppCompatActivity;
49+
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
4750

4851
import java.io.File;
4952
import java.io.IOException;
@@ -120,6 +123,16 @@ abstract class TestAudioActivity extends AppCompatActivity {
120123
private String mTestResults;
121124
private ExternalFileWriter mExternalFileWriter = new ExternalFileWriter(this);
122125

126+
private TestTimeoutScheduler mTestTimeoutScheduler = new TestTimeoutScheduler();
127+
private BroadcastReceiver mStopTestReceiver = new BroadcastReceiver() {
128+
@Override
129+
public void onReceive(Context context, Intent intent) {
130+
if (mTestRunningByIntent) {
131+
stopAutomaticTest();
132+
}
133+
}
134+
};
135+
123136
public String getTestName() {
124137
return "TestAudio";
125138
}
@@ -318,6 +331,8 @@ protected void resetConfiguration() {
318331
@Override
319332
public void onResume() {
320333
super.onResume();
334+
LocalBroadcastManager.getInstance(this).registerReceiver(mStopTestReceiver,
335+
new IntentFilter(TestTimeoutReceiver.ACTION_STOP_TEST));
321336
if (mBundleFromIntent != null) {
322337
processBundleFromIntent();
323338
}
@@ -355,6 +370,12 @@ public void run() {
355370
mRestartStreamIfClosed = mBundleFromIntent.getBoolean(KEY_RESTART_STREAM_IF_CLOSED,
356371
false);
357372
setVolumeFromIntent();
373+
374+
int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent);
375+
if (durationSeconds > 0) {
376+
mTestTimeoutScheduler.scheduleTestTimeout(TestAudioActivity.this, durationSeconds);
377+
}
378+
358379
startTestUsingBundle();
359380
} catch( Exception e) {
360381
showErrorToast(e.getMessage());
@@ -365,9 +386,14 @@ public void run() {
365386
public void startTestUsingBundle() {
366387
}
367388

389+
public void stopAutomaticTest() {
390+
391+
}
392+
368393
@Override
369394
protected void onPause() {
370395
super.onPause();
396+
LocalBroadcastManager.getInstance(this).unregisterReceiver(mStopTestReceiver);
371397
}
372398

373399
@Override
@@ -870,6 +896,9 @@ public void saveIntentLog() {
870896

871897
// This should only be called from UI events such as onStop or a button press.
872898
public void onStopTest() {
899+
if (mTestRunningByIntent) {
900+
mTestTimeoutScheduler.cancelTestTimeout(this);
901+
}
873902
stopTest();
874903
}
875904

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -204,26 +204,15 @@ public void startTestUsingBundle() {
204204

205205
openAudio();
206206
startAudio();
207-
208-
int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent);
209-
if (durationSeconds > 0) {
210-
// Schedule the end of the test.
211-
Handler handler = new Handler(Looper.getMainLooper()); // UI thread
212-
handler.postDelayed(new Runnable() {
213-
@Override
214-
public void run() {
215-
stopAutomaticTest();
216-
}
217-
}, durationSeconds * 1000);
218-
}
219207
} catch (Exception e) {
220208
showErrorToast(e.getMessage());
221209
} finally {
222210
mBundleFromIntent = null;
223211
}
224212
}
225213

226-
void stopAutomaticTest() {
214+
@Override
215+
public void stopAutomaticTest() {
227216
String report = getCommonTestReport();
228217
stopAudio();
229218
maybeWriteTestResult(report);

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -321,26 +321,15 @@ public void startTestUsingBundle() {
321321
}
322322
}
323323
startAudio();
324-
325-
int durationSeconds = IntentBasedTestSupport.getDurationSeconds(mBundleFromIntent);
326-
if (durationSeconds > 0) {
327-
// Schedule the end of the test.
328-
Handler handler = new Handler(Looper.getMainLooper()); // UI thread
329-
handler.postDelayed(new Runnable() {
330-
@Override
331-
public void run() {
332-
stopAutomaticTest();
333-
}
334-
}, durationSeconds * 1000);
335-
}
336324
} catch (Exception e) {
337325
showErrorToast(e.getMessage());
338326
} finally {
339327
mBundleFromIntent = null;
340328
}
341329
}
342330

343-
void stopAutomaticTest() {
331+
@Override
332+
public void stopAutomaticTest() {
344333
String report = getCommonTestReport();
345334
stopAudio();
346335
maybeWriteTestResult(report);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mobileer.oboetester;
18+
19+
import android.content.BroadcastReceiver;
20+
import android.content.Context;
21+
import android.content.Intent;
22+
import android.util.Log;
23+
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
24+
25+
public class TestTimeoutReceiver extends BroadcastReceiver {
26+
private static final String TAG = "TestTimeoutReceiver";
27+
public static final String ACTION_STOP_TEST = "com.mobileer.oboetester.ACTION_STOP_TEST";
28+
29+
@Override
30+
public void onReceive(Context context, Intent intent) {
31+
Log.d(TAG, "Alarm received. Stopping test via local broadcast.");
32+
Intent stopIntent = new Intent(ACTION_STOP_TEST);
33+
LocalBroadcastManager.getInstance(context).sendBroadcast(stopIntent);
34+
}
35+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mobileer.oboetester;
18+
19+
import android.app.AlarmManager;
20+
import android.app.PendingIntent;
21+
import android.content.Context;
22+
import android.content.Intent;
23+
import android.os.Build;
24+
import android.os.SystemClock;
25+
import android.provider.Settings;
26+
import android.util.Log;
27+
import android.widget.Toast;
28+
29+
public class TestTimeoutScheduler {
30+
31+
private static final int REQUEST_CODE = 54321;
32+
private static final String TAG = "TestTimeoutScheduler";
33+
34+
public void scheduleTestTimeout(Context context, int durationSeconds) {
35+
Log.d(TAG, "Attempting to schedule test timeout.");
36+
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
37+
if (alarmManager == null) {
38+
Log.e(TAG, "AlarmManager is null.");
39+
return;
40+
}
41+
42+
// On Android 12 (S) and higher, we need to check for the permission.
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
44+
if (!alarmManager.canScheduleExactAlarms()) {
45+
Log.w(TAG, "Missing SCHEDULE_EXACT_ALARM permission.");
46+
Toast.makeText(context, "Permission needed to schedule test timeout", Toast.LENGTH_LONG).show();
47+
// Guide the user to the settings screen to grant the permission.
48+
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
49+
context.startActivity(intent);
50+
return; // Stop execution until the permission is granted.
51+
}
52+
}
53+
54+
Intent intent = new Intent(context, TestTimeoutReceiver.class);
55+
PendingIntent pendingIntent = PendingIntent.getBroadcast(
56+
context,
57+
REQUEST_CODE,
58+
intent,
59+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
60+
);
61+
62+
long triggerAtMillis = SystemClock.elapsedRealtime() + durationSeconds * 1000L;
63+
64+
try {
65+
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
66+
Log.i(TAG, "Exact alarm scheduled successfully for " + durationSeconds + " seconds.");
67+
} catch (SecurityException se) {
68+
// This catch is now a fallback, as the check above should prevent this.
69+
Log.e(TAG, "SecurityException while setting alarm. This should not happen after the check.", se);
70+
}
71+
}
72+
73+
public void cancelTestTimeout(Context context) {
74+
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
75+
if (alarmManager == null) {
76+
Log.e(TAG, "AlarmManager is null when trying to cancel.");
77+
return;
78+
}
79+
80+
Intent intent = new Intent(context, TestTimeoutReceiver.class);
81+
PendingIntent pendingIntent = PendingIntent.getBroadcast(
82+
context,
83+
REQUEST_CODE,
84+
intent,
85+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
86+
);
87+
88+
alarmManager.cancel(pendingIntent);
89+
Log.i(TAG, "Canceled test timeout alarm.");
90+
}
91+
}

0 commit comments

Comments
 (0)