initial commit

This commit is contained in:
2025-06-13 16:58:23 -04:00
commit 5b07106722
63 changed files with 5063 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

67
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,67 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "com.mattintech.androidtestapp"
compileSdk = 35
defaultConfig {
applicationId = "com.mattintech.androidtestapp"
minSdk = 32
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation("com.google.android.material:material:1.12.0")
implementation(libs.constraintlayout)
implementation(libs.activity)
implementation(libs.fragment)
// Coroutines for background tasks
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// WorkManager for scheduled downloads
implementation("androidx.work:work-runtime:2.9.0")
// OkHttp for downloads
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Location services
implementation("com.google.android.gms:play-services-location:21.0.1")
// CameraX for camera operations
implementation("androidx.camera:camera-core:1.3.1")
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.mattintech.androidtestapp;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.mattintech.androidtestapp", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Features -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="false" />
<uses-feature android:name="android.hardware.sensor.gyroscope" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidTestApp"
tools:targetApi="31">
<!-- Activities -->
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.AndroidTestApp.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false" />
<activity
android:name=".IntroActivity"
android:exported="false" />
<activity
android:name=".BatteryDrainActivity"
android:label="Battery Drain Test"
android:screenOrientation="portrait" />
<activity
android:name=".BadBehaviorActivity"
android:label="Bad Behavior Test"
android:screenOrientation="portrait" />
<activity
android:name=".DownloaderActivity"
android:label="Downloader Test"
android:screenOrientation="portrait" />
<!-- Services -->
<service
android:name=".services.BatteryDrainService"
android:foregroundServiceType="location"
android:exported="false" />
<service
android:name=".services.DownloadService"
android:exported="false" />
<service
android:name=".BadBehaviorActivity$SlowService"
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,562 @@
package com.mattintech.androidtestapp;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import androidx.core.app.NotificationCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import com.mattintech.androidtestapp.databinding.ActivityBadBehaviorBinding;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class BadBehaviorActivity extends AppCompatActivity {
private static final String TAG = Constants.LOG_TAG + "BadBehavior";
private ActivityBadBehaviorBinding binding;
private Handler handler = new Handler(Looper.getMainLooper());
private Random random = new Random();
private boolean isRandomModeActive = false;
private Runnable randomCrashRunnable;
private boolean isServiceRunning = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up crash handler to restart app
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
// Check if we're in random mode
SharedPreferences prefs = getSharedPreferences("BadBehaviorPrefs", Context.MODE_PRIVATE);
boolean isRandomActive = prefs.getBoolean("isRandomActive", false);
// Restart the app
Intent intent;
if (isRandomActive) {
// Go directly to BadBehaviorActivity to resume random mode
intent = new Intent(getApplicationContext(), BadBehaviorActivity.class);
intent.putExtra("resumeRandomMode", true);
} else {
// Normal restart to MainActivity
intent = new Intent(getApplicationContext(), MainActivity.class);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
android.os.Process.killProcess(android.os.Process.myPid());
}
});
binding = ActivityBadBehaviorBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Setup edge-to-edge
setupWindowInsets();
setupUI();
// Setup toolbar navigation
binding.toolbar.setNavigationOnClickListener(v -> {
// Check if this is the root activity (launched from notification)
if (isTaskRoot()) {
// Create a new back stack with MainActivity
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
finish();
});
// Check if we need to resume random mode after a crash
checkAndResumeRandomMode();
}
private void setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(v.getPaddingLeft(), insets.top, v.getPaddingRight(), insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
}
private void setupUI() {
// Setup crash type spinner
List<String> crashTypes = new ArrayList<>();
crashTypes.add("Division by Zero");
crashTypes.add("Null Pointer Exception");
crashTypes.add("Out of Memory");
crashTypes.add("Stack Overflow");
crashTypes.add("Array Index Out of Bounds");
ArrayAdapter<String> crashAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, crashTypes);
crashAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerCrashType.setAdapter(crashAdapter);
// Setup ANR type spinner
List<String> anrTypes = new ArrayList<>();
anrTypes.add("UI Thread Blocking");
anrTypes.add("Broadcast Receiver Timeout");
anrTypes.add("Service Timeout");
ArrayAdapter<String> anrAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, anrTypes);
anrAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerAnrType.setAdapter(anrAdapter);
// Setup frequency spinner
List<String> frequencies = new ArrayList<>();
frequencies.add("Manual");
frequencies.add("Every 10 seconds");
frequencies.add("Every 30 seconds");
frequencies.add("Every 1 minute");
frequencies.add("Every 5 minutes");
frequencies.add("Random (10-60 seconds)");
ArrayAdapter<String> frequencyAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, frequencies);
frequencyAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerFrequency.setAdapter(frequencyAdapter);
// Button listeners
binding.btnTriggerCrash.setOnClickListener(v -> triggerCrash());
binding.btnTriggerAnr.setOnClickListener(v -> triggerANR());
binding.btnStartRandom.setOnClickListener(v -> startRandomMode());
binding.btnStopRandom.setOnClickListener(v -> stopRandomMode());
binding.btnViewScheduled.setOnClickListener(v -> viewScheduledCrashes());
// Initially disable stop button
binding.btnStopRandom.setEnabled(false);
}
private void triggerCrash() {
String crashType = binding.spinnerCrashType.getSelectedItem().toString();
Log.d(TAG, "Triggering crash: " + crashType);
switch (crashType) {
case "Division by Zero":
int a = 10;
int b = 0;
int result = a / b; // This will crash
break;
case "Null Pointer Exception":
String nullString = null;
nullString.length(); // This will crash
break;
case "Out of Memory":
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // Allocate 1MB until OOM
}
case "Stack Overflow":
recursiveMethod(); // This will crash
break;
case "Array Index Out of Bounds":
int[] array = new int[5];
int value = array[10]; // This will crash
break;
}
}
private void recursiveMethod() {
recursiveMethod(); // Infinite recursion causes stack overflow
}
private void triggerANR() {
String anrType = binding.spinnerAnrType.getSelectedItem().toString();
Log.d(TAG, "Triggering ANR: " + anrType);
switch (anrType) {
case "UI Thread Blocking":
// Show warning dialog
new AlertDialog.Builder(this)
.setTitle("UI Thread Blocking ANR")
.setMessage("This will block the UI thread for 10 seconds, causing an ANR.\n\n" +
"To clear the ANR:\n" +
"1. Wait for the ANR dialog (appears after ~5 seconds)\n" +
"2. Choose 'Wait' to continue or 'Close app'\n" +
"3. The app will respond again after 10 seconds total\n\n" +
"During this time, the app will be completely frozen.")
.setPositiveButton("Block UI Thread", (dialog, which) -> {
Log.d(TAG, "User confirmed UI Thread Blocking ANR");
Toast.makeText(this, "Blocking UI thread for 10 seconds...", Toast.LENGTH_SHORT).show();
handler.postDelayed(() -> {
try {
Log.d(TAG, "Starting UI thread block");
Thread.sleep(10000); // This will cause ANR
Log.d(TAG, "UI thread block completed");
} catch (InterruptedException e) {
Log.e(TAG, "UI thread block interrupted", e);
e.printStackTrace();
}
}, 100); // Small delay to show toast
})
.setNegativeButton("Cancel", null)
.show();
break;
case "Broadcast Receiver Timeout":
// Show warning dialog
new AlertDialog.Builder(this)
.setTitle("Broadcast Receiver Timeout ANR")
.setMessage("This will trigger a broadcast receiver that blocks for 15 seconds, causing an ANR.\n\n" +
"To clear the ANR:\n" +
"1. Wait for the ANR dialog (appears after ~10 seconds)\n" +
"2. Choose 'Wait' or 'Close app'\n" +
"3. The receiver will complete after 15 seconds\n\n" +
"Note: The app may remain partially responsive during this ANR.")
.setPositiveButton("Trigger Broadcast ANR", (dialog, which) -> {
Log.d(TAG, "User confirmed Broadcast Receiver ANR");
BroadcastReceiver slowReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
try {
Log.d(TAG, "Starting broadcast receiver block");
Thread.sleep(15000); // This will cause ANR
Log.d(TAG, "Broadcast receiver block completed");
} catch (InterruptedException e) {
Log.e(TAG, "Broadcast receiver block interrupted", e);
e.printStackTrace();
}
}
};
registerReceiver(slowReceiver, new IntentFilter("com.mattintech.SLOW_ACTION"));
sendBroadcast(new Intent("com.mattintech.SLOW_ACTION"));
Toast.makeText(this, "Broadcast sent - ANR will occur soon", Toast.LENGTH_LONG).show();
})
.setNegativeButton("Cancel", null)
.show();
break;
case "Service Timeout":
if (!isServiceRunning) {
// Show warning dialog
new AlertDialog.Builder(this)
.setTitle("Service Timeout ANR")
.setMessage("This will start a service that blocks for 20 seconds, causing an ANR.\n\n" +
"To clear the ANR:\n" +
"1. Wait for the ANR dialog to appear\n" +
"2. Choose 'Wait' or 'Close app'\n" +
"3. Force stop the app from Settings if needed\n\n" +
"The service will automatically stop after 20 seconds.")
.setPositiveButton("Start Service", (dialog, which) -> {
Log.d(TAG, "User confirmed Service Timeout ANR");
isServiceRunning = true;
Intent serviceIntent = new Intent(this, SlowService.class);
startService(serviceIntent);
Log.d(TAG, "Starting slow service");
Toast.makeText(this, "Service started - ANR will occur in ~5 seconds", Toast.LENGTH_LONG).show();
// Schedule service status reset
handler.postDelayed(() -> {
isServiceRunning = false;
Log.d(TAG, "Service timeout cleared");
Toast.makeText(this, "Service timeout cleared", Toast.LENGTH_SHORT).show();
}, 20000);
})
.setNegativeButton("Cancel", null)
.show();
} else {
Toast.makeText(this, "Service is already running. Wait for it to complete.", Toast.LENGTH_LONG).show();
}
break;
}
}
private void startRandomMode() {
String frequency = binding.spinnerFrequency.getSelectedItem().toString();
if (frequency.equals("Manual")) {
Toast.makeText(this, "Please select a time interval", Toast.LENGTH_SHORT).show();
return;
}
// Show warning dialog before starting
new AlertDialog.Builder(this)
.setTitle("⚠️ Warning: Random Crash Mode")
.setMessage("IMPORTANT: The app must stay in the foreground!\n" +
"Crashes only occur when this screen is visible.\n\n" +
"Starting random crash mode will:\n" +
"• INTERRUPT all other running tests\n" +
"• STOP battery drain tests\n" +
"• FAIL scheduled downloads\n" +
"• Crash the app " + frequency.toLowerCase() + "\n\n" +
"The app will auto-restart and return to this screen.\n" +
"Keep the app open for continuous testing.\n" +
"This mode will automatically stop after 24 hours.\n\n" +
"Are you sure you want to start random crash mode?")
.setPositiveButton("Start", (dialog, which) -> {
Log.d(TAG, "Starting random mode with frequency: " + frequency);
// Save random mode configuration
SharedPreferences prefs = getSharedPreferences("BadBehaviorPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("isRandomActive", true);
editor.putString("frequency", frequency);
editor.putLong("startTime", System.currentTimeMillis());
editor.apply();
isRandomModeActive = true;
binding.btnStartRandom.setEnabled(false);
binding.btnStopRandom.setEnabled(true);
binding.btnTriggerCrash.setEnabled(false);
binding.btnTriggerAnr.setEnabled(false);
long delayMillis = getDelayMillis(frequency);
scheduleRandomBehavior(delayMillis);
// Show notification for random mode
showRandomModeNotification(frequency);
Toast.makeText(this, "Random mode started - keep app in foreground!", Toast.LENGTH_LONG).show();
})
.setNegativeButton("Cancel", null)
.show();
}
private void scheduleRandomBehavior(long delayMillis) {
if (!isRandomModeActive) return;
randomCrashRunnable = () -> {
if (!isRandomModeActive) return;
// Only trigger crashes in random mode (no ANRs to avoid dialogs)
int crashIndex = random.nextInt(5);
binding.spinnerCrashType.setSelection(crashIndex);
Log.d(TAG, "Random mode triggering crash type: " + binding.spinnerCrashType.getSelectedItem());
triggerCrash();
// Note: The next crash will be scheduled after the app restarts
// via checkAndResumeRandomMode()
};
handler.postDelayed(randomCrashRunnable, delayMillis);
}
private long getDelayMillis(String frequency) {
switch (frequency) {
case "Every 10 seconds":
return 10000;
case "Every 30 seconds":
return 30000;
case "Every 1 minute":
return 60000;
case "Every 5 minutes":
return 300000;
case "Random (10-60 seconds)":
return 10000 + random.nextInt(50000);
default:
return 10000;
}
}
private void stopRandomMode() {
Log.d(TAG, "Stopping random mode");
isRandomModeActive = false;
handler.removeCallbacks(randomCrashRunnable);
// Clear random mode from preferences
SharedPreferences prefs = getSharedPreferences("BadBehaviorPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("isRandomActive", false);
editor.apply();
binding.btnStartRandom.setEnabled(true);
binding.btnStopRandom.setEnabled(false);
binding.btnTriggerCrash.setEnabled(true);
binding.btnTriggerAnr.setEnabled(true);
// Cancel notification
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Constants.NOTIFICATION_ID_BAD_BEHAVIOR);
Toast.makeText(this, "Random mode stopped", Toast.LENGTH_SHORT).show();
}
private void viewScheduledCrashes() {
SharedPreferences prefs = getSharedPreferences("BadBehaviorPrefs", Context.MODE_PRIVATE);
boolean isRandomActive = prefs.getBoolean("isRandomActive", false);
if (isRandomActive && isRandomModeActive) {
String frequency = prefs.getString("frequency", "Unknown");
long startTime = prefs.getLong("startTime", 0);
StringBuilder info = new StringBuilder();
info.append("Random Crash Mode Active\n\n");
info.append("Frequency: ").append(frequency).append("\n");
if (startTime > 0) {
long elapsedMinutes = (System.currentTimeMillis() - startTime) / 60000;
info.append("Running for: ").append(elapsedMinutes).append(" minutes\n");
}
info.append("\nThis mode randomly triggers:\n");
info.append("• App crashes only (no ANRs)\n");
info.append("• 5 different crash types\n");
info.append("\nThe app will auto-restart after crashes.\n");
info.append("Mode stops automatically after 24 hours.");
new AlertDialog.Builder(this)
.setTitle("Scheduled Crashes")
.setMessage(info.toString())
.setPositiveButton("OK", null)
.setNegativeButton("Stop Random Mode", (dialog, which) -> stopRandomMode())
.show();
} else {
new AlertDialog.Builder(this)
.setTitle("No Scheduled Crashes")
.setMessage("Random crash mode is not active.\n\nUse 'Start Random' to schedule automatic crashes/ANRs.")
.setPositiveButton("OK", null)
.show();
}
}
private void showRandomModeNotification(String frequency) {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Intent to open this activity when notification is clicked
Intent intent = new Intent(this, BadBehaviorActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
// Intent to stop random mode
Intent stopIntent = new Intent(this, BadBehaviorActivity.class);
stopIntent.setAction("STOP_RANDOM_MODE");
PendingIntent stopPendingIntent = PendingIntent.getActivity(this, 1, stopIntent,
PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_BAD_BEHAVIOR)
.setContentTitle("⚠️ Random Crash Mode Active")
.setContentText("Keep app in foreground! Crashes " + frequency.toLowerCase())
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("IMPORTANT: App must stay in foreground for crashes!\n" +
"All other tests are being interrupted!\n" +
"Frequency: " + frequency + "\n" +
"Auto-restarts after crashes\n" +
"Stops after 24 hours"))
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent);
notificationManager.notify(Constants.NOTIFICATION_ID_BAD_BEHAVIOR, builder.build());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if ("STOP_RANDOM_MODE".equals(intent.getAction())) {
stopRandomMode();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopRandomMode();
// Stop service if running
if (isServiceRunning) {
stopService(new Intent(this, SlowService.class));
isServiceRunning = false;
}
}
private void stopSlowService() {
if (isServiceRunning) {
stopService(new Intent(this, SlowService.class));
isServiceRunning = false;
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show();
}
}
private void checkAndResumeRandomMode() {
SharedPreferences prefs = getSharedPreferences("BadBehaviorPrefs", Context.MODE_PRIVATE);
boolean wasRandomActive = prefs.getBoolean("isRandomActive", false);
if (wasRandomActive) {
String frequency = prefs.getString("frequency", "");
long startTime = prefs.getLong("startTime", 0);
// Check if we should still be in random mode (e.g., within 24 hours)
long elapsedHours = (System.currentTimeMillis() - startTime) / (1000 * 60 * 60);
if (elapsedHours < 24 && !frequency.isEmpty()) {
Log.d(TAG, "Resuming random mode after restart: " + frequency);
// Update UI to reflect active state
isRandomModeActive = true;
binding.btnStartRandom.setEnabled(false);
binding.btnStopRandom.setEnabled(true);
binding.btnTriggerCrash.setEnabled(false);
binding.btnTriggerAnr.setEnabled(false);
// Set the spinner to the saved frequency
ArrayAdapter<String> adapter = (ArrayAdapter<String>) binding.spinnerFrequency.getAdapter();
int position = adapter.getPosition(frequency);
if (position >= 0) {
binding.spinnerFrequency.setSelection(position);
}
// Resume scheduling with a shorter initial delay after crash
long delayMillis = Math.min(10000, getDelayMillis(frequency)); // 10 seconds or normal delay, whichever is shorter
scheduleRandomBehavior(delayMillis);
// Show notification again
showRandomModeNotification(frequency);
Toast.makeText(this, "Random mode resumed", Toast.LENGTH_SHORT).show();
} else {
// Clear stale random mode
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("isRandomActive", false);
editor.apply();
}
}
}
// Inner class for slow service
public static class SlowService extends android.app.Service {
private static final String TAG = Constants.LOG_TAG + "SlowService";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "SlowService started - blocking for 20 seconds");
// Block the service
try {
Thread.sleep(20000); // This will cause ANR
Log.d(TAG, "SlowService block completed");
} catch (InterruptedException e) {
Log.e(TAG, "SlowService interrupted", e);
e.printStackTrace();
}
return START_NOT_STICKY;
}
@Override
public android.os.IBinder onBind(Intent intent) {
return null;
}
}
}

View File

@@ -0,0 +1,362 @@
package com.mattintech.androidtestapp;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import com.mattintech.androidtestapp.databinding.ActivityBatteryDrainBinding;
import com.mattintech.androidtestapp.services.BatteryDrainService;
import java.util.ArrayList;
import java.util.List;
import android.app.AlertDialog;
import android.app.ActivityManager;
import android.content.SharedPreferences;
public class BatteryDrainActivity extends AppCompatActivity {
private static final String TAG = Constants.LOG_TAG + "BatteryDrain";
private ActivityBatteryDrainBinding binding;
private BatteryDrainService.BatteryDrainBinder binder;
private boolean isServiceBound = false;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
binder = (BatteryDrainService.BatteryDrainBinder) service;
isServiceBound = true;
}
@Override
public void onServiceDisconnected(ComponentName name) {
isServiceBound = false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityBatteryDrainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Setup edge-to-edge
setupWindowInsets();
setupUI();
setupPresets();
// Setup toolbar navigation
binding.toolbar.setNavigationOnClickListener(v -> {
// Check if this is the root activity (launched from notification)
if (isTaskRoot()) {
// Create a new back stack with MainActivity
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
finish();
});
}
private void setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(v.getPaddingLeft(), insets.top, v.getPaddingRight(), insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
}
private void setupUI() {
// Setup duration spinner
List<String> durations = new ArrayList<>();
durations.add("1 minute");
durations.add("5 minutes");
durations.add("10 minutes");
durations.add("30 minutes");
durations.add("1 hour");
durations.add("Unlimited");
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_list_item_1, durations);
((AutoCompleteTextView)binding.spinnerDuration).setAdapter(adapter);
((AutoCompleteTextView)binding.spinnerDuration).setText(durations.get(0), false);
// Start button
binding.btnStart.setOnClickListener(v -> startBatteryDrain());
// Stop button
binding.btnStop.setOnClickListener(v -> stopBatteryDrain());
binding.btnStop.setEnabled(false);
// View active test button
binding.btnViewActiveTest.setOnClickListener(v -> viewActiveTest());
}
private void setupPresets() {
binding.btnPresetMax.setOnCheckedChangeListener((chip, isChecked) -> {
if (isChecked) {
// Select all options
binding.cbCpu.setChecked(true);
binding.cbScreen.setChecked(true);
binding.cbFlashlight.setChecked(true);
binding.cbGps.setChecked(true);
binding.cbBluetooth.setChecked(true);
binding.cbWifi.setChecked(true);
binding.cbNetwork.setChecked(true);
binding.cbSensors.setChecked(true);
binding.cbCamera.setChecked(true);
binding.cbAudio.setChecked(true);
binding.cbVibration.setChecked(true);
}
});
binding.btnPresetCpuGpu.setOnCheckedChangeListener((chip, isChecked) -> {
if (isChecked) {
clearAllOptions();
binding.cbCpu.setChecked(true);
binding.cbScreen.setChecked(true);
}
});
binding.btnPresetRadio.setOnCheckedChangeListener((chip, isChecked) -> {
if (isChecked) {
clearAllOptions();
binding.cbGps.setChecked(true);
binding.cbBluetooth.setChecked(true);
binding.cbWifi.setChecked(true);
}
});
binding.btnPresetSensors.setOnCheckedChangeListener((chip, isChecked) -> {
if (isChecked) {
clearAllOptions();
binding.cbSensors.setChecked(true);
binding.cbVibration.setChecked(true);
}
});
}
private void clearAllOptions() {
binding.cbCpu.setChecked(false);
binding.cbScreen.setChecked(false);
binding.cbFlashlight.setChecked(false);
binding.cbGps.setChecked(false);
binding.cbBluetooth.setChecked(false);
binding.cbWifi.setChecked(false);
binding.cbNetwork.setChecked(false);
binding.cbSensors.setChecked(false);
binding.cbCamera.setChecked(false);
binding.cbAudio.setChecked(false);
binding.cbVibration.setChecked(false);
}
private void startBatteryDrain() {
// Validate at least one option is selected
if (!binding.cbCpu.isChecked() && !binding.cbScreen.isChecked() &&
!binding.cbFlashlight.isChecked() && !binding.cbGps.isChecked() &&
!binding.cbBluetooth.isChecked() && !binding.cbWifi.isChecked() &&
!binding.cbNetwork.isChecked() && !binding.cbSensors.isChecked() &&
!binding.cbCamera.isChecked() && !binding.cbAudio.isChecked() &&
!binding.cbVibration.isChecked()) {
Toast.makeText(this, "Please select at least one option", Toast.LENGTH_SHORT).show();
return;
}
// Get duration
int durationMinutes = getDurationMinutes();
// Log test configuration
Log.d(TAG, "Starting battery drain test with configuration:");
Log.d(TAG, " Duration: " + (durationMinutes == -1 ? "Unlimited" : durationMinutes + " minutes"));
Log.d(TAG, " CPU: " + binding.cbCpu.isChecked());
Log.d(TAG, " Screen: " + binding.cbScreen.isChecked());
Log.d(TAG, " Flashlight: " + binding.cbFlashlight.isChecked());
Log.d(TAG, " GPS: " + binding.cbGps.isChecked());
Log.d(TAG, " Bluetooth: " + binding.cbBluetooth.isChecked());
Log.d(TAG, " WiFi: " + binding.cbWifi.isChecked());
Log.d(TAG, " Network: " + binding.cbNetwork.isChecked());
Log.d(TAG, " Sensors: " + binding.cbSensors.isChecked());
Log.d(TAG, " Camera: " + binding.cbCamera.isChecked());
Log.d(TAG, " Audio: " + binding.cbAudio.isChecked());
Log.d(TAG, " Vibration: " + binding.cbVibration.isChecked());
// Create intent for service
Intent serviceIntent = new Intent(this, BatteryDrainService.class);
serviceIntent.putExtra("cpu", binding.cbCpu.isChecked());
serviceIntent.putExtra("screen", binding.cbScreen.isChecked());
serviceIntent.putExtra("flashlight", binding.cbFlashlight.isChecked());
serviceIntent.putExtra("gps", binding.cbGps.isChecked());
serviceIntent.putExtra("bluetooth", binding.cbBluetooth.isChecked());
serviceIntent.putExtra("wifi", binding.cbWifi.isChecked());
serviceIntent.putExtra("network", binding.cbNetwork.isChecked());
serviceIntent.putExtra("sensors", binding.cbSensors.isChecked());
serviceIntent.putExtra("camera", binding.cbCamera.isChecked());
serviceIntent.putExtra("audio", binding.cbAudio.isChecked());
serviceIntent.putExtra("vibration", binding.cbVibration.isChecked());
serviceIntent.putExtra("duration", durationMinutes);
// Save configuration to SharedPreferences
SharedPreferences prefs = getSharedPreferences("BatteryDrainPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("cpu", binding.cbCpu.isChecked());
editor.putBoolean("screen", binding.cbScreen.isChecked());
editor.putBoolean("flashlight", binding.cbFlashlight.isChecked());
editor.putBoolean("gps", binding.cbGps.isChecked());
editor.putBoolean("bluetooth", binding.cbBluetooth.isChecked());
editor.putBoolean("wifi", binding.cbWifi.isChecked());
editor.putBoolean("network", binding.cbNetwork.isChecked());
editor.putBoolean("sensors", binding.cbSensors.isChecked());
editor.putBoolean("camera", binding.cbCamera.isChecked());
editor.putBoolean("audio", binding.cbAudio.isChecked());
editor.putBoolean("vibration", binding.cbVibration.isChecked());
editor.putInt("duration", durationMinutes);
editor.putLong("startTime", System.currentTimeMillis());
editor.apply();
// Start service
startService(serviceIntent);
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
// Update UI
binding.btnStart.setEnabled(false);
binding.btnStop.setEnabled(true);
disableOptions();
Toast.makeText(this, "Battery drain test started", Toast.LENGTH_SHORT).show();
}
private void stopBatteryDrain() {
Log.d(TAG, "Stopping battery drain test");
if (isServiceBound) {
unbindService(serviceConnection);
isServiceBound = false;
}
stopService(new Intent(this, BatteryDrainService.class));
// Update UI
binding.btnStart.setEnabled(true);
binding.btnStop.setEnabled(false);
enableOptions();
Toast.makeText(this, "Battery drain test stopped", Toast.LENGTH_SHORT).show();
}
private int getDurationMinutes() {
String selected = ((AutoCompleteTextView)binding.spinnerDuration).getText().toString();
switch (selected) {
case "1 minute": return 1;
case "5 minutes": return 5;
case "10 minutes": return 10;
case "30 minutes": return 30;
case "1 hour": return 60;
default: return -1; // Unlimited
}
}
private void disableOptions() {
binding.cbCpu.setEnabled(false);
binding.cbScreen.setEnabled(false);
binding.cbFlashlight.setEnabled(false);
binding.cbGps.setEnabled(false);
binding.cbBluetooth.setEnabled(false);
binding.cbWifi.setEnabled(false);
binding.cbNetwork.setEnabled(false);
binding.cbSensors.setEnabled(false);
binding.cbCamera.setEnabled(false);
binding.cbAudio.setEnabled(false);
binding.cbVibration.setEnabled(false);
binding.spinnerDuration.setEnabled(false);
}
private void enableOptions() {
binding.cbCpu.setEnabled(true);
binding.cbScreen.setEnabled(true);
binding.cbFlashlight.setEnabled(true);
binding.cbGps.setEnabled(true);
binding.cbBluetooth.setEnabled(true);
binding.cbWifi.setEnabled(true);
binding.cbNetwork.setEnabled(true);
binding.cbSensors.setEnabled(true);
binding.cbCamera.setEnabled(true);
binding.cbAudio.setEnabled(true);
binding.cbVibration.setEnabled(true);
binding.spinnerDuration.setEnabled(true);
}
private void viewActiveTest() {
// Check if service is running
if (isServiceRunning(BatteryDrainService.class)) {
// Get test configuration from SharedPreferences (we'll store it when starting)
SharedPreferences prefs = getSharedPreferences("BatteryDrainPrefs", Context.MODE_PRIVATE);
StringBuilder activeTestInfo = new StringBuilder();
activeTestInfo.append("Battery Drain Test Active\n\n");
// Duration
int duration = prefs.getInt("duration", -1);
activeTestInfo.append("Duration: ").append(duration == -1 ? "Unlimited" : duration + " minutes").append("\n\n");
// Active components
activeTestInfo.append("Active Components:\n");
if (prefs.getBoolean("cpu", false)) activeTestInfo.append("• CPU Intensive Operations\n");
if (prefs.getBoolean("screen", false)) activeTestInfo.append("• Screen (Max Brightness)\n");
if (prefs.getBoolean("flashlight", false)) activeTestInfo.append("• Flashlight\n");
if (prefs.getBoolean("gps", false)) activeTestInfo.append("• GPS Location\n");
if (prefs.getBoolean("bluetooth", false)) activeTestInfo.append("• Bluetooth Scanning\n");
if (prefs.getBoolean("wifi", false)) activeTestInfo.append("• WiFi Scanning\n");
if (prefs.getBoolean("network", false)) activeTestInfo.append("• Network Activity\n");
if (prefs.getBoolean("sensors", false)) activeTestInfo.append("• Sensors\n");
if (prefs.getBoolean("camera", false)) activeTestInfo.append("• Camera Preview\n");
if (prefs.getBoolean("audio", false)) activeTestInfo.append("• Audio Playback\n");
if (prefs.getBoolean("vibration", false)) activeTestInfo.append("• Vibration\n");
// Start time
long startTime = prefs.getLong("startTime", 0);
if (startTime > 0) {
long elapsedMinutes = (System.currentTimeMillis() - startTime) / 60000;
activeTestInfo.append("\nElapsed Time: ").append(elapsedMinutes).append(" minutes");
}
new AlertDialog.Builder(this)
.setTitle("Active Battery Drain Test")
.setMessage(activeTestInfo.toString())
.setPositiveButton("OK", null)
.setNegativeButton("Stop Test", (dialog, which) -> stopBatteryDrain())
.show();
} else {
new AlertDialog.Builder(this)
.setTitle("No Active Test")
.setMessage("No battery drain test is currently running")
.setPositiveButton("OK", null)
.show();
}
}
private boolean isServiceRunning(Class<?> serviceClass) {
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
return true;
}
}
return false;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isServiceBound) {
unbindService(serviceConnection);
}
}
}

View File

@@ -0,0 +1,15 @@
package com.mattintech.androidtestapp;
public class Constants {
public static final String LOG_TAG = "AndroidTestApp_";
// Notification channels
public static final String NOTIFICATION_CHANNEL_BATTERY = "battery_drain_channel";
public static final String NOTIFICATION_CHANNEL_DOWNLOAD = "download_channel";
public static final String NOTIFICATION_CHANNEL_BAD_BEHAVIOR = "bad_behavior_channel";
// Notification IDs
public static final int NOTIFICATION_ID_BATTERY = 1001;
public static final int NOTIFICATION_ID_DOWNLOAD = 1002;
public static final int NOTIFICATION_ID_BAD_BEHAVIOR = 1003;
}

View File

@@ -0,0 +1,570 @@
package com.mattintech.androidtestapp;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkInfo;
import com.mattintech.androidtestapp.databinding.ActivityDownloaderBinding;
import com.mattintech.androidtestapp.services.DownloadService;
import com.mattintech.androidtestapp.workers.DownloadWorker;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import android.app.AlertDialog;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import android.app.NotificationManager;
import android.app.PendingIntent;
import androidx.core.app.NotificationCompat;
public class DownloaderActivity extends AppCompatActivity {
private static final String TAG = Constants.LOG_TAG + "DownloaderActivity";
private static final String PREFS_NAME = "DownloaderPrefs";
private static final String KEY_TOTAL_BYTES = "totalBytesDownloaded";
private static final String KEY_DOWNLOAD_COUNT = "downloadCount";
private ActivityDownloaderBinding binding;
private DownloadProgressReceiver progressReceiver;
private SharedPreferences sharedPrefs;
private long totalBytesDownloaded = 0;
private int downloadCount = 0;
private Map<Long, View> activeDownloads = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDownloaderBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Initialize SharedPreferences
sharedPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
loadStats();
// Setup edge-to-edge
setupWindowInsets();
setupUI();
registerProgressReceiver();
// Setup toolbar navigation
binding.toolbar.setNavigationOnClickListener(v -> {
// Check if this is the root activity (launched from notification)
if (isTaskRoot()) {
// Create a new back stack with MainActivity
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
finish();
});
}
private void setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(v.getPaddingLeft(), insets.top, v.getPaddingRight(), insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
}
private void setupUI() {
// Default URLs
binding.etUrl.setText("https://test1.bigstam.net/download/1mb.bin");
// File size spinner
List<String> fileSizes = new ArrayList<>();
fileSizes.add("1 MB");
fileSizes.add("10 MB");
fileSizes.add("100 MB");
fileSizes.add("Custom URL");
ArrayAdapter<String> sizeAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, fileSizes);
sizeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerFileSize.setAdapter(sizeAdapter);
// Schedule type spinner
List<String> scheduleTypes = new ArrayList<>();
scheduleTypes.add("One-time");
scheduleTypes.add("Every 15 minutes");
scheduleTypes.add("Every 30 minutes");
scheduleTypes.add("Every 1 hour");
scheduleTypes.add("Every 6 hours");
scheduleTypes.add("Every 24 hours");
ArrayAdapter<String> scheduleAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, scheduleTypes);
scheduleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerSchedule.setAdapter(scheduleAdapter);
// Network preference spinner
List<String> networkTypes = new ArrayList<>();
networkTypes.add("Any");
networkTypes.add("WiFi Only");
networkTypes.add("Cellular Only");
ArrayAdapter<String> networkAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, networkTypes);
networkAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.spinnerNetwork.setAdapter(networkAdapter);
// Concurrent downloads
binding.seekBarConcurrent.setMax(9);
binding.seekBarConcurrent.setProgress(0);
binding.tvConcurrentValue.setText("1");
binding.seekBarConcurrent.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(android.widget.SeekBar seekBar, int progress, boolean fromUser) {
binding.tvConcurrentValue.setText(String.valueOf(progress + 1));
}
@Override
public void onStartTrackingTouch(android.widget.SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(android.widget.SeekBar seekBar) {}
});
// Update URL based on file size selection
binding.spinnerFileSize.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, android.view.View view, int position, long id) {
switch (position) {
case 0: // 1 MB
binding.etUrl.setText("https://test1.bigstam.net/download/1mb.bin");
break;
case 1: // 10 MB
binding.etUrl.setText("https://test1.bigstam.net/download/10mb.bin");
break;
case 2: // 100 MB
binding.etUrl.setText("https://test1.bigstam.net/download/100mb.bin");
break;
case 3: // Custom
binding.etUrl.setText("");
binding.etUrl.setEnabled(true);
binding.etUrl.requestFocus();
return;
}
binding.etUrl.setEnabled(false);
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
});
// Buttons
binding.btnStartDownload.setOnClickListener(v -> startDownload());
binding.btnStopAll.setOnClickListener(v -> stopAllDownloads());
binding.btnClearHistory.setOnClickListener(v -> clearHistory());
binding.btnViewScheduled.setOnClickListener(v -> viewScheduledDownloads());
updateStats();
}
private void registerProgressReceiver() {
progressReceiver = new DownloadProgressReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(DownloadService.ACTION_DOWNLOAD_PROGRESS);
filter.addAction(DownloadService.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(progressReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
Log.d(TAG, "Registered broadcast receiver for download progress");
}
private void startDownload() {
String url = binding.etUrl.getText().toString().trim();
if (url.isEmpty()) {
Toast.makeText(this, "Please enter a URL", Toast.LENGTH_SHORT).show();
return;
}
boolean saveFile = binding.cbSaveFile.isChecked();
boolean backgroundDownload = binding.cbBackground.isChecked();
int concurrentDownloads = binding.seekBarConcurrent.getProgress() + 1;
String scheduleType = binding.spinnerSchedule.getSelectedItem().toString();
String networkPref = binding.spinnerNetwork.getSelectedItem().toString();
// Create network constraints
NetworkType networkType;
switch (networkPref) {
case "WiFi Only":
networkType = NetworkType.UNMETERED;
break;
case "Cellular Only":
networkType = NetworkType.METERED;
break;
default:
networkType = NetworkType.CONNECTED;
break;
}
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(networkType)
.build();
// Create work data
Data inputData = new Data.Builder()
.putString("url", url)
.putBoolean("saveFile", saveFile)
.putBoolean("background", backgroundDownload)
.build();
if (scheduleType.equals("One-time")) {
// Start immediate downloads using service
for (int i = 0; i < concurrentDownloads; i++) {
Intent intent = new Intent(this, DownloadService.class);
intent.putExtra("url", url);
intent.putExtra("saveFile", saveFile);
intent.putExtra("downloadId", System.currentTimeMillis() + i);
Log.d(TAG, "Starting download service for URL: " + url);
startService(intent);
}
Toast.makeText(this, "Started " + concurrentDownloads + " download(s)", Toast.LENGTH_SHORT).show();
} else {
// Schedule periodic downloads using WorkManager
long intervalMinutes = getIntervalMinutes(scheduleType);
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(
DownloadWorker.class, intervalMinutes, TimeUnit.MINUTES)
.setConstraints(constraints)
.setInputData(inputData)
.addTag("scheduled_download")
.addTag("url:" + url)
.addTag("interval:" + intervalMinutes)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"download_work",
ExistingPeriodicWorkPolicy.REPLACE,
periodicWork
);
// Show notification for scheduled downloads
showScheduledDownloadNotification(url, scheduleType);
Toast.makeText(this, "Scheduled downloads " + scheduleType.toLowerCase(), Toast.LENGTH_SHORT).show();
}
}
private long getIntervalMinutes(String scheduleType) {
switch (scheduleType) {
case "Every 15 minutes": return 15;
case "Every 30 minutes": return 30;
case "Every 1 hour": return 60;
case "Every 6 hours": return 360;
case "Every 24 hours": return 1440;
default: return 15;
}
}
private void stopAllDownloads() {
// Stop service
stopService(new Intent(this, DownloadService.class));
// Cancel scheduled work
WorkManager.getInstance(this).cancelUniqueWork("download_work");
// Cancel notification
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Constants.NOTIFICATION_ID_DOWNLOAD);
// Clear all progress views
clearAllProgressViews();
Toast.makeText(this, "Stopped all downloads", Toast.LENGTH_SHORT).show();
}
private void addProgressView(long downloadId, String url) {
// Remove existing view if any
if (activeDownloads.containsKey(downloadId)) {
binding.progressContainer.removeView(activeDownloads.get(downloadId));
}
// Inflate new progress view
View progressView = LayoutInflater.from(this).inflate(R.layout.item_download_progress, binding.progressContainer, false);
TextView tvDownloadId = progressView.findViewById(R.id.tvDownloadId);
TextView tvProgress = progressView.findViewById(R.id.tvDownloadProgress);
ProgressBar progressBar = progressView.findViewById(R.id.progressBarDownload);
// Extract filename from URL
String filename = url.substring(url.lastIndexOf('/') + 1);
tvDownloadId.setText("Download: " + filename);
tvProgress.setText("0%");
progressBar.setProgress(0);
// Add to container
binding.progressContainer.addView(progressView);
activeDownloads.put(downloadId, progressView);
// Hide "no downloads" text
binding.tvNoDownloads.setVisibility(View.GONE);
Log.d(TAG, "Added progress view for download " + downloadId);
}
private void updateProgressView(long downloadId, int progress) {
View progressView = activeDownloads.get(downloadId);
if (progressView != null) {
TextView tvProgress = progressView.findViewById(R.id.tvDownloadProgress);
ProgressBar progressBar = progressView.findViewById(R.id.progressBarDownload);
tvProgress.setText(progress + "%");
progressBar.setProgress(progress);
}
}
private void removeProgressView(long downloadId) {
View progressView = activeDownloads.remove(downloadId);
if (progressView != null) {
binding.progressContainer.removeView(progressView);
// Show "no downloads" text if empty
if (activeDownloads.isEmpty()) {
binding.tvNoDownloads.setVisibility(View.VISIBLE);
}
}
}
private void clearAllProgressViews() {
binding.progressContainer.removeAllViews();
activeDownloads.clear();
binding.tvNoDownloads.setVisibility(View.VISIBLE);
}
private void clearHistory() {
totalBytesDownloaded = 0;
downloadCount = 0;
saveStats();
updateStats();
Toast.makeText(this, "History cleared", Toast.LENGTH_SHORT).show();
}
private void updateStats() {
String stats = String.format("Downloads: %d | Total: %s",
downloadCount, formatBytes(totalBytesDownloaded));
binding.tvStats.setText(stats);
Log.d(TAG, "Updated stats: " + stats);
}
private void loadStats() {
totalBytesDownloaded = sharedPrefs.getLong(KEY_TOTAL_BYTES, 0);
downloadCount = sharedPrefs.getInt(KEY_DOWNLOAD_COUNT, 0);
Log.d(TAG, "Loaded stats - Downloads: " + downloadCount + ", Bytes: " + totalBytesDownloaded);
}
private void saveStats() {
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putLong(KEY_TOTAL_BYTES, totalBytesDownloaded);
editor.putInt(KEY_DOWNLOAD_COUNT, downloadCount);
editor.apply();
Log.d(TAG, "Saved stats - Downloads: " + downloadCount + ", Bytes: " + totalBytesDownloaded);
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
else if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
else return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
private void viewScheduledDownloads() {
WorkManager workManager = WorkManager.getInstance(this);
try {
List<WorkInfo> workInfos = workManager.getWorkInfosForUniqueWork("download_work").get();
if (workInfos == null || workInfos.isEmpty()) {
showNoScheduledDownloadsDialog();
return;
}
StringBuilder scheduledInfo = new StringBuilder();
SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss", Locale.getDefault());
for (WorkInfo workInfo : workInfos) {
scheduledInfo.append("Status: ").append(workInfo.getState().name()).append("\n");
if (workInfo.getState() == WorkInfo.State.ENQUEUED ||
workInfo.getState() == WorkInfo.State.RUNNING ||
workInfo.getState() == WorkInfo.State.SUCCEEDED) {
// Extract URL from tags
String url = workInfo.getTags().stream()
.filter(tag -> tag.startsWith("url:"))
.map(tag -> tag.substring(4))
.findFirst()
.orElse("Unknown URL");
// Extract interval from tags
String interval = workInfo.getTags().stream()
.filter(tag -> tag.startsWith("interval:"))
.map(tag -> tag.substring(9))
.findFirst()
.orElse("Unknown");
scheduledInfo.append("URL: ").append(url).append("\n");
scheduledInfo.append("Interval: Every ").append(interval).append(" minutes\n");
// Show run status
if (workInfo.getState() == WorkInfo.State.RUNNING) {
scheduledInfo.append("Status: Currently running\n");
} else if (workInfo.getState() == WorkInfo.State.ENQUEUED) {
scheduledInfo.append("Status: Waiting for next run\n");
} else if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
scheduledInfo.append("Status: Last run succeeded\n");
}
}
scheduledInfo.append("\n");
}
if (scheduledInfo.length() == 0) {
showNoScheduledDownloadsDialog();
} else {
showScheduledDownloadsDialog(scheduledInfo.toString());
}
} catch (Exception e) {
Log.e(TAG, "Error getting scheduled downloads", e);
Toast.makeText(this, "Error retrieving scheduled downloads", Toast.LENGTH_SHORT).show();
}
}
private void showScheduledDownloadsDialog(String info) {
new AlertDialog.Builder(this)
.setTitle("Scheduled Downloads")
.setMessage(info)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel All", (dialog, which) -> {
WorkManager.getInstance(this).cancelUniqueWork("download_work");
Toast.makeText(this, "All scheduled downloads cancelled", Toast.LENGTH_SHORT).show();
})
.show();
}
private void showNoScheduledDownloadsDialog() {
new AlertDialog.Builder(this)
.setTitle("Scheduled Downloads")
.setMessage("No scheduled downloads found")
.setPositiveButton("OK", null)
.show();
}
private void showScheduledDownloadNotification(String url, String scheduleType) {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Intent to open this activity when notification is clicked
Intent intent = new Intent(this, DownloaderActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
// Intent to cancel downloads
Intent cancelIntent = new Intent(this, DownloaderActivity.class);
cancelIntent.setAction("CANCEL_DOWNLOADS");
PendingIntent cancelPendingIntent = PendingIntent.getActivity(this, 1, cancelIntent,
PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_DOWNLOAD)
.setContentTitle("Scheduled Downloads Active")
.setContentText("Downloading " + scheduleType.toLowerCase() + ": " + url)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("URL: " + url + "\nSchedule: " + scheduleType))
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Cancel", cancelPendingIntent);
notificationManager.notify(Constants.NOTIFICATION_ID_DOWNLOAD, builder.build());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if ("CANCEL_DOWNLOADS".equals(intent.getAction())) {
stopAllDownloads();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (progressReceiver != null) {
unregisterReceiver(progressReceiver);
}
}
private class DownloadProgressReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received broadcast: " + intent.getAction());
long downloadId = intent.getLongExtra("downloadId", -1);
String url = intent.getStringExtra("url");
if (DownloadService.ACTION_DOWNLOAD_PROGRESS.equals(intent.getAction())) {
int progress = intent.getIntExtra("progress", 0);
Log.d(TAG, "Progress update received for download " + downloadId + ": " + progress + "%");
// Ensure UI updates happen on main thread
runOnUiThread(() -> {
// Add progress view if not exists
if (!activeDownloads.containsKey(downloadId) && url != null) {
addProgressView(downloadId, url);
}
updateProgressView(downloadId, progress);
});
} else if (DownloadService.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
String error = intent.getStringExtra("error");
if (error != null) {
Log.e(TAG, "Download " + downloadId + " error: " + error);
runOnUiThread(() -> {
Toast.makeText(context, "Download failed: " + error, Toast.LENGTH_SHORT).show();
removeProgressView(downloadId);
});
} else {
long bytesDownloaded = intent.getLongExtra("bytesDownloaded", 0);
Log.d(TAG, "Download " + downloadId + " complete, bytes: " + bytesDownloaded);
totalBytesDownloaded += bytesDownloaded;
downloadCount++;
saveStats(); // Save stats after update
runOnUiThread(() -> {
updateStats();
// Remove progress view after a short delay to show 100%
binding.progressContainer.postDelayed(() -> removeProgressView(downloadId), 1000);
});
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.mattintech.androidtestapp;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import androidx.viewpager2.widget.ViewPager2;
import com.mattintech.androidtestapp.databinding.ActivityIntroBinding;
import com.google.android.material.tabs.TabLayoutMediator;
public class IntroActivity extends AppCompatActivity {
private ActivityIntroBinding binding;
private IntroPagerAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityIntroBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Setup edge-to-edge
setupWindowInsets();
setupViewPager();
setupButtons();
}
private void setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(v.getPaddingLeft(), insets.top, v.getPaddingRight(), insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
}
private void setupViewPager() {
adapter = new IntroPagerAdapter(this);
binding.viewPager.setAdapter(adapter);
// Connect TabLayout with ViewPager2
new TabLayoutMediator(binding.tabLayout, binding.viewPager,
(tab, position) -> {
// Empty - we just want the dots
}).attach();
// Update button visibility based on page
binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
updateButtonVisibility(position);
}
});
}
private void setupButtons() {
binding.btnNext.setOnClickListener(v -> {
int currentItem = binding.viewPager.getCurrentItem();
if (currentItem < adapter.getItemCount() - 1) {
binding.viewPager.setCurrentItem(currentItem + 1);
}
});
binding.btnSkip.setOnClickListener(v -> finishIntro());
binding.btnGetStarted.setOnClickListener(v -> finishIntro());
}
private void updateButtonVisibility(int position) {
boolean isLastPage = position == adapter.getItemCount() - 1;
binding.btnNext.setVisibility(isLastPage ? View.GONE : View.VISIBLE);
binding.btnSkip.setVisibility(isLastPage ? View.GONE : View.VISIBLE);
binding.btnGetStarted.setVisibility(isLastPage ? View.VISIBLE : View.GONE);
// Hide tab indicators on the last page
binding.tabLayout.setVisibility(isLastPage ? View.GONE : View.VISIBLE);
}
private void finishIntro() {
// Mark intro as seen
SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE);
prefs.edit().putBoolean("intro_completed", true).apply();
// Navigate to MainActivity
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}

View File

@@ -0,0 +1,95 @@
package com.mattintech.androidtestapp;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class IntroPagerAdapter extends RecyclerView.Adapter<IntroPagerAdapter.IntroViewHolder> {
private final Context context;
private final IntroSlide[] slides = {
new IntroSlide(
R.drawable.ic_launcher_foreground,
"Welcome to Android Test App",
"A comprehensive testing tool designed to push your Android device to its limits"
),
new IntroSlide(
R.drawable.ic_launcher_foreground,
"Battery Drain Testing",
"Test battery consumption with CPU, GPS, screen brightness, sensors, and more running simultaneously"
),
new IntroSlide(
R.drawable.ic_launcher_foreground,
"Crash & ANR Simulation",
"Trigger various types of crashes and Application Not Responding scenarios for testing"
),
new IntroSlide(
R.drawable.ic_launcher_foreground,
"Network Stress Testing",
"Schedule and execute concurrent file downloads to test network performance and reliability"
),
new IntroSlide(
R.drawable.ic_launcher_foreground,
"Ready to Test?",
"All tests can drain battery and may cause temporary device issues. Use responsibly!"
)
};
public IntroPagerAdapter(Context context) {
this.context = context;
}
@NonNull
@Override
public IntroViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_intro_slide, parent, false);
return new IntroViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull IntroViewHolder holder, int position) {
IntroSlide slide = slides[position];
holder.bind(slide);
}
@Override
public int getItemCount() {
return slides.length;
}
static class IntroViewHolder extends RecyclerView.ViewHolder {
private final ImageView imageView;
private final TextView titleView;
private final TextView descriptionView;
public IntroViewHolder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.ivSlideImage);
titleView = itemView.findViewById(R.id.tvSlideTitle);
descriptionView = itemView.findViewById(R.id.tvSlideDescription);
}
public void bind(IntroSlide slide) {
imageView.setImageResource(slide.imageResId);
titleView.setText(slide.title);
descriptionView.setText(slide.description);
}
}
static class IntroSlide {
final int imageResId;
final String title;
final String description;
IntroSlide(int imageResId, String title, String description) {
this.imageResId = imageResId;
this.title = title;
this.description = description;
}
}
}

View File

@@ -0,0 +1,245 @@
package com.mattintech.androidtestapp;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import com.mattintech.androidtestapp.databinding.ActivityMainBinding;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private static final int PERMISSION_REQUEST_CODE = 100;
private final String[] requiredPermissions = {
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.VIBRATE,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.WAKE_LOCK,
Manifest.permission.HIGH_SAMPLING_RATE_SENSORS,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.RECORD_AUDIO
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Setup edge-to-edge
setupWindowInsets();
setupButtons();
setVersionText();
checkAndRequestPermissions();
createNotificationChannels();
checkBatteryOptimization();
}
private void setupWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(v.getPaddingLeft(), insets.top, v.getPaddingRight(), insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
}
private void setupButtons() {
binding.btnBatteryDrain.setOnClickListener(v -> {
Intent intent = new Intent(this, BatteryDrainActivity.class);
startActivity(intent);
});
binding.btnBadBehavior.setOnClickListener(v -> {
Intent intent = new Intent(this, BadBehaviorActivity.class);
startActivity(intent);
});
binding.btnDownloader.setOnClickListener(v -> {
Intent intent = new Intent(this, DownloaderActivity.class);
startActivity(intent);
});
binding.btnInfo.setOnClickListener(v -> showInfoDialog());
}
private void setVersionText() {
try {
String version = "v" + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
binding.tvVersion.setText(version);
} catch (PackageManager.NameNotFoundException e) {
binding.tvVersion.setText("v1.0");
}
}
private void checkAndRequestPermissions() {
List<String> permissionsNeeded = new ArrayList<>();
// Add runtime permissions based on Android version
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(permission);
}
}
// Add Bluetooth permissions for Android 12+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH_SCAN);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.BLUETOOTH_CONNECT);
}
}
// Add notification permission for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
permissionsNeeded.add(Manifest.permission.POST_NOTIFICATIONS);
}
}
if (!permissionsNeeded.isEmpty()) {
ActivityCompat.requestPermissions(this,
permissionsNeeded.toArray(new String[0]), PERMISSION_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (!allGranted) {
Toast.makeText(this, "Some permissions were denied. App may not function properly.",
Toast.LENGTH_LONG).show();
}
}
}
private void showInfoDialog() {
String version = "";
try {
version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
version = "1.0";
}
new AlertDialog.Builder(this)
.setTitle("Android Test App")
.setMessage("A comprehensive testing tool for Android devices that simulates battery drain, crashes, ANRs, and network downloads to help developers test device behavior under stress conditions.")
.setPositiveButton("Report Issue / Request Feature", (dialog, which) -> {
String url = "https://github.com/mattintech/AndroidTestApp/issues";
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
})
.setNegativeButton("Close", null)
.show();
}
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = getSystemService(NotificationManager.class);
// Battery Drain channel
NotificationChannel batteryChannel = new NotificationChannel(
Constants.NOTIFICATION_CHANNEL_BATTERY,
"Battery Drain Test",
NotificationManager.IMPORTANCE_HIGH
);
batteryChannel.setDescription("Notifications for active battery drain tests");
batteryChannel.setShowBadge(false);
batteryChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
notificationManager.createNotificationChannel(batteryChannel);
// Downloads channel
NotificationChannel downloadChannel = new NotificationChannel(
Constants.NOTIFICATION_CHANNEL_DOWNLOAD,
"Downloads",
NotificationManager.IMPORTANCE_HIGH
);
downloadChannel.setDescription("Notifications for scheduled downloads");
downloadChannel.setShowBadge(false);
downloadChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
notificationManager.createNotificationChannel(downloadChannel);
// Bad Behavior channel
NotificationChannel badBehaviorChannel = new NotificationChannel(
Constants.NOTIFICATION_CHANNEL_BAD_BEHAVIOR,
"Bad Behavior",
NotificationManager.IMPORTANCE_HIGH
);
badBehaviorChannel.setDescription("Notifications for scheduled crashes/ANRs");
badBehaviorChannel.setShowBadge(false);
badBehaviorChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
notificationManager.createNotificationChannel(badBehaviorChannel);
}
}
private void checkBatteryOptimization() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
String packageName = getPackageName();
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
// Show dialog explaining why battery optimization should be disabled
new AlertDialog.Builder(this)
.setTitle("Disable Battery Optimization")
.setMessage("This app needs to run tests in the background without interruption.\n\n" +
"Battery optimization may stop or limit:\n" +
"• Long-running battery drain tests\n" +
"• Scheduled downloads\n" +
"• Background test services\n\n" +
"Please disable battery optimization for this app to ensure tests run properly.")
.setPositiveButton("Go to Settings", (dialog, which) -> {
try {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
startActivity(intent);
} catch (Exception e) {
// Fallback to battery optimization settings
Intent intent = new Intent();
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(intent);
}
})
.setNegativeButton("Not Now", (dialog, which) -> {
Toast.makeText(this, "Tests may be interrupted by battery optimization",
Toast.LENGTH_LONG).show();
})
.setCancelable(false)
.show();
}
}
}
}

View File

@@ -0,0 +1,40 @@
package com.mattintech.androidtestapp;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.graphics.Insets;
import com.mattintech.androidtestapp.databinding.ActivitySplashBinding;
public class SplashActivity extends AppCompatActivity {
private static final long SPLASH_DELAY = 2000; // 2 seconds
private ActivitySplashBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplashBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Check if this is first launch
SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE);
boolean introCompleted = prefs.getBoolean("intro_completed", false);
// Navigate to appropriate activity after delay
new Handler(Looper.getMainLooper()).postDelayed(() -> {
Intent intent;
if (introCompleted) {
intent = new Intent(SplashActivity.this, MainActivity.class);
} else {
intent = new Intent(SplashActivity.this, IntroActivity.class);
}
startActivity(intent);
finish();
}, SPLASH_DELAY);
}
}

View File

@@ -0,0 +1,518 @@
package com.mattintech.androidtestapp.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.WindowManager;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import com.mattintech.androidtestapp.Constants;
import com.mattintech.androidtestapp.MainActivity;
import com.mattintech.androidtestapp.BatteryDrainActivity;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BatteryDrainService extends Service implements SensorEventListener {
private static final String TAG = Constants.LOG_TAG + "BatteryDrainSvc";
private static final String CHANNEL_ID = Constants.NOTIFICATION_CHANNEL_BATTERY;
private static final int NOTIFICATION_ID = Constants.NOTIFICATION_ID_BATTERY;
private final IBinder binder = new BatteryDrainBinder();
private ExecutorService executorService;
private Handler handler;
private HandlerThread handlerThread;
// Components
private PowerManager.WakeLock wakeLock;
private CameraManager cameraManager;
private LocationManager locationManager;
private SensorManager sensorManager;
private Vibrator vibrator;
private AudioTrack audioTrack;
private WifiManager wifiManager;
private BluetoothAdapter bluetoothAdapter;
private BluetoothLeScanner bluetoothLeScanner;
// State
private boolean isRunning = false;
private long endTime = 0;
private boolean cpuEnabled, screenEnabled, flashlightEnabled, gpsEnabled;
private boolean bluetoothEnabled, wifiEnabled, networkEnabled, sensorsEnabled;
private boolean cameraEnabled, audioEnabled, vibrationEnabled;
// Threads
private Thread cpuThread;
private Thread networkThread;
public class BatteryDrainBinder extends Binder {
public BatteryDrainService getService() {
return BatteryDrainService.this;
}
}
@Override
public void onCreate() {
super.onCreate();
executorService = Executors.newFixedThreadPool(4);
handlerThread = new HandlerThread("BatteryDrainHandler");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
// Initialize managers
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "BatteryDrain:WakeLock");
cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
// Check if this is a stop action
if ("STOP".equals(intent.getAction())) {
Log.d(TAG, "Received stop action from notification");
stopSelf();
return START_NOT_STICKY;
}
cpuEnabled = intent.getBooleanExtra("cpu", false);
screenEnabled = intent.getBooleanExtra("screen", false);
flashlightEnabled = intent.getBooleanExtra("flashlight", false);
gpsEnabled = intent.getBooleanExtra("gps", false);
bluetoothEnabled = intent.getBooleanExtra("bluetooth", false);
wifiEnabled = intent.getBooleanExtra("wifi", false);
networkEnabled = intent.getBooleanExtra("network", false);
sensorsEnabled = intent.getBooleanExtra("sensors", false);
cameraEnabled = intent.getBooleanExtra("camera", false);
audioEnabled = intent.getBooleanExtra("audio", false);
vibrationEnabled = intent.getBooleanExtra("vibration", false);
int durationMinutes = intent.getIntExtra("duration", -1);
if (durationMinutes > 0) {
endTime = System.currentTimeMillis() + (durationMinutes * 60 * 1000);
} else {
endTime = 0; // Unlimited
}
Log.d(TAG, "Battery drain service starting with features:");
Log.d(TAG, " CPU: " + cpuEnabled);
Log.d(TAG, " Screen: " + screenEnabled);
Log.d(TAG, " Flashlight: " + flashlightEnabled);
Log.d(TAG, " GPS: " + gpsEnabled);
Log.d(TAG, " Bluetooth: " + bluetoothEnabled);
Log.d(TAG, " WiFi: " + wifiEnabled);
Log.d(TAG, " Network: " + networkEnabled);
Log.d(TAG, " Sensors: " + sensorsEnabled);
Log.d(TAG, " Camera: " + cameraEnabled);
Log.d(TAG, " Audio: " + audioEnabled);
Log.d(TAG, " Vibration: " + vibrationEnabled);
Log.d(TAG, " Duration: " + (durationMinutes == -1 ? "Unlimited" : durationMinutes + " minutes"));
startForeground(NOTIFICATION_ID, createNotification());
startDraining();
}
return START_STICKY;
}
private Notification createNotification() {
// Create proper back stack for notification
Intent mainIntent = new Intent(this, MainActivity.class);
Intent batteryIntent = new Intent(this, BatteryDrainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntent(mainIntent);
stackBuilder.addNextIntent(batteryIntent);
PendingIntent pendingIntent = stackBuilder.getPendingIntent(0,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
// Build active components list
StringBuilder activeComponents = new StringBuilder();
if (cpuEnabled) activeComponents.append("CPU, ");
if (screenEnabled) activeComponents.append("Screen, ");
if (flashlightEnabled) activeComponents.append("Flashlight, ");
if (gpsEnabled) activeComponents.append("GPS, ");
if (bluetoothEnabled) activeComponents.append("Bluetooth, ");
if (wifiEnabled) activeComponents.append("WiFi, ");
if (networkEnabled) activeComponents.append("Network, ");
if (sensorsEnabled) activeComponents.append("Sensors, ");
if (cameraEnabled) activeComponents.append("Camera, ");
if (audioEnabled) activeComponents.append("Audio, ");
if (vibrationEnabled) activeComponents.append("Vibration, ");
// Remove trailing comma and space
if (activeComponents.length() > 2) {
activeComponents.setLength(activeComponents.length() - 2);
}
String durationText;
if (endTime > 0) {
long remainingMillis = endTime - System.currentTimeMillis();
if (remainingMillis <= 0) {
durationText = "Duration: Completing...";
} else {
long minutes = remainingMillis / 60000;
long hours = minutes / 60;
minutes = minutes % 60;
if (hours > 0) {
durationText = String.format("Duration: %dh %dm remaining", hours, minutes);
} else {
durationText = String.format("Duration: %d min remaining", minutes);
}
}
} else {
durationText = "Duration: Unlimited";
}
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Battery Drain Test Active")
.setContentText(activeComponents.toString())
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(activeComponents.toString() + "\n" + durationText))
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop Test",
PendingIntent.getService(this, 0,
new Intent(this, BatteryDrainService.class).setAction("STOP"),
PendingIntent.FLAG_IMMUTABLE))
.build();
}
private void startDraining() {
isRunning = true;
wakeLock.acquire();
Log.d(TAG, "Starting battery drain features");
if (cpuEnabled) {
Log.d(TAG, "Starting CPU drain");
startCpuDrain();
}
if (screenEnabled) {
Log.d(TAG, "Starting screen drain");
startScreenDrain();
}
if (flashlightEnabled) {
Log.d(TAG, "Starting flashlight drain");
startFlashlightDrain();
}
if (gpsEnabled) {
Log.d(TAG, "Starting GPS drain");
startGpsDrain();
}
if (bluetoothEnabled) {
Log.d(TAG, "Starting Bluetooth drain");
startBluetoothDrain();
}
if (wifiEnabled) {
Log.d(TAG, "Starting WiFi drain");
startWifiDrain();
}
if (networkEnabled) {
Log.d(TAG, "Starting network drain");
startNetworkDrain();
}
if (sensorsEnabled) {
Log.d(TAG, "Starting sensors drain");
startSensorsDrain();
}
if (cameraEnabled) {
Log.d(TAG, "Starting camera drain");
startCameraDrain();
}
if (audioEnabled) {
Log.d(TAG, "Starting audio drain");
startAudioDrain();
}
if (vibrationEnabled) {
Log.d(TAG, "Starting vibration drain");
startVibrationDrain();
}
// Schedule check for duration and notification updates
if (endTime > 0) {
handler.postDelayed(this::checkDuration, 10000); // Check every 10 seconds
}
}
private void checkDuration() {
if (System.currentTimeMillis() >= endTime) {
Log.d(TAG, "Battery drain duration reached, stopping service");
stopSelf();
} else {
// Update notification every 10 seconds
updateNotification();
handler.postDelayed(this::checkDuration, 10000); // Check every 10 seconds
}
}
private void updateNotification() {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, createNotification());
}
private void startCpuDrain() {
cpuThread = new Thread(() -> {
while (isRunning) {
// Intensive mathematical calculations
double result = 0;
for (int i = 0; i < 1000000; i++) {
result += Math.sqrt(i) * Math.sin(i) * Math.cos(i);
}
}
});
cpuThread.start();
}
private void startScreenDrain() {
// Note: Screen brightness control requires WRITE_SETTINGS permission
// which requires user to manually grant in settings
// For now, we'll just keep the wake lock
wakeLock.acquire();
}
private void startFlashlightDrain() {
try {
String cameraId = cameraManager.getCameraIdList()[0];
cameraManager.setTorchMode(cameraId, true);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void startGpsDrain() {
if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0, // minimum time interval
0, // minimum distance
location -> {}, // Empty listener
handler.getLooper()
);
}
}
private void startBluetoothDrain() {
if (bluetoothLeScanner != null &&
checkSelfPermission(android.Manifest.permission.BLUETOOTH_SCAN)
== PackageManager.PERMISSION_GRANTED) {
bluetoothLeScanner.startScan(new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
// Do nothing with results
}
});
}
}
private void startWifiDrain() {
if (wifiManager != null) {
handler.post(new Runnable() {
@Override
public void run() {
if (isRunning) {
wifiManager.startScan();
handler.postDelayed(this, 1000);
}
}
});
}
}
private void startNetworkDrain() {
networkThread = new Thread(() -> {
while (isRunning) {
try {
URL url = new URL("https://www.google.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.getResponseCode();
connection.disconnect();
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
});
networkThread.start();
}
private void startSensorsDrain() {
List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
for (Sensor sensor : sensors) {
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
}
private void startCameraDrain() {
// Camera preview would require a surface view
// For battery drain, just opening camera is enough
try {
String[] cameraIds = cameraManager.getCameraIdList();
if (cameraIds.length > 0) {
// Camera opening would be done here with proper surface
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void startAudioDrain() {
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(audioFormat)
.setSampleRate(sampleRate)
.setChannelMask(channelConfig)
.build())
.setBufferSizeInBytes(bufferSize)
.build();
audioTrack.play();
// Generate silent audio
short[] buffer = new short[bufferSize];
new Thread(() -> {
while (isRunning) {
audioTrack.write(buffer, 0, buffer.length);
}
}).start();
}
private void startVibrationDrain() {
if (vibrator != null && vibrator.hasVibrator()) {
handler.post(new Runnable() {
@Override
public void run() {
if (isRunning) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(500,
VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(500);
}
handler.postDelayed(this, 600);
}
}
});
}
}
@Override
public void onSensorChanged(SensorEvent event) {
// Do nothing - just drain battery
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do nothing
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Battery drain service stopping");
isRunning = false;
// Stop all draining activities
if (cpuThread != null) cpuThread.interrupt();
if (networkThread != null) networkThread.interrupt();
if (flashlightEnabled) {
try {
String cameraId = cameraManager.getCameraIdList()[0];
cameraManager.setTorchMode(cameraId, false);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
if (gpsEnabled) {
locationManager.removeUpdates(location -> {});
}
if (bluetoothLeScanner != null && bluetoothEnabled) {
bluetoothLeScanner.stopScan(new ScanCallback() {});
}
if (sensorsEnabled) {
sensorManager.unregisterListener(this);
}
if (audioTrack != null) {
audioTrack.stop();
audioTrack.release();
}
if (wakeLock.isHeld()) {
wakeLock.release();
}
handler.removeCallbacksAndMessages(null);
handlerThread.quit();
executorService.shutdown();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

View File

@@ -0,0 +1,176 @@
package com.mattintech.androidtestapp.services;
import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import com.mattintech.androidtestapp.Constants;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DownloadService extends Service {
private static final String TAG = Constants.LOG_TAG + "DownloadService";
public static final String ACTION_DOWNLOAD_PROGRESS = "com.mattintech.DOWNLOAD_PROGRESS";
public static final String ACTION_DOWNLOAD_COMPLETE = "com.mattintech.DOWNLOAD_COMPLETE";
private ExecutorService executorService;
private Handler mainHandler;
@Override
public void onCreate() {
super.onCreate();
executorService = Executors.newFixedThreadPool(10);
mainHandler = new Handler(Looper.getMainLooper());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
String url = intent.getStringExtra("url");
boolean saveFile = intent.getBooleanExtra("saveFile", false);
long downloadId = intent.getLongExtra("downloadId", System.currentTimeMillis());
if (url != null) {
executorService.execute(() -> downloadFile(url, saveFile, downloadId));
}
}
return START_NOT_STICKY;
}
private void downloadFile(String urlString, boolean saveFile, long downloadId) {
HttpURLConnection connection = null;
InputStream inputStream = null;
FileOutputStream outputStream = null;
Log.d(TAG, "Starting download: " + urlString);
try {
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
Log.e(TAG, "HTTP error code: " + responseCode);
sendDownloadError(downloadId, "HTTP error: " + responseCode, urlString);
return;
}
int fileLength = connection.getContentLength();
Log.d(TAG, "File length: " + fileLength + " bytes");
inputStream = connection.getInputStream();
File outputFile = null;
if (saveFile) {
File downloadsDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS);
if (!downloadsDir.exists()) {
downloadsDir.mkdirs();
}
outputFile = new File(downloadsDir, "download_" + downloadId + ".tmp");
outputStream = new FileOutputStream(outputFile);
}
byte[] buffer = new byte[4096];
long total = 0;
int count;
long lastProgressUpdate = 0;
while ((count = inputStream.read(buffer)) != -1) {
total += count;
if (saveFile && outputStream != null) {
outputStream.write(buffer, 0, count);
}
// Update progress more frequently - every 10KB for smaller files, 100KB for larger
long updateInterval = fileLength < 1048576 ? 10240 : 102400; // 10KB if < 1MB, else 100KB
if (fileLength > 0 && (total - lastProgressUpdate) > updateInterval) {
lastProgressUpdate = total;
int progress = (int) ((total * 100) / fileLength);
Log.d(TAG, "Progress: " + progress + "% (" + total + "/" + fileLength + ")");
sendProgressUpdate(downloadId, progress, urlString);
}
}
// Send final 100% progress update
if (fileLength > 0) {
Log.d(TAG, "Download complete, sending 100% progress");
sendProgressUpdate(downloadId, 100, urlString);
}
// Send completion
Log.d(TAG, "Download finished, total bytes: " + total);
sendDownloadComplete(downloadId, total, urlString);
} catch (IOException e) {
Log.e(TAG, "Download failed", e);
sendDownloadError(downloadId, "Download failed: " + e.getMessage(), urlString);
} finally {
try {
if (outputStream != null) outputStream.close();
if (inputStream != null) inputStream.close();
if (connection != null) connection.disconnect();
} catch (IOException e) {
Log.e(TAG, "Error closing streams", e);
}
}
}
private void sendProgressUpdate(long downloadId, int progress, String url) {
Intent intent = new Intent(ACTION_DOWNLOAD_PROGRESS);
intent.setPackage(getPackageName()); // Restrict to this app
intent.putExtra("downloadId", downloadId);
intent.putExtra("progress", progress);
intent.putExtra("url", url);
Log.d(TAG, "Sending progress broadcast for download " + downloadId + ": " + progress + "%");
sendBroadcast(intent);
}
private void sendDownloadComplete(long downloadId, long bytesDownloaded, String url) {
Intent intent = new Intent(ACTION_DOWNLOAD_COMPLETE);
intent.setPackage(getPackageName()); // Restrict to this app
intent.putExtra("downloadId", downloadId);
intent.putExtra("bytesDownloaded", bytesDownloaded);
intent.putExtra("url", url);
Log.d(TAG, "Sending download complete broadcast for download " + downloadId + ": " + bytesDownloaded + " bytes");
sendBroadcast(intent);
}
private void sendDownloadError(long downloadId, String error, String url) {
Intent intent = new Intent(ACTION_DOWNLOAD_COMPLETE);
intent.setPackage(getPackageName()); // Restrict to this app
intent.putExtra("downloadId", downloadId);
intent.putExtra("error", error);
intent.putExtra("bytesDownloaded", 0L);
intent.putExtra("url", url);
Log.d(TAG, "Sending download error broadcast for download " + downloadId + ": " + error);
sendBroadcast(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
if (executorService != null) {
executorService.shutdownNow();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@@ -0,0 +1,36 @@
package com.mattintech.androidtestapp.workers;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.mattintech.androidtestapp.services.DownloadService;
public class DownloadWorker extends Worker {
public DownloadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
String url = getInputData().getString("url");
boolean saveFile = getInputData().getBoolean("saveFile", false);
boolean background = getInputData().getBoolean("background", false);
if (url == null || url.isEmpty()) {
return Result.failure();
}
// Start download service
Intent intent = new Intent(getApplicationContext(), DownloadService.class);
intent.putExtra("url", url);
intent.putExtra("saveFile", saveFile);
intent.putExtra("downloadId", System.currentTimeMillis());
getApplicationContext().startService(intent);
return Result.success();
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/primary" />
<corners android:radius="4dp" />
<size
android:width="24dp"
android:height="8dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#40000000" />
<corners android:radius="4dp" />
<size
android:width="8dp"
android:height="8dp" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Gradient background -->
<path
android:fillColor="#E3F2FD"
android:pathData="M0,0h108v108h-108z" />
<!-- Subtle pattern -->
<path
android:fillColor="#BBDEFB"
android:pathData="M0,0 L54,0 L54,54 L0,54 Z" />
<path
android:fillColor="#BBDEFB"
android:pathData="M54,54 L108,54 L108,108 L54,108 Z" />
</vector>

View File

@@ -0,0 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Phone/Device shape -->
<path
android:fillColor="#212121"
android:pathData="M40,30 L68,30 Q72,30 72,34 L72,74 Q72,78 68,78 L40,78 Q36,78 36,74 L36,34 Q36,30 40,30 Z"/>
<!-- Screen -->
<path
android:fillColor="#FFFFFF"
android:pathData="M39,36 L69,36 L69,68 L39,68 Z"/>
<!-- Lightning bolt (stress indicator) -->
<path
android:fillColor="#FF5722"
android:pathData="M56,44 L48,56 L52,56 L52,64 L60,52 L56,52 Z"/>
<!-- Warning indicator top right -->
<path
android:fillColor="#FFC107"
android:pathData="M62,40 L66,48 L58,48 Z"/>
<path
android:fillColor="#212121"
android:pathData="M61.5,44 L62.5,44 L62.5,46 L61.5,46 Z"/>
<path
android:fillColor="#212121"
android:pathData="M62,46.5 m-0.7,0 a0.7,0.7 0 1,1 1.4,0 a0.7,0.7 0 1,1 -1.4,0"/>
<!-- Home button -->
<path
android:fillColor="#616161"
android:pathData="M54,72 m-2,0 a2,2 0 1,1 4,0 a2,2 0 1,1 -4,0"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/dot_active" android:state_selected="true" />
<item android:drawable="@drawable/dot_inactive" />
</selector>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="@color/background"
tools:context=".BadBehaviorActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="Bad Behavior Test"
app:titleTextColor="@color/text_primary"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="@color/bad_behavior" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Foreground Notice -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📱 App must be in foreground for crashes/ANRs to trigger"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/primary"
android:gravity="center"
android:padding="12dp"
android:background="@color/surface"
android:layout_marginBottom="16dp" />
<!-- Crash Section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Crash Type:"
android:textStyle="bold"
android:layout_marginTop="8dp" />
<Spinner
android:id="@+id/spinnerCrashType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnTriggerCrash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Trigger Crash"
android:backgroundTint="@android:color/holo_orange_dark"
android:layout_marginBottom="24dp" />
<!-- ANR Section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ANR Type:"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerAnrType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnTriggerAnr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Trigger ANR"
android:backgroundTint="@android:color/holo_red_dark"
android:layout_marginBottom="24dp" />
<!-- Random Mode Section -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
android:layout_marginVertical="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Random Mode"
android:textStyle="bold"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Frequency:"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerFrequency"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnStartRandom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Start Random"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnStopRandom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Stop Random"
android:backgroundTint="@android:color/holo_red_dark" />
</LinearLayout>
<Button
android:id="@+id/btnViewScheduled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="View Scheduled Crashes"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚠️ IMPORTANT: Crashes/ANRs only occur when app is in foreground!\n• Keep this screen open for tests to work\n• Random mode: crashes only (no ANRs) for unattended testing\n• Random mode: interferes with ALL other tests\n• App auto-restarts after crashes\n• Random mode stops after 24 hours"
android:textSize="12sp"
android:textColor="@android:color/holo_red_dark"
android:layout_marginTop="8dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,309 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="@color/background"
tools:context=".BatteryDrainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="Battery Drain Test"
app:titleTextColor="@color/text_primary"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="@color/battery_drain" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Configuration Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Configuration"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="16dp" />
<!-- Duration Section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Duration"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:layout_marginBottom="4dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_marginBottom="16dp">
<AutoCompleteTextView
android:id="@+id/spinnerDuration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Presets Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quick Presets"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="12dp" />
<com.google.android.material.chip.ChipGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="false">
<com.google.android.material.chip.Chip
android:id="@+id/btnPresetMax"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Maximum"
app:chipBackgroundColor="@color/battery_drain"
android:textColor="@color/white"
style="@style/Widget.Material3.Chip.Suggestion" />
<com.google.android.material.chip.Chip
android:id="@+id/btnPresetCpuGpu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CPU/GPU"
style="@style/Widget.Material3.Chip.Suggestion" />
<com.google.android.material.chip.Chip
android:id="@+id/btnPresetRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Radio"
style="@style/Widget.Material3.Chip.Suggestion" />
<com.google.android.material.chip.Chip
android:id="@+id/btnPresetSensors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sensors"
style="@style/Widget.Material3.Chip.Suggestion" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Components Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Components"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="12dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbCpu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="CPU Intensive Operations"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbScreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Screen (Max Brightness)"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbFlashlight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Flashlight"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbGps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="GPS Location"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbBluetooth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bluetooth Scanning"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbWifi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="WiFi Scanning"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbNetwork"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Network Activity"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbSensors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sensors"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Camera Preview"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbAudio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Audio Playback"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbVibration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vibration"
android:textColor="@color/text_primary" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Control Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStart"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:text="Start Test"
style="@style/Widget.App.Button"
android:backgroundTint="@color/success"
android:layout_marginEnd="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStop"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:text="Stop Test"
style="@style/Widget.App.Button"
android:backgroundTint="@color/error" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnViewActiveTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="View Active Test"
style="@style/Widget.App.Button.Outlined"
android:layout_marginBottom="32dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="@color/background"
tools:context=".DownloaderActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="Download Configuration"
app:titleTextColor="@color/text_primary"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationIconTint="@color/downloader" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- URL Section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Download URL:"
android:textStyle="bold" />
<EditText
android:id="@+id/etUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter URL"
android:inputType="textUri"
android:enabled="false"
android:layout_marginBottom="8dp" />
<!-- File Size Selection -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="File Size:"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerFileSize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Schedule Type -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Schedule Type:"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerSchedule"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Network Preference -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Network Preference:"
android:textStyle="bold" />
<Spinner
android:id="@+id/spinnerNetwork"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Concurrent Downloads -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Concurrent Downloads:"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<SeekBar
android:id="@+id/seekBarConcurrent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tvConcurrentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="16sp"
android:layout_marginStart="8dp" />
</LinearLayout>
<!-- Options -->
<CheckBox
android:id="@+id/cbSaveFile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save file after download"
android:checked="false" />
<CheckBox
android:id="@+id/cbBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Continue download in background"
android:checked="true"
android:layout_marginBottom="16dp" />
<!-- Control Buttons -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStartDownload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Download"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnStopAll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Stop All"
android:backgroundTint="@android:color/holo_red_dark"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnClearHistory"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Clear History" />
</LinearLayout>
<Button
android:id="@+id/btnViewScheduled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="View Scheduled Downloads"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
<!-- Progress Section -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
android:layout_marginVertical="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Active Downloads:"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<!-- Container for dynamic progress bars -->
<LinearLayout
android:id="@+id/progressContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp" />
<!-- Placeholder when no downloads -->
<TextView
android:id="@+id/tvNoDownloads"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No active downloads"
android:layout_gravity="center_horizontal"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="16dp" />
<!-- Stats -->
<TextView
android:id="@+id/tvStats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Downloads: 0 | Total: 0 B"
android:textSize="14sp"
android:layout_gravity="center_horizontal" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:fitsSystemWindows="false"
tools:context=".IntroActivity">
<!-- ViewPager for slides -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/tabLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Tab dots indicator -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:background="@android:color/transparent"
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
app:tabPaddingStart="8dp"
app:tabPaddingEnd="8dp"
app:tabMinWidth="0dp"
app:layout_constraintBottom_toTopOf="@+id/btnNext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- Skip button -->
<Button
android:id="@+id/btnSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:text="Skip"
style="@style/Widget.Material3.Button.TextButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- Next button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:text="Next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Get Started button (hidden initially) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnGetStarted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:text="Get Started"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,272 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
tools:context=".MainActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
<!-- Header Section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="60dp"
android:paddingBottom="32dp">
<!-- Info button in top right -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnInfo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginBottom="8dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
android:padding="0dp"
android:text="ⓘ"
android:textSize="24sp"
android:textColor="@color/text_secondary"
style="@style/Widget.Material3.Button.TextButton" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Device Stress Tester"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:fontFamily="sans-serif-medium" />
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test your device limits"
android:textSize="18sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- Feature Cards -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:cardBackgroundColor="@color/surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Battery Drain Test"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Maximize power consumption"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBatteryDrain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
style="@style/Widget.App.Button.Battery"
android:layout_gravity="center_vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:cardBackgroundColor="@color/surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bad Behavior Test"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Trigger crashes and ANRs"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBadBehavior"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
style="@style/Widget.App.Button.BadBehavior"
android:layout_gravity="center_vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:cardBackgroundColor="@color/surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Downloader Test"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Schedule file downloads"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDownloader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
style="@style/Widget.App.Button.Downloader"
android:layout_gravity="center_vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Warning Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:cardBackgroundColor="#FFF3E0">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_launcher_foreground"
android:layout_marginEnd="12dp"
app:tint="@color/warning" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="These tests can drain battery, cause crashes, and use mobile data"
android:textSize="14sp"
android:textColor="@color/text_primary" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
<!-- Version number in bottom right -->
<TextView
android:id="@+id/tvVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="v1.0"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:alpha="0.6" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary"
android:fitsSystemWindows="false"
tools:context=".SplashActivity">
<!-- App Icon -->
<ImageView
android:id="@+id/ivAppIcon"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toTopOf="@+id/tvAppName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<!-- App Name -->
<TextView
android:id="@+id/tvAppName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Android Test App"
android:textColor="@android:color/white"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/tvTagline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivAppIcon" />
<!-- Tagline -->
<TextView
android:id="@+id/tvTagline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Device Stress Testing Tool"
android:textColor="@android:color/white"
android:textSize="16sp"
android:alpha="0.8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvAppName" />
<!-- Loading indicator -->
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:indeterminateTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvDownloadId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Download #1"
android:textSize="14sp" />
<TextView
android:id="@+id/tvDownloadProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0%"
android:textSize="14sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBarDownload"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<!-- Slide Image -->
<ImageView
android:id="@+id/ivSlideImage"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="48dp"
android:scaleType="centerInside" />
<!-- Slide Title -->
<TextView
android:id="@+id/tvSlideTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Title"
android:textColor="@color/text_primary"
android:textSize="24sp"
android:textStyle="bold" />
<!-- Slide Description -->
<TextView
android:id="@+id/tvSlideDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:text="Description"
android:textColor="@color/text_secondary"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -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="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -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="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidTestApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Primary Colors -->
<color name="primary">#FF1976D2</color>
<color name="primary_dark">#FF1565C0</color>
<color name="primary_light">#FF42A5F5</color>
<!-- Secondary Colors -->
<color name="secondary">#FFFF6B6B</color>
<color name="secondary_dark">#FFE85454</color>
<color name="secondary_light">#FFFF8787</color>
<!-- Background Colors -->
<color name="background">#FFF5F7FA</color>
<color name="surface">#FFFFFFFF</color>
<color name="surface_variant">#FFF8F9FC</color>
<!-- Text Colors -->
<color name="text_primary">#FF2C3E50</color>
<color name="text_secondary">#FF7F8C8D</color>
<color name="text_hint">#FFB0BEC5</color>
<!-- Status Colors -->
<color name="success">#FF4CAF50</color>
<color name="warning">#FFFF9800</color>
<color name="error">#FFF44336</color>
<color name="info">#FF2196F3</color>
<!-- Feature Colors -->
<color name="battery_drain">#FFFF5722</color>
<color name="bad_behavior">#FFE91E63</color>
<color name="downloader">#FF00BCD4</color>
<!-- Default Colors -->
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AndroidTestApp</string>
</resources>

View File

@@ -0,0 +1,104 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidTestApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/secondary</item>
<item name="colorSecondaryVariant">@color/secondary_dark</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Background colors -->
<item name="android:colorBackground">@color/background</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/text_primary</item>
<!-- Status bar and navigation bar -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
<!-- Enable edge-to-edge -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:fitsSystemWindows">false</item>
<!-- Text appearances -->
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="android:textColorHint">@color/text_hint</item>
<!-- Window background -->
<item name="android:windowBackground">@color/background</item>
<!-- Material 3 components -->
<item name="materialCardViewStyle">@style/Widget.App.CardView</item>
<item name="materialButtonStyle">@style/Widget.App.Button</item>
<item name="textInputStyle">@style/Widget.App.TextInputLayout</item>
</style>
<!-- Custom Card Style -->
<style name="Widget.App.CardView" parent="Widget.Material3.CardView.Elevated">
<item name="cardCornerRadius">16dp</item>
<item name="cardElevation">4dp</item>
<item name="cardBackgroundColor">@color/surface</item>
<item name="contentPadding">16dp</item>
</style>
<!-- Custom Button Style -->
<style name="Widget.App.Button" parent="Widget.Material3.Button">
<item name="cornerRadius">12dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:letterSpacing">0.02</item>
<item name="android:paddingTop">14dp</item>
<item name="android:paddingBottom">14dp</item>
</style>
<!-- Custom Button Outlined -->
<style name="Widget.App.Button.Outlined" parent="Widget.Material3.Button.OutlinedButton">
<item name="cornerRadius">12dp</item>
<item name="android:textAllCaps">false</item>
<item name="strokeColor">@color/primary</item>
<item name="strokeWidth">2dp</item>
</style>
<!-- Text Input Style -->
<style name="Widget.App.TextInputLayout" parent="Widget.Material3.TextInputLayout.OutlinedBox">
<item name="boxCornerRadiusTopStart">12dp</item>
<item name="boxCornerRadiusTopEnd">12dp</item>
<item name="boxCornerRadiusBottomStart">12dp</item>
<item name="boxCornerRadiusBottomEnd">12dp</item>
</style>
<!-- Feature Button Styles -->
<style name="Widget.App.Button.Battery" parent="Widget.App.Button">
<item name="backgroundTint">@color/battery_drain</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="Widget.App.Button.BadBehavior" parent="Widget.App.Button">
<item name="backgroundTint">@color/bad_behavior</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="Widget.App.Button.Downloader" parent="Widget.App.Button">
<item name="backgroundTint">@color/downloader</item>
<item name="android:textColor">@color/white</item>
</style>
<!-- Full Screen Splash Theme -->
<style name="Theme.AndroidTestApp.Splash" parent="Theme.AndroidTestApp">
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@color/primary</item>
<item name="android:statusBarColor">@color/primary</item>
<item name="android:navigationBarColor">@color/primary</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.mattintech.androidtestapp;
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() {
assertEquals(4, 2 + 2);
}
}