initial commit
@@ -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());
|
||||
}
|
||||
}
|
||||
98
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/dot_active.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/dot_inactive.xml
Normal 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>
|
||||
20
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
37
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/tab_selector.xml
Normal 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>
|
||||
167
app/src/main/res/layout/activity_bad_behavior.xml
Normal 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>
|
||||
309
app/src/main/res/layout/activity_battery_drain.xml
Normal 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>
|
||||
225
app/src/main/res/layout/activity_downloader.xml
Normal 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>
|
||||
72
app/src/main/res/layout/activity_intro.xml
Normal 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>
|
||||
272
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
66
app/src/main/res/layout/activity_splash.xml
Normal 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>
|
||||
38
app/src/main/res/layout/item_download_progress.xml
Normal 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>
|
||||
40
app/src/main/res/layout/item_intro_slide.xml
Normal 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>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal 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>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
16
app/src/main/res/values-night/themes.xml
Normal 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>
|
||||
42
app/src/main/res/values/colors.xml
Normal 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>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">AndroidTestApp</string>
|
||||
</resources>
|
||||
104
app/src/main/res/values/themes.xml
Normal 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>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||