├── .gitattributes ├── .github └── workflows │ └── android.yml ├── .gitignore ├── CONTRIBUTE.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── app ├── .gitignore ├── build.gradle.kts ├── build │ └── outputs │ │ └── apk │ │ └── debug │ │ └── output-metadata.json ├── proguard-rules.pro ├── schemas │ └── de.swiftbird.elasticandroid.AppDatabase │ │ ├── 18.json │ │ ├── 19.json │ │ ├── 20.json │ │ ├── 21.json │ │ ├── 22.json │ │ ├── 23.json │ │ ├── 24.json │ │ ├── 25.json │ │ ├── 26.json │ │ ├── 27.json │ │ ├── 28.json │ │ ├── 29.json │ │ ├── 30.json │ │ └── 31.json └── src │ ├── androidTest │ └── java │ │ └── de │ │ └── swiftbird │ │ └── elasticandroid │ │ └── AppInteractionTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── de │ │ │ └── swiftbird │ │ │ └── elasticandroid │ │ │ ├── AckRequest.java │ │ │ ├── AckResponse.java │ │ │ ├── AgentMetadata.java │ │ │ ├── AppBroadcastReceiver.java │ │ │ ├── AppConverters.java │ │ │ ├── AppDatabase.java │ │ │ ├── AppDeviceAdminReceiver.java │ │ │ ├── AppEnrollRequest.java │ │ │ ├── AppInstance.java │ │ │ ├── AppLog.java │ │ │ ├── AppSecurePreferences.java │ │ │ ├── AppStatisticsData.java │ │ │ ├── AppStatisticsDataDAO.java │ │ │ ├── Component.java │ │ │ ├── ComponentFactory.java │ │ │ ├── ComponentWorker.java │ │ │ ├── DetailsActivity.java │ │ │ ├── ElasticApi.java │ │ │ ├── ElasticDocument.java │ │ │ ├── ElasticResponse.java │ │ │ ├── ElasticWorker.java │ │ │ ├── FleetApi.java │ │ │ ├── FleetCheckinRepository.java │ │ │ ├── FleetCheckinRequest.java │ │ │ ├── FleetCheckinResponse.java │ │ │ ├── FleetCheckinWorker.java │ │ │ ├── FleetEnrollActivity.java │ │ │ ├── FleetEnrollData.java │ │ │ ├── FleetEnrollDataDAO.java │ │ │ ├── FleetEnrollRepository.java │ │ │ ├── FleetEnrollRequest.java │ │ │ ├── FleetEnrollResponse.java │ │ │ ├── FleetStatusResponse.java │ │ │ ├── HelpActivity.java │ │ │ ├── LegalActivity.java │ │ │ ├── LicenseActivity.java │ │ │ ├── LocationComp.java │ │ │ ├── LocationCompBuffer.java │ │ │ ├── LocationCompDocument.java │ │ │ ├── LocationForegroundService.java │ │ │ ├── LocationReceiver.java │ │ │ ├── MainActivity.java │ │ │ ├── NetworkBuilder.java │ │ │ ├── NetworkLogsComp.java │ │ │ ├── NetworkLogsCompBuffer.java │ │ │ ├── NetworkLogsCompDocument.java │ │ │ ├── PermissionRequestActivity.java │ │ │ ├── PolicyData.java │ │ │ ├── PolicyDataDAO.java │ │ │ ├── SecurityLogsComp.java │ │ │ ├── SecurityLogsCompBuffer.java │ │ │ ├── SecurityLogsCompDocument.java │ │ │ ├── SelfLogComp.java │ │ │ ├── SelfLogCompBuffer.java │ │ │ ├── SelfLogCompDocument.java │ │ │ ├── StatusCallback.java │ │ │ └── WorkScheduler.java │ └── res │ │ ├── drawable │ │ ├── bordered_background.xml │ │ ├── edgy_notch_shape.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── icon.png │ │ ├── layout │ │ ├── activity_details.xml │ │ ├── activity_enrollment.xml │ │ ├── activity_help.xml │ │ ├── activity_legal.xml │ │ ├── activity_licenses.xml │ │ └── activity_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-v23 │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── device_admin.xml │ └── test │ └── java │ └── de │ └── swiftbird │ └── elasticandroid │ └── FleetEnrollRepositoryTest.java ├── build.gradle.kts ├── create_enrollment_string.sh ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── logo_elastic_agent.png └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | 29 | - name: Generate Debug and Release APK 30 | run: | 31 | ./gradlew assembleDebug 32 | ./gradlew assembleRelease 33 | 34 | - name: Upload APK 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: elastic-agent-android.apk 38 | path: app/build/outputs/apk/debug/app-debug.apk 39 | 40 | 41 | test: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v3 46 | - name: set up JDK 11 47 | uses: actions/setup-java@v3 48 | with: 49 | java-version: '17' 50 | distribution: 'temurin' 51 | cache: gradle 52 | 53 | - name: Grant execute permission for gradlew 54 | run: chmod +x gradlew 55 | - name: Run unit tests 56 | run: ./gradlew test 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in the build directory 2 | app/build/ 3 | # But do not ignore the apk directory and its contents 4 | !app/build/outputs/apk/* 5 | 6 | # Ignore Gradle system files 7 | .gradle 8 | 9 | # Ignore Gradle GUI config 10 | gradle-app.setting 11 | 12 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 13 | !gradle-wrapper.jar 14 | 15 | 16 | # Cache of project 17 | .gradletasknamecache 18 | 19 | # Eclipse Gradle plugin generated files 20 | .project 21 | .classpath 22 | 23 | # Environment files 24 | *.env 25 | .env 26 | 27 | # macOS filesystem file 28 | .DS_Store 29 | 30 | # IntelliJ IDEA / Android Studio project files 31 | .idea/ 32 | 33 | *.crt -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | We welcome contributions to this project. Please read the following guidelines. 4 | 5 | ## Guidelines 6 | We follow the "fork-and-pull" Git workflow. Here are the steps: 7 | 1. **Fork** the repo on GitHub. 8 | 2. **Clone** the project to your own machine. 9 | 3. **Commit** changes to your own branch. 10 | 4. **Push** your work back up to your fork. 11 | 5. Submit a **Pull Request** so we can review your changes. 12 | 13 | ## Setting up the project 14 | - **Clone** the repository. 15 | - **Install** Android Studio (if you haven't already). 16 | - **Open** the project in Android Studio. 17 | - Ensure the project **builds without errors** (dependencies should download automatically). 18 | - You can now use the Android emulator or a physical device to **run the app**. 19 | 20 | ## Architecture Overview 21 | 22 | Below is an overview of the architecture of the Android app to help you understand the codebase better. 23 | 24 | ### Background Workers 25 | 26 | The application leverages Android's WorkManager API to manage periodic tasks necessary for its operation. 27 | There are two primary background workers that are initially started right after enrollment: `FleetCheckinWorker` and `ElasticWorker`. 28 | These workers are responsible for interacting with Fleet and Elasticsearch to ensure that the device is compliant with the current policy and that data is being sent to the server. 29 | 30 | #### FleetCheckinWorker 31 | 32 | ##### Purpose 33 | The `FleetCheckinWorker` is tasked with periodic check-ins with the Fleet server. These check-ins are crucial for: 34 | - Receiving new policy updates which might include changes in data collection frequency, data types collected (internally "paths"), or security parameters (api keys, certificates). 35 | - Updating the local policy data with any new configurations received from the Fleet server (Fleet Checkin -> PolicyData). 36 | - Confirming the device's compliance with the current policies (Fleet Ack). 37 | 38 | ##### Implementation Details 39 | - **Trigger**: This worker runs at intervals specified by the `checkin_interval` field in the policy. 40 | - **Data Handling**: During each run, it fetches the current enrollment details from the `EnrollmentData` database to authenticate and establish a connection with the Fleet server. The necessary details include the server URL and certificates for a secure connection. 41 | - **Execution**: On execution, it sends a check-in request and waits for a response. Upon receiving a new policy, it updates the local database with the new settings and reschedules itself if the `checkin_interval` has changed. Also it Acknowledges possible policy changes to the Fleet server. 42 | 43 | #### ElasticWorker 44 | 45 | ##### Purpose 46 | The `ElasticWorker` handles the aggregation and transmission of logged data to an Elasticsearch server. Its responsibilities include: 47 | - Collecting data entries from various components within the app which may include logs, metrics, or operational events (modular data collection). 48 | - Ensuring that the data is formatted correctly, batched, and sent to the Elasticsearch server at regular intervals. 49 | 50 | ##### Implementation Details 51 | - **Trigger**: Operates at intervals defined by the `put_interval` in the policy. 52 | - **Data Collection**: Utilizes the `ComponentFactory` to access individual component buffers, each potentially holding data of type `ElasticDocument` which represent the documents for Elasticsearch. 53 | - **Data Processing**: Before transmission, it combines data entries into larger batches to minimize network requests and align with Elasticsearch's bulk upload API. 54 | - 55 | The `WorkScheduler` class manages the scheduling and re-scheduling of these workers based on the current policy settings. 56 | 57 | >Note: It is not possible to use periodic tasks with intervals less than 15 minutes on Android 12 and above, which is why this app uses "worker chaining" to achieve the desired behavior. 58 | 59 | ### Class Structure 60 | The app classes are organized into several functional areas: 61 | 62 | - **FleetEnrollmentX**: Manages the enrollment process to fleet. 63 | - **FleetCheckinX**: Manages the check-in process to fleet. 64 | - **AppX**: Handles general app functionality (e.g., database, converters). 65 | - **ComponentNameX**: Manages components that fetch various data from the device and send these data to an internal buffer (which is then emptied by the ElasticWorker mentioned above). 66 | 67 | Within each functional area, the classes are further organized into the following categories: 68 | 69 | - **XActivity**: UI classes displaying data to the user. 70 | - **XRepository**: Classes handling business logic, typically linked to an Activity. 71 | - **XRequest/XResponse**: Retrofit classes for API call requests and responses. 72 | - **XWorker**: Background task handlers (see above). 73 | - **XComp**: Implementations of the `Component` interface. 74 | - **XData**: Classes defining the Room database structure for local data storage. 75 | 76 | ## Developing a New Component 77 | To develop a new component: 78 | 1. **Implement the `Component` interface.** 79 | 2. **Extend the `ElasticDocument` class** to represent the data your component will send to Elasticsearch. 80 | 3. **Add your component to the `ComponentFactory` class** to ensure it is used by `ElasticWorker`. 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Martin Offermann (@swiftbird07) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elastic Agent Android (Unofficial) 2 | [![Android CI](https://github.com/swiftbird07/elastic-agent-android/actions/workflows/android.yml/badge.svg)](https://github.com/swiftbird07/elastic-agent-android/actions/workflows/android.yml) 3 | ![Elastic Agent Android Logo](logo_elastic_agent.png) 4 | 5 | 6 | Elastic Agent Android is an unofficial implementation of the Elastic Agent for Android devices, bringing the powerful observability and management features of the Elastic Stack to the Android ecosystem. With Elastic Agent Android, you can enroll your Android devices into a Fleet server and start collecting a wide range of data directly into Elasticsearch, allowing for real-time monitoring, alerting, and analysis. 7 | 8 | ## Use Cases 9 | 10 | Elastic Agent Android aims to extend the powerful features of Elastic Observability and Security to mobile devices, providing detailed insights and security monitoring for Android devices. Whether you're managing a fleet of corporate devices or looking for a way to integrate mobile device data into your Elastic Stack, Elastic Agent Android offers a versatile and powerful solution. 11 | 12 | ## Features 13 | 14 | Elastic Agent Android supports a variety of components that collect different types of data from the Android device, including: 15 | 16 | - **Location:** Sends periodic location updates to Elasticsearch, with configurable intervals. 17 | - **Network Logs:** Collects network logs (DNS, TCP connections) provided by the Android OS. 18 | - **Security Logs:** Gathers security-related logs, like app (un-) installation, failed PIN attempts etc. 19 | 20 | 21 | >**Note**: Currently not working on any tested devices. See Issue [#01](https://github.com/swiftbird07/elastic-agent-android/issues/1). 22 | - **Self Log:** Logs the agent's own operational logs for diagnostics and monitoring. 23 | 24 | > **Note:** The Network Logs and Security Logs components require the device to be configured as a device owner. Instructions on how to do that can be found [here](https://github.com/swiftbird07/elastic-agent-android/wiki/Register-App-as-Device-Owner). 25 | 26 | ## Compatibility 27 | 28 | Elastic Agent Android is designed to work with Android devices running **Android 7.0 (Nougat) and above**. 29 | The app is built using the latest Android SDK and follows best practices for compatibility and performance. 30 | 31 | To enroll the agent, you will need a Fleet server running **Elastic / Fleet 8.10.2 or later** (possibly earlier versions, but not tested). 32 | 33 | ## Quick Start 34 | 35 | To get started with Elastic Agent Android, follow these steps: 36 | 37 | ### 1. Download the APK 38 | Download the latest APK from the [Artifacts section](https://github.com/swiftbird07/elastic-agent-android/actions) of the GitHub Actions page. Choose the latest successful build and download the APK by scrolling down and clicking on the `elastic-agent-android.apk`. 39 | 40 | ### 2. Create a New Policy in Fleet 41 | In your Fleet server, create a new policy using the "Custom Logs" integration (a "real" Android integration will be available in the future) 42 | This policy will define which components of the Elastic Agent Android will be activated. 43 | 44 | ### 3. Configure the Policy 45 | - Under "Paths", specify one path for each component you wish to activate. Examples include: 46 | - `android://self-log.warn` for warning level self logs. 47 | - `android://location.fine?minTimeMs=300000&minDistanceMeters=50` for fine location updates every 5 minutes or 50 meters. 48 | - `android://security-logs.all` for all security logs (device owner required). 49 | - `android://network-logs.all` for all network logs (device owner required). 50 | - In "Advanced options" -> "Custom Configurations", add: 51 | ```yaml 52 | max_documents_per_request: 200 53 | put_interval: 1m 54 | checkin_interval: 1m 55 | use_backoff: true 56 | max_backoff_interval: 5m 57 | backoff_on_empty_buffer: false 58 | disable_on_low_battery: false 59 | ``` 60 | These settings control how documents are batched and sent to Elasticsearch, with options for backoff strategies. 61 | 62 | ### 4. Install the App 63 | Install the downloaded APK on your target Android device. 64 | 65 | If you want to use the security- or network logs you need to configure the app as device owner as well. See [here](https://github.com/swiftbird07/elastic-agent-android/wiki/Register-App-as-Device-Owner) for instructions. 66 | 67 | ### 5. Enroll the Agent 68 | Open the app and tap on "Enroll Agent". Fill in the server URL, enrollment token, and hostname. You can also toggle SSL verification as needed. For mass enrollment, these fields can be autofilled using the clipboard or build configurations. 69 | 70 | ### 6. Verify the Enrollment 71 | After enrolling, the agent should report as "Healthy" within a few seconds, and you should start seeing events in Elasticsearch based on the `put_interval` setting. 72 | 73 | ## Contributions and Feedback 74 | 75 | We are open to feature requests, contributions, questions, and any feedback. If you're interested in contributing or have suggestions for improvement, please feel free to reach out or submit an issue/pull request on our GitHub repository. 76 | See [here](CONTRIBUTE.md) for more info about how to contribute, as well as for a general overview of the app's architecture. 77 | 78 | 79 | --- 80 | _**Disclaimer:** This project is not affiliated with Elastic N.V. or their Elastic Agent offerings. It is an independent implementation created by the community for educational and experimental purposes. 81 | The maintainers of this project are not responsible for any misuse or damage caused by the software. Use at your own risk. See license for more details. 82 | The Elastic Agent logo is a registered trademark of Elastic N.V. and is used here for illustrative purposes only and does not imply any affiliation with or endorsement by Elastic N.V._ 83 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | Any | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Send a mail to info[at]swiftbird.de 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | // Load the .env file if it exists 4 | val props = Properties() 5 | val envFile = rootProject.file(".env") 6 | if (envFile.exists()) { 7 | envFile.inputStream().use { props.load(it) } 8 | } 9 | 10 | 11 | plugins { 12 | id("com.android.application") 13 | kotlin("android") 14 | //kotlin("kapt") 15 | id("com.google.android.gms.oss-licenses-plugin") 16 | } 17 | 18 | android { 19 | namespace = "de.swiftbird.elasticandroid" 20 | compileSdk = 34 21 | 22 | defaultConfig { 23 | applicationId = "de.swiftbird.elasticandroid" 24 | minSdk = 24 25 | targetSdk = 34 26 | versionCode = 1 27 | versionName = "1.3.0" 28 | android.buildFeatures.buildConfig = true 29 | 30 | val enrollmentString = props.getProperty("ENROLLMENT_STRING") ?: "" 31 | buildConfigField("String", "ENROLLMENT_STRING", "\"$enrollmentString\"") 32 | buildConfigField("String", "AGENT_VERSION", "\"8.10.2\"") 33 | 34 | javaCompileOptions { 35 | annotationProcessorOptions { 36 | argument("room.schemaLocation", "$projectDir/schemas".toString()) 37 | } 38 | } 39 | 40 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 41 | 42 | } 43 | 44 | testOptions { 45 | unitTests.apply { 46 | isIncludeAndroidResources = true 47 | } 48 | } 49 | 50 | buildTypes { 51 | release { 52 | isMinifyEnabled = false 53 | isDebuggable = false // make debugging is disabled for release builds 54 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 55 | } 56 | debug { 57 | isDebuggable = true 58 | } 59 | } 60 | compileOptions { 61 | sourceCompatibility = JavaVersion.VERSION_15 62 | targetCompatibility = JavaVersion.VERSION_15 63 | } 64 | buildFeatures { 65 | viewBinding = true 66 | } 67 | } 68 | 69 | dependencies { 70 | 71 | implementation(libs.appcompat) 72 | implementation(libs.material) 73 | implementation(libs.constraintlayout) 74 | implementation(libs.navigation.fragment) 75 | implementation(libs.navigation.ui) 76 | testImplementation(libs.junit) 77 | androidTestImplementation(libs.ext.junit) 78 | androidTestImplementation(libs.espresso.core) 79 | 80 | // Retrofit for network requests 81 | implementation(libs.retrofit) 82 | // Gson converter for parsing JSON responses 83 | implementation(libs.converter.gson) 84 | 85 | // LiveData for reactive UI updates 86 | implementation(libs.lifecycle.livedata.ktx) 87 | 88 | // ViewModel for managing UI-related data in a lifecycle-conscious way 89 | implementation(libs.lifecycle.viewmodel.ktx) 90 | 91 | implementation(libs.room.runtime) 92 | 93 | //noinspection UseTomlInstead 94 | implementation("com.google.android.material:material") 95 | 96 | implementation(libs.work.runtime) 97 | // Unit testing dependencies 98 | testImplementation(libs.junit) 99 | testImplementation(libs.mockito.core) 100 | 101 | // For Android-specific mocking 102 | testImplementation(libs.robolectric) 103 | androidTestImplementation(libs.work.testing) 104 | 105 | implementation(libs.play.services.oss.licenses) 106 | 107 | implementation(libs.room.runtime) 108 | annotationProcessor(libs.room.compiler) 109 | 110 | implementation(libs.security.crypto) 111 | 112 | testImplementation(libs.mockito.core) 113 | testImplementation(libs.robolectric) 114 | //noinspection UseTomlInstead 115 | testImplementation("androidx.test:core:1.5.0") 116 | //noinspection UseTomlInstead 117 | testImplementation("org.mockito:mockito-inline:4.0.0") 118 | 119 | } 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/build/outputs/apk/debug/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "de.swiftbird.elasticandroid", 8 | "variantName": "debug", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 1, 15 | "versionName": "1.3.0", 16 | "outputFile": "app-debug.apk" 17 | } 18 | ], 19 | "elementType": "File", 20 | "minSdkVersionForDexing": 24 21 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/de/swiftbird/elasticandroid/AppInteractionTest.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.test.ext.junit.rules.ActivityScenarioRule; 4 | import androidx.test.ext.junit.runners.AndroidJUnit4; 5 | import androidx.test.filters.LargeTest; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import static androidx.test.espresso.Espresso.onView; 10 | import static androidx.test.espresso.Espresso.pressBack; 11 | import static androidx.test.espresso.assertion.ViewAssertions.matches; 12 | import static androidx.test.espresso.matcher.RootMatchers.isDialog; 13 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 14 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 15 | import static androidx.test.espresso.matcher.ViewMatchers.withText; 16 | import static androidx.test.espresso.action.ViewActions.click; 17 | 18 | /** 19 | * Extends the basic sanity UI test to interact with buttons that start new activities. 20 | * This test demonstrates clicking buttons in the main activity and verifying 21 | * that new activities containing specific text are launched accordingly and no crashes occur. 22 | */ 23 | @RunWith(AndroidJUnit4.class) 24 | @LargeTest 25 | public class AppInteractionTest { 26 | 27 | @Rule 28 | public ActivityScenarioRule activityRule = 29 | new ActivityScenarioRule<>(MainActivity.class); 30 | 31 | /** 32 | * Test to verify interactions with buttons in the main activity. 33 | * Each button press is expected to launch a new activity. The presence of specific 34 | * text within those activities confirms the expected navigation occurred. 35 | */ 36 | @Test 37 | public void testButtonInteractionsAndActivityLaunch() { 38 | // Click on the Accept button in the dialog and verify it is displayed 39 | onView(withText("ACCEPT")).inRoot(isDialog()).check(matches(isDisplayed())).perform(click()); 40 | 41 | // Click on the Help button and verify the Help activity is shown 42 | onView(withId(R.id.btnHelp)).perform(click()); 43 | onView(withText("Version and Legal")).check(matches(isDisplayed())); 44 | pressBack(); // Navigate back to the MainActivity 45 | 46 | // Click on the License button and verify the License activity is shown 47 | onView(withId(R.id.btnLicenses)).perform(click()); 48 | 49 | // License is long so just look for tvLicenseText 50 | onView(withId(R.id.tvLicenseText)).check(matches(isDisplayed())); 51 | 52 | // Click on the Open Source Licenses button and verify the OssLicensesMenuActivity is shown 53 | onView(withId(R.id.btnOSSLicenses)).perform(click()); 54 | // onView(withId(R.id.act)).check(matches(withText("Open Source Licenses")); TODO: Fix this 55 | 56 | pressBack(); // Navigate back to the LicenseActivity 57 | pressBack(); // Navigate back to the MainActivity 58 | 59 | // Click on the Legal button and verify the Legal activity is shown 60 | onView(withId(R.id.btnLegal)).perform(click()); 61 | 62 | // Legal is long so just look for legalDisclaimerText 63 | onView(withId(R.id.tvLegalText)).check(matches(isDisplayed())); 64 | 65 | // No need to pressBack() if this is the last activity interaction in the test 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 43 | 44 | 47 | 48 | 51 | 52 | 55 | 56 | 59 | 60 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbird07/elastic-agent-android/799a8b79f42009a860d5b5c83e0f8a4897ac6a39/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AckRequest.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Represents an acknowledgment request to be sent to a fleet management system. 7 | * This class models the structure of the data required by Retrofit to perform the acknowledgment action. 8 | * It encapsulates a list of events, with each event capturing details about the acknowledgment action. 9 | */ 10 | public class AckRequest { 11 | /** 12 | * Array of events to be acknowledged. Each event contains details such as type, subtype, agent ID, action ID, and a message. 13 | */ 14 | @SerializedName("events") 15 | private final Event[] events; 16 | 17 | /** 18 | * Represents a single event in the acknowledgment request. 19 | * This nested class details the structure of an event, including its type, subtype, agent and action IDs, and a descriptive message. 20 | */ 21 | public static class Event { 22 | @SerializedName("type") 23 | private String type; 24 | 25 | @SerializedName("subtype") 26 | private String subtype; 27 | 28 | @SerializedName("agent_id") 29 | private String agentId; 30 | 31 | @SerializedName("action_id") 32 | private String actionId; 33 | 34 | @SerializedName("message") 35 | private String message; 36 | } 37 | 38 | /** 39 | * Constructs an acknowledgment request with specified details for a single event. 40 | * 41 | * @param type The type of the event, e.g., "ACTION_RESULT". 42 | * @param subtype The subtype of the event, e.g., "ACKNOWLEDGED". 43 | * @param agent_id The unique identifier of the agent. 44 | * @param action_id The unique identifier of the action being acknowledged. 45 | * @param message A descriptive message about the acknowledgment, e.g., "Policy update success." 46 | */ 47 | public AckRequest(String type, String subtype, String agent_id, String action_id, String message) { 48 | this.events = new Event[]{new Event()}; 49 | events[0].type = type; 50 | events[0].subtype = subtype; 51 | events[0].agentId = agent_id; 52 | events[0].actionId = action_id; 53 | events[0].message = message; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AckResponse.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * AckResponse models the JSON structure of a response from a fleet management system following an acknowledgment request. 7 | * This class is prepared for Retrofit to easily deserialize the received JSON into a structured Java object. It specifically 8 | * captures details about the action being acknowledged and the outcomes of such acknowledgments through an array of items, 9 | * each item containing a status code and a message indicative of the result of the acknowledgment process. 10 | */ 11 | public class AckResponse { 12 | @SerializedName("action") 13 | private String action; // Indicates the action type of the acknowledgment, e.g., "acks". 14 | 15 | @SerializedName("items") 16 | private Item[] items; // Array of individual acknowledgment outcomes. 17 | 18 | public String getAction() { 19 | return action; 20 | } 21 | 22 | public Item[] getItems() { 23 | return items; 24 | } 25 | 26 | public static class Item { 27 | @SerializedName("message") 28 | private String message; // Descriptive message of the acknowledgment outcome. 29 | 30 | @SerializedName("status") 31 | private int status; // HTTP status code representing the result of the acknowledgment. 32 | 33 | public String getMessage() { 34 | return message; 35 | } 36 | 37 | public int getStatus() { 38 | return status; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | /** 8 | * Listens for system-wide broadcast events that the app is interested in. 9 | * Specifically, this receiver is set up to listen for the device's boot completion event. 10 | * Upon receiving this event, it initiates the location receiver component to start its operations. 11 | */ 12 | public class AppBroadcastReceiver extends BroadcastReceiver { 13 | 14 | /** 15 | * Called when the receiver receives a broadcast that it is registered for. 16 | * This implementation checks if the broadcast received is for the boot completion of the device. 17 | * If so, it logs the event and initializes the location receiver component. 18 | * 19 | * @param context The Context in which the receiver is running. 20 | * @param intent The Intent being received, containing the broadcast action. 21 | */ 22 | @Override 23 | public void onReceive(Context context, Intent intent) { 24 | if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { 25 | AppLog.i("AppBroadcastReceiver", "Device booted up"); 26 | // Initialize the location receiver to start its operation post-boot. 27 | LocationReceiver locationReceiver = new LocationReceiver(context); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppConverters.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.TypeConverter; 4 | import com.google.gson.Gson; 5 | import com.google.gson.reflect.TypeToken; 6 | import java.net.InetAddress; 7 | import java.lang.reflect.Type; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | /** 12 | * AppConverters provides a collection of methods to assist Room in converting complex data types to and from 13 | * a format that can be stored in the database easily. This includes conversions for lists of InetAddress objects, 14 | * strings, and custom objects like GeoLocation, facilitating their storage in a single column. 15 | */ 16 | public class AppConverters { 17 | 18 | /** 19 | * Converts a JSON string representation of a list of InetAddress objects back into a List. 20 | * @param value JSON string representing the list of InetAddress objects. 21 | * @return A List of InetAddress objects or an empty list if the input is null. 22 | */ 23 | @TypeConverter 24 | public static List fromString(String value) { 25 | if (value == null) { 26 | return Collections.emptyList(); 27 | } 28 | Type listType = new TypeToken>() {}.getType(); 29 | return new Gson().fromJson(value, listType); 30 | } 31 | 32 | /** 33 | * Converts a List of InetAddress objects into a JSON string representation. 34 | * @param list The List of InetAddress objects to convert. 35 | * @return A JSON string representation of the list. 36 | */ 37 | @TypeConverter 38 | public static String fromList(List list) { 39 | Gson gson = new Gson(); 40 | return gson.toJson(list); 41 | } 42 | 43 | 44 | 45 | /** 46 | * Converts a JSON string representation of a list of strings back into a List. 47 | * @param value JSON string representing the list of strings. 48 | * @return A List of strings or an empty list if the input is null. 49 | */ 50 | @TypeConverter 51 | public static List fromStringList(String value) { 52 | if (value == null) { 53 | return Collections.emptyList(); 54 | } 55 | Type listType = new TypeToken>() {}.getType(); 56 | return new Gson().fromJson(value, listType); 57 | } 58 | 59 | /** 60 | * Converts a List of strings into a JSON string representation. 61 | * @param list The List of strings to convert. 62 | * @return A JSON string representation of the list. 63 | */ 64 | @TypeConverter 65 | public static String fromStringList(List list) { 66 | Gson gson = new Gson(); 67 | return gson.toJson(list); 68 | } 69 | 70 | /** 71 | * Converts a GeoLocation object into a simplified string representation "lat,lon". 72 | * @param geoLocation The GeoLocation object to convert. 73 | * @return A string representing the latitude and longitude, separated by a comma. 74 | */ 75 | @TypeConverter 76 | public static String fromGeoLocation(LocationCompDocument.GeoLocation geoLocation) { 77 | if (geoLocation == null) return null; 78 | return geoLocation.lat + "," + geoLocation.lon; 79 | } 80 | 81 | /** 82 | * Converts a simplified string representation "lat,lon" of a GeoLocation back into a GeoLocation object. 83 | * @param data The string representation of the GeoLocation. 84 | * @return A GeoLocation object constructed from the provided string. 85 | */ 86 | @TypeConverter 87 | public static LocationCompDocument.GeoLocation toGeoLocation(String data) { 88 | if (data == null) return null; 89 | String[] pieces = data.split(","); 90 | return new LocationCompDocument.GeoLocation(Double.parseDouble(pieces[0]), Double.parseDouble(pieces[1])); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppDatabase.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.room.Database; 6 | import androidx.room.Room; 7 | import androidx.room.RoomDatabase; 8 | import androidx.room.TypeConverters; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | 12 | /** 13 | * Central abstract database class for the application, using the Room persistence library to manage SQLite database operations. 14 | * This class defines all entities that form the database and their respective DAOs for data access. It implements a singleton 15 | * pattern to ensure that only one instance of the database is created throughout the application's lifecycle. 16 | * 17 | * @Database annotation specifies the entities contained within the database and its version. The exportSchema attribute 18 | * enables schema export, which is useful for version tracking and migration. autoMigrations defines automatic migration paths 19 | * between database versions, though this is commented out in the provided code snippet. 20 | * 21 | * @TypeConverters annotation lists classes that provide custom conversions between database types and Java data types, enabling 22 | * storage of complex types in the database. 23 | */ 24 | @Database( 25 | entities = { 26 | FleetEnrollData.class, 27 | PolicyData.class, 28 | SelfLogCompDocument.class, 29 | AppStatisticsData.class, 30 | SecurityLogsCompDocument.class, 31 | NetworkLogsCompDocument.class, 32 | LocationCompDocument.class, 33 | }, 34 | version = 31, 35 | exportSchema = true, 36 | autoMigrations = { 37 | //@AutoMigration(from = 30, to = 31), 38 | } 39 | ) 40 | @TypeConverters({AppConverters.class}) 41 | public abstract class AppDatabase extends RoomDatabase { 42 | 43 | public abstract FleetEnrollDataDAO enrollmentDataDAO(); 44 | public abstract PolicyDataDAO policyDataDAO(); 45 | public abstract AppStatisticsDataDAO statisticsDataDAO(); 46 | public abstract SelfLogCompBuffer selfLogCompBuffer(); 47 | public abstract SecurityLogsCompBuffer securityLogCompBuffer(); 48 | public abstract NetworkLogsCompBuffer networkLogsCompBuffer(); 49 | public abstract LocationCompBuffer locationCompBuffer(); 50 | 51 | private static final boolean FALLBACK_TO_DESTRUCTIVE_MIGRATION = BuildConfig.DEBUG; // Flag to enable destructive migration in debug mode 52 | private static final int NUMBER_OF_THREADS = 4; // Thread pool size for database write operations 53 | static final ExecutorService databaseWriteExecutor = 54 | Executors.newFixedThreadPool(NUMBER_OF_THREADS); // Executor service for asynchronous database operations 55 | private static volatile AppDatabase appDatabase; // Singleton instance of the database 56 | 57 | 58 | 59 | /** 60 | * Gets the singleton instance of the AppDatabase. 61 | * This method uses a double-checked locking pattern to initialize the AppDatabase instance in a thread-safe manner. 62 | * Debug builds opt for a destructive migration strategy for simplicity, while release builds use the default migration strategy. 63 | * 64 | * @param context The context used to build the database instance. 65 | * @return The singleton instance of AppDatabase. 66 | */ 67 | protected static AppDatabase getDatabase(final Context context) { 68 | if (appDatabase == null) { 69 | synchronized (AppDatabase.class) { 70 | if (appDatabase == null) { 71 | Builder builder = Room.databaseBuilder(context.getApplicationContext(), 72 | AppDatabase.class, "agent-data"); 73 | 74 | if(FALLBACK_TO_DESTRUCTIVE_MIGRATION){ 75 | builder.fallbackToDestructiveMigration(); 76 | } 77 | 78 | appDatabase = builder.build(); 79 | } 80 | } 81 | } 82 | return appDatabase; 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppDeviceAdminReceiver.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.app.admin.DeviceAdminReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import androidx.annotation.NonNull; 7 | 8 | /** 9 | * Extends DeviceAdminReceiver to handle specific admin events related to the application's device management features. 10 | * This receiver is crucial for responding to security and network log availability, among other device admin events. 11 | */ 12 | public class AppDeviceAdminReceiver extends DeviceAdminReceiver { 13 | 14 | /** 15 | * Called when the application is granted device admin access. 16 | * This method could be used to perform some setup actions upon admin activation. 17 | * Tho currently this does nothing. 18 | * 19 | * @param context The Context in which the receiver is running. 20 | * @param intent The Intent being received. 21 | */ 22 | @Override 23 | public void onEnabled(@NonNull Context context, @NonNull Intent intent) { 24 | super.onEnabled(context, intent); 25 | AppLog.i("AppDeviceAdminReceiver", "Device admin enabled"); 26 | } 27 | 28 | /** 29 | * Invoked when security logs are available for the device. This method initiates a new thread 30 | * to handle the security logs asynchronously to avoid blocking the main thread. 31 | * 32 | * @param context The Context in which the receiver is running. 33 | * @param intent The Intent being received. 34 | */ 35 | @Override 36 | public void onSecurityLogsAvailable(@NonNull Context context, @NonNull Intent intent) { 37 | super.onSecurityLogsAvailable(context, intent); 38 | AppLog.i("AppDeviceAdminReceiver", "Security logs available"); 39 | SecurityLogsComp logsComp = SecurityLogsComp.getInstance(); 40 | // New thread to handle the security logs 41 | new Thread(() -> { 42 | try { 43 | logsComp.handleSecurityLogs(context); 44 | } catch (Exception e) { 45 | AppLog.e("AppDeviceAdminReceiver", "Error handling security logs: " + e.getMessage()); 46 | } 47 | }).start(); 48 | } 49 | 50 | /** 51 | * Called when network logs are available. This method starts a new thread to process the network logs 52 | * asynchronously, based on the received batchToken and the count of network logs. 53 | * 54 | * @param context The Context in which the receiver is running. 55 | * @param intent The Intent being received. 56 | * @param batchToken The token identifying the batch of network logs available. 57 | * @param networkLogsCount The number of network logs available in this batch. 58 | */ 59 | @Override 60 | public void onNetworkLogsAvailable(@NonNull Context context, @NonNull Intent intent, long batchToken, int networkLogsCount) { 61 | super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount); 62 | AppLog.i("AppDeviceAdminReceiver", "Network logs available from batchToken: " + batchToken + " with count: " + networkLogsCount); 63 | NetworkLogsComp logsComp = NetworkLogsComp.getInstance(); 64 | // New thread to handle the network logs 65 | new Thread(() -> { 66 | try { 67 | logsComp.handleNetworkLogs(context, batchToken); 68 | } catch (Exception e) { 69 | AppLog.e("AppDeviceAdminReceiver", "Error handling network logs: " + e.getMessage()); 70 | } 71 | }).start(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppEnrollRequest.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | /** 4 | * Represents a request to enroll the app to the fleet server, containing all necessary information for the enrollment process. 5 | * This class encapsulates details such as the server URL, enrollment token, hostname, certificate, and security checks to be performed. 6 | * Don't confuse this class with the FleetEnrollRequest class, which is the final parsed request send used by retrofit (this is an intermediate representation). 7 | */ 8 | public class AppEnrollRequest { 9 | private final String serverUrl; 10 | private final String token; 11 | private final String hostname; 12 | private final String certificate; 13 | private final boolean checkCert; 14 | private final boolean fingerprintRootCA; 15 | 16 | /** 17 | * Constructs an AppEnrollRequest with all necessary information for enrolling the app. 18 | * Warning: Expects parameters to be sanitized and validated *before* calling this constructor. 19 | * 20 | * @param serverUrl URL of the fleet server. 21 | * @param token Enrollment token. 22 | * @param hostname Hostname of the device. 23 | * @param certificate Certificate of the fleet server in PEM format (UTF-8, Raw). Effective only if checkCert is true. 24 | * If empty, the server's certificate will be checked against the system's trust store. 25 | * Currently not implemented. 26 | * @param checkCert Whether to check the server's certificate (either against system trust store or the provided certificate if not empty). 27 | * @param fingerprintRootCA Whether to save the fingerprint of the fleet server's root CA certificate after successful enrollment and validate future connections against it. 28 | * Currently not implemented. 29 | */ 30 | public AppEnrollRequest(String serverUrl, String token, String hostname, String certificate, boolean checkCert, boolean fingerprintRootCA){ 31 | this.serverUrl = serverUrl; 32 | this.token = token; 33 | this.hostname = hostname; 34 | this.certificate = certificate; 35 | this.checkCert = checkCert; 36 | this.fingerprintRootCA = fingerprintRootCA; 37 | } 38 | 39 | public String getServerUrl() { 40 | return serverUrl; 41 | } 42 | 43 | 44 | public String getHostname() { 45 | return hostname; 46 | } 47 | 48 | public String getCertificate() { 49 | return certificate; 50 | } 51 | 52 | public boolean isCheckCert() { 53 | return checkCert; 54 | } 55 | 56 | public boolean isCheckFingerprintCert() { 57 | return fingerprintRootCA; 58 | } 59 | 60 | public String getToken() { 61 | return token; 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppInstance.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | /** 7 | * Extends the Application class to provide a singleton instance and application context accessible throughout the app. 8 | * This class ensures that a global application context is available, which can be useful for accessing resources, 9 | * starting activities, and more, from places not inherently tied to an activity's context. 10 | */ 11 | public class AppInstance extends Application { 12 | private static AppInstance instance; 13 | 14 | /** 15 | * Initializes the singleton application instance. This method is called when the application is starting, 16 | * before any other application objects have been created. 17 | */ 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | instance = this; 22 | } 23 | 24 | /** 25 | * Provides access to the singleton instance of the application. 26 | * 27 | * @return The singleton instance of AppInstance. 28 | */ 29 | public static AppInstance getInstance() { 30 | return instance; 31 | } 32 | 33 | /** 34 | * Retrieves a context for the application. 35 | * This context is tied to the lifecycle of the application and can be used where an activity context is not available. 36 | * 37 | * @return The application context for global use. 38 | */ 39 | public static Context getAppContext() { 40 | return instance.getApplicationContext(); 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppLog.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.os.Build; 4 | import android.util.Log; 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | 8 | /** 9 | * Provides a unified logging interface that extends Android's Log class functionalities. 10 | * In addition to outputting logs to the console, it inserts log records into the application's 11 | * database for persistence and further processing, depending on configured policies. 12 | */ 13 | public class AppLog { 14 | private static final ExecutorService executor = Executors.newSingleThreadExecutor(); 15 | 16 | /** 17 | * Logs an informational message both to the console and the application's log storage. 18 | * 19 | * @param tag Tag used to identify the source of the log message. 20 | * @param msg The message to log. 21 | * @return The result from Android's native Log.i method. 22 | */ 23 | public static int i(String tag, String msg) { 24 | insertLog("INFO", tag, msg); 25 | return Log.i(tag, msg); 26 | } 27 | 28 | /** 29 | * Logs a warning message both to the console and the application's log storage. 30 | * 31 | * @param tag Tag for the log message. 32 | * @param msg Warning message to log. 33 | * @return The result from the native Log.w method. 34 | */ 35 | public static int w(String tag, String msg) { 36 | insertLog("WARN", tag, msg); 37 | return Log.w(tag, msg); 38 | } 39 | 40 | /** 41 | * Logs an error message both to the console and the application's log storage. 42 | * 43 | * @param tag Tag for the log message. 44 | * @param msg Error message to log. 45 | * @return The result from the native Log.e method. 46 | */ 47 | public static int e(String tag, String msg) { 48 | insertLog("ERROR", tag, msg); 49 | return Log.e(tag, msg); 50 | } 51 | 52 | /** 53 | * Logs an error message along with an exception both to the console and the application's log storage. 54 | * 55 | * @param tag Tag for the log message. 56 | * @param msg Error message to log. 57 | * @param tr Throwable associated with the error condition. 58 | * @return The result from the native Log.e method. 59 | */ 60 | public static int e(String tag, String msg, Throwable tr) { 61 | insertLog("ERROR", tag, msg + '\n' + Log.getStackTraceString(tr)); 62 | return Log.e(tag, msg, tr); 63 | } 64 | 65 | /** 66 | * Logs a debug message both to the console and the application's log storage. 67 | * 68 | * @param tag Tag for the log message. 69 | * @param msg Debug message to log. 70 | * @return The result from the native Log.d method. 71 | */ 72 | public static int d(String tag, String msg) { 73 | insertLog("DEBUG", tag, msg); 74 | if(BuildConfig.DEBUG){ // Only log debug messages to logcat in debug builds 75 | return Log.d(tag, msg); 76 | } 77 | return 0; 78 | } 79 | 80 | /** 81 | * Inserts a log record into the application's log storage for persistence and later processing. 82 | * This operation is performed asynchronously to prevent blocking the main thread. 83 | * 84 | * @param level The severity level of the log message (e.g., INFO, WARN, ERROR). 85 | * @param tag Tag associated with the log message. 86 | * @param message The log message. 87 | */ 88 | private static void insertLog(String level, String tag, String message) { 89 | // Execute on a background thread to avoid blocking the main thread 90 | executor.submit(() -> { 91 | try { 92 | AppDatabase db = AppDatabase.getDatabase(AppInstance.getAppContext()); 93 | FleetEnrollData enrollmentData = db.enrollmentDataDAO().getEnrollmentInfoSync(1); 94 | PolicyData policyData = db.policyDataDAO().getPolicyDataSync(); 95 | boolean selfLogEnabled = policyData != null && policyData.paths != null && policyData.paths.contains("android://self-log"); 96 | if(!selfLogEnabled) { 97 | return; 98 | } 99 | 100 | // Get the path from the policy data for self-log in the "," separated format 101 | String[] paths = policyData.paths.split(","); 102 | for (String path : paths) { 103 | if (path.startsWith("android://self-log")) { 104 | // The path is in the format "android://self-log.INFO" 105 | String[] pathParts = path.split("\\."); 106 | if (pathParts.length > 1) { 107 | // Check if the log level is high enough to be sent 108 | if (level.equals("DEBUG") && pathParts[1].equals("info")) { 109 | return; 110 | } 111 | if ((level.equals("DEBUG") || level.equals("INFO")) && pathParts[1].equals("warn")) { 112 | return; 113 | } 114 | if ((level.equals("DEBUG") || level.equals("INFO") || level.equals("WARN")) && pathParts[1].equals("error")) { 115 | return; 116 | } 117 | } else { 118 | // If no log level is specified, send only INFO logs 119 | if (!level.equals("INFO")) { 120 | return; 121 | } 122 | } 123 | } 124 | } 125 | 126 | SelfLogCompDocument document = new SelfLogCompDocument(enrollmentData, policyData, level, tag, message); 127 | SelfLogComp selfLogComp = SelfLogComp.getInstance(); 128 | selfLogComp.setup(AppInstance.getAppContext() , null, null, ""); 129 | selfLogComp.addDocumentToBuffer(document); 130 | } catch (Exception e) { 131 | // Ignore any exceptions, as it may be that the agent is not enrolled yet and therefor can't send logs anyway 132 | } 133 | }); 134 | 135 | } 136 | 137 | /** 138 | * Shuts down the logger by stopping the executor service. 139 | */ 140 | public static void shutdownLogger() { 141 | executor.shutdown(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppSecurePreferences.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import androidx.security.crypto.EncryptedSharedPreferences; 8 | import androidx.security.crypto.MasterKey; 9 | import java.io.IOException; 10 | import java.security.GeneralSecurityException; 11 | 12 | /** 13 | * Provides secure storage for sensitive application preferences like the Fleet and Elastic API keys. 14 | * The preferences are stored in an encrypted form using the AndroidX Security library. 15 | */ 16 | public class AppSecurePreferences { 17 | private static final String FILE_NAME = "secure_prefs"; 18 | private static final String ELASTIC_API_KEY_FIELD_NAME = "elastic_api_key"; 19 | private static final String FLEET_API_KEY_FIELD_NAME = "fleet_api_key"; 20 | private final SharedPreferences encryptedPreferences; 21 | private static AppSecurePreferences instance; 22 | 23 | /** 24 | * Gets the singleton instance of the AppSecurePreferences class. 25 | * 26 | * @param context The application context. 27 | * @return The singleton instance of the AppSecurePreferences class. 28 | */ 29 | public static AppSecurePreferences getInstance(Context context) { 30 | if (instance == null) { 31 | try { 32 | instance = new AppSecurePreferences(context); 33 | } catch (GeneralSecurityException | IOException e) { 34 | AppLog.e("AppSecurePreferences", "Failed to create secure preferences", e); 35 | throw new RuntimeException("Failed to create secure preferences. Reason: " + e.getMessage() + " Cause: " + e.getCause()); 36 | } 37 | } 38 | return instance; 39 | } 40 | 41 | /** 42 | * Initializes a new instance of the AppSecurePreferences class. 43 | * 44 | * @param context The application context. 45 | * @throws GeneralSecurityException If an error occurs during encryption. 46 | * @throws IOException If an error occurs during I/O operations. 47 | */ 48 | AppSecurePreferences(Context context) throws GeneralSecurityException, IOException { 49 | AppLog.i("AppSecurePreferences", "Creating secure preferences"); 50 | MasterKey masterKey = new MasterKey.Builder(context) 51 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) 52 | .build(); 53 | 54 | encryptedPreferences = EncryptedSharedPreferences.create( 55 | context, 56 | FILE_NAME, 57 | masterKey, 58 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, 59 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM); 60 | } 61 | 62 | /** 63 | * Saves the Elastic API key to the secure preferences. 64 | * 65 | * @param apiKey The enrollment token to save. 66 | */ 67 | public void saveElasticApiKey(String apiKey) { 68 | AppLog.i("AppSecurePreferences", "Saving Elastic API key to secure preferences"); 69 | encryptedPreferences.edit().putString(ELASTIC_API_KEY_FIELD_NAME, apiKey).apply(); 70 | } 71 | 72 | /** 73 | * Retrieves the Elastic API key from the secure preferences. 74 | * 75 | * @return The enrollment token if found; null otherwise. 76 | */ 77 | public String getElasticApiKey() { 78 | AppLog.i("AppSecurePreferences", "Retrieving Elastic API key from secure preferences"); 79 | return encryptedPreferences.getString(ELASTIC_API_KEY_FIELD_NAME, null); 80 | } 81 | 82 | /** 83 | * Saves the Fleet API key to the secure preferences. 84 | * 85 | * @param apiKey The API key to save. 86 | */ 87 | public void saveFleetApiKey(String apiKey) { 88 | AppLog.i("AppSecurePreferences", "Saving Fleet API key to secure preferences"); 89 | encryptedPreferences.edit().putString(FLEET_API_KEY_FIELD_NAME, apiKey).apply(); 90 | } 91 | 92 | /** 93 | * Retrieves the Fleet API key from the secure preferences. 94 | * 95 | * @return The API key if found; null otherwise. 96 | */ 97 | public String getFleetApiKey() { 98 | AppLog.i("AppSecurePreferences", "Retrieving Fleet API key from secure preferences"); 99 | return encryptedPreferences.getString(FLEET_API_KEY_FIELD_NAME, null); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppStatisticsData.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.Entity; 5 | import androidx.room.OnConflictStrategy; 6 | import androidx.room.PrimaryKey; 7 | 8 | /** 9 | * Entity representing various statistical data points collected about the application's performance and activities. 10 | * This includes metrics such as the total number of check-ins, failures, details about the last documents sent, 11 | * the combined buffer size for pending operations, and the overall health status of the agent. 12 | */ 13 | @Entity 14 | public class AppStatisticsData { 15 | @PrimaryKey 16 | @ColumnInfo(name = "id") 17 | public int id; // Unique identifier for the statistics data record. Currently set to 1 as a singleton pattern. 18 | 19 | @ColumnInfo(name = "total_checkins") 20 | public int totalCheckins; // The total number of check-ins performed by the application. 21 | 22 | @ColumnInfo(name = "total_failures") 23 | public int totalFailures; // The total number of failures encountered by the application. 24 | 25 | @ColumnInfo(name = "last_documents_sent_at") 26 | public String lastDocumentsSentAt; // Timestamp of the last successful document transmission. 27 | 28 | @ColumnInfo(name = "last_documents_sent_count") 29 | public int lastDocumentsSentCount; // Number of documents sent in the last transmission. 30 | 31 | @ColumnInfo(name = "combined_buffer_size") 32 | public int combinedBufferSize; // The combined size of all buffers awaiting transmission. 33 | 34 | @ColumnInfo(name = "agent_health") 35 | public String agentHealth; // Descriptive status of the agent's health. 36 | 37 | /** 38 | * Constructor initializing the statistics data with a default id. 39 | * This ensures that the entity acts as a singleton, only allowing a single set of statistics data. 40 | */ 41 | public AppStatisticsData() { 42 | id = 1; // Ensures singleton behavior by using a constant ID. 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/AppStatisticsDataDAO.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.lifecycle.LiveData; 4 | import androidx.room.Dao; 5 | import androidx.room.Insert; 6 | import androidx.room.OnConflictStrategy; 7 | import androidx.room.Query; 8 | 9 | /** 10 | * Data Access Object (DAO) for managing operations related to the AppStatisticsData entity. 11 | * Defines methods for inserting, updating, and querying application statistics data from the database. 12 | */ 13 | @Dao 14 | public interface AppStatisticsDataDAO { 15 | /** 16 | * Updates or replaces the existing statistics data in the database. 17 | * 18 | * @param statisticsData The statistics data to be updated or inserted. 19 | */ 20 | @Insert(onConflict = OnConflictStrategy.REPLACE) 21 | void updateStatistics(AppStatisticsData statisticsData); 22 | 23 | /** 24 | * Increments the total number of check-ins by one. 25 | */ 26 | @Query("UPDATE AppStatisticsData SET total_checkins = total_checkins + 1") 27 | void increaseTotalCheckins(); 28 | 29 | /** 30 | * Increments the total number of failures by one. 31 | */ 32 | @Query("UPDATE AppStatisticsData SET total_failures = total_failures + 1") 33 | void increaseTotalFailures(); 34 | 35 | /** 36 | * Sets the timestamp for the last set of documents sent. 37 | * 38 | * @param lastDocumentsSentAt The timestamp when the last documents were sent. 39 | */ 40 | @Query("UPDATE AppStatisticsData SET last_documents_sent_at = :lastDocumentsSentAt") 41 | void setLastDocumentsSentAt(String lastDocumentsSentAt); 42 | 43 | /** 44 | * Sets the count for the last set of documents sent. 45 | * 46 | * @param lastDocumentsSentCount The number of documents sent in the last operation. 47 | */ 48 | @Query("UPDATE AppStatisticsData SET last_documents_sent_count = :lastDocumentsSentCount") 49 | void setLastDocumentsSentCount(int lastDocumentsSentCount); 50 | 51 | /** 52 | * Increases the combined buffer size by a specified amount. 53 | * 54 | * @param amount The amount by which to increase the combined buffer size. 55 | */ 56 | @Query("UPDATE AppStatisticsData SET combined_buffer_size = combined_buffer_size + :amount") 57 | void increaseCombinedBufferSize(int amount); 58 | 59 | /** 60 | * Decreases the combined buffer size by a specified amount. 61 | * 62 | * @param amount The amount by which to decrease the combined buffer size. 63 | */ 64 | @Query("UPDATE AppStatisticsData SET combined_buffer_size = combined_buffer_size - :amount") 65 | void decreaseCombinedBufferSize(int amount); 66 | 67 | /** 68 | * Updates the health status of the agent. 69 | * 70 | * @param agentHealth The new health status of the agent. 71 | */ 72 | @Query("UPDATE AppStatisticsData SET agent_health = :agentHealth") 73 | void setAgentHealth(String agentHealth); 74 | 75 | /** 76 | * Inserts a new statistics data record into the database. 77 | * If the record already exists, it ignores the insertion. 78 | * 79 | * @param statisticsData The new statistics data to insert. 80 | */ 81 | @Insert(onConflict = OnConflictStrategy.IGNORE) 82 | void insert(AppStatisticsData statisticsData); 83 | 84 | /** 85 | * Retrieves the application statistics data as a LiveData object. 86 | * LiveData allows observing data changes in a lifecycle-aware manner. 87 | * 88 | * @return A LiveData object containing the application statistics data. 89 | */ 90 | @Query("SELECT * FROM AppStatisticsData WHERE id = 1") 91 | LiveData getStatistics(); 92 | 93 | /** 94 | * Synchronous version of getStatistics for immediate data retrieval. 95 | * 96 | * @return The application statistics data. 97 | */ 98 | @Query("SELECT * FROM AppStatisticsData WHERE id = 1") 99 | AppStatisticsData getStatisticsSync(); 100 | 101 | /** 102 | * Deletes all statistics data records from the database. 103 | */ 104 | @Query("DELETE FROM AppStatisticsData") 105 | void delete(); 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/Component.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import java.util.List; 5 | 6 | /** 7 | * Interface defining the common functionality of components within an Elastic Agent, 8 | * such as collecting, storing, and managing specific types of data (e.g., location, network). 9 | * Each component must be initialized with setup() before use, providing a consistent lifecycle across components. 10 | */ 11 | public interface Component { 12 | 13 | /** 14 | * Prepares the component for operation, initializing it with necessary data. 15 | * 16 | * @param context The application context. 17 | * @param enrollmentData Data regarding the agent's enrollment. 18 | * @param policyData Policy configurations affecting the component's behavior. 19 | * @param subComponent Optional identifier for sub-components. 20 | * @return True if setup was successful, false otherwise. 21 | */ 22 | boolean setup(Context context, FleetEnrollData enrollmentData, PolicyData policyData, String subComponent); 23 | 24 | /** 25 | * Collects relevant events or data, as per the component's functionality. 26 | * This method will be called periodically by the agent's main loop (TODO: not yet implemented). 27 | * Notice: Most components will not need to implement this method as they will be event-driven. 28 | * 29 | * @param enrollmentData Data regarding the agent's enrollment. 30 | * @param policyData Policy configurations affecting data collection. 31 | */ 32 | default void collectEvents(FleetEnrollData enrollmentData, PolicyData policyData) {} 33 | 34 | /** 35 | * Adds a document to the component's internal buffer for later processing or transmission. 36 | * 37 | * @param document The document to add to the buffer. 38 | */ 39 | void addDocumentToBuffer(ElasticDocument document); 40 | 41 | /** 42 | * Retrieves a list of documents from the buffer, up to a specified maximum number. 43 | * 44 | * @param maxDocuments The maximum number of documents to retrieve. 45 | * @return A list of documents from the buffer. 46 | */ 47 | List getDocumentsFromBuffer(int maxDocuments); 48 | 49 | /** 50 | * Gets the count of documents currently stored in the buffer. 51 | * 52 | * @return The number of documents in the buffer. 53 | */ 54 | int getDocumentsInBufferCount(); 55 | 56 | /** 57 | * Lists the permissions required by the component for its operation. 58 | * 59 | * @return A list of permissions required by the component. 60 | */ 61 | default List getRequiredPermissions() { 62 | return List.of(); 63 | } 64 | 65 | /** 66 | * Returns the path name (without sub-component or parameters) associated with the policy configuration for the component. 67 | * 68 | * @return The component's path name. 69 | */ 70 | String getPathName(); 71 | 72 | /** 73 | * Disables the component, cleaning up resources and stopping any ongoing operations. 74 | * 75 | * @param context The application context. 76 | * @param enrollmentData Data regarding the agent's enrollment. 77 | * @param policyData Policy configurations affecting the component's behavior. 78 | */ 79 | default void disable(Context context, FleetEnrollData enrollmentData, PolicyData policyData) {} 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/ComponentFactory.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * Manages the creation and retrieval of {@link Component} instances based on URIs defined in the fleet agent policy response. 11 | * Utilizes a registry pattern where components are registered with a unique key (URI scheme), allowing for dynamic instantiation 12 | * based on administrative configurations. This approach supports flexible component activation and configuration through policy settings. 13 | */ 14 | public class ComponentFactory { 15 | private static final Map> components = new ConcurrentHashMap<>(); 16 | 17 | static { 18 | // Initial component registration. 19 | components.put("android://self-log", SelfLogComp::getInstance); 20 | components.put("android://security-logs", SecurityLogsComp::getInstance); 21 | components.put("android://network-logs", NetworkLogsComp::getInstance); 22 | components.put("android://location", LocationComp::getInstance); 23 | // Additional components can be registered here following the URI scheme. 24 | } 25 | 26 | /** 27 | * Creates an instance of a {@link Component} based on the specified key. 28 | * The key corresponds to a URI that not only identifies the component type but can also include configuration directives. 29 | * 30 | * @param key The unique key (URI) identifying the component type only (URI until the first colon, e.g., "android://location"). 31 | * @return An instance of the requested Component. 32 | * @throws IllegalArgumentException If no component is registered under the specified key. 33 | */ 34 | public static Component createInstance(String key) { 35 | Supplier supplier = components.get(key); 36 | if (supplier != null) { 37 | return (Component) supplier.get(); 38 | } 39 | throw new IllegalArgumentException("Component with key " + key + " not found."); 40 | } 41 | 42 | /** 43 | * Retrieves instances of all registered components. 44 | * This method can be used to initialize or manage all available components dynamically, based on the current policy configuration. 45 | * 46 | * @return An array of all Component instances currently registered. 47 | */ 48 | public static Component[] getAllInstances() { 49 | List list = new ArrayList<>(); 50 | for (Supplier supplier : components.values()) { 51 | Component o = (Component) supplier.get(); 52 | list.add(o); 53 | } 54 | return list.toArray(new Component[0]); 55 | } 56 | 57 | /** 58 | * Registers a new component with the factory, associating it with a unique key. 59 | * The key should be a URI scheme that identifies the component type and can include configuration directives. 60 | * 61 | * @param key The unique key (URI) identifying the component type only (URI until the first colon, e.g., "android://location"). 62 | * @param supplier A supplier function that creates a new instance of the component. 63 | */ 64 | public static void registerComponent(String key, Supplier supplier) { 65 | components.put(key, supplier); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/ComponentWorker.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import androidx.annotation.NonNull; 5 | import androidx.work.Worker; 6 | import androidx.work.WorkerParameters; 7 | 8 | /** 9 | * A worker class that extends WorkManager's Worker, designed to execute background tasks for various components of the application. 10 | * It aims to perform operations that should run outside of the application's UI thread for collecting events. 11 | * This worker iterates over all components specified in the application's policy data, initializes them, 12 | * and triggers their event collection routines. 13 | * 14 | *

Note: This class is part of the application's background execution strategy but is not yet integrated into the application's workflow. 15 | * TODO: Integrate this worker into the application's workflow to enable background data collection for components. 16 | */ 17 | public class ComponentWorker extends Worker { 18 | /** 19 | * Constructor initializing the worker with application context and worker parameters. 20 | * 21 | * @param context The application context. 22 | * @param workerParams Parameters for configuring the worker, including input data and tags. 23 | */ 24 | public ComponentWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { 25 | super(context, workerParams); 26 | // Future implementation will use parameters to configure worker tasks. 27 | } 28 | 29 | /** 30 | * Executes the background task. This is where the worker iterates over available components, 31 | * performs setup and triggers data collection based on the current policy data. 32 | * 33 | * @return The result of the background work; {@link Result#success()} if the operation completes successfully. 34 | */ 35 | @NonNull 36 | @Override 37 | public Result doWork() { 38 | AppLog.i("ComponentWorker", "Performing component worker tasks in the background"); 39 | 40 | // Retrieves enrollment and policy data to configure and operate on components. 41 | FleetEnrollData enrollmentData = AppDatabase.getDatabase(getApplicationContext()).enrollmentDataDAO().getEnrollmentInfoSync(1); 42 | PolicyData policyData = AppDatabase.getDatabase(getApplicationContext()).policyDataDAO().getPolicyDataSync(); 43 | 44 | // Iterates over components defined in policy data, initializing and collecting events for each. 45 | for(String componentPath : policyData.paths.split(",")) { 46 | try { 47 | if (!componentPath.startsWith("android://")) { 48 | AppLog.w("ComponentWorker", "Invalid component path: " + componentPath); 49 | continue; 50 | } 51 | Component component = ComponentFactory.createInstance(componentPath); 52 | component.setup(getApplicationContext(), enrollmentData, policyData, ""); 53 | component.collectEvents(enrollmentData, policyData); 54 | 55 | } catch (Exception e) { 56 | AppLog.e("ComponentWorker", "Error processing component: " + componentPath); 57 | } 58 | } 59 | return Result.success(); // Indicates successful completion of worker tasks. 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/ElasticApi.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import okhttp3.RequestBody; 4 | import retrofit2.Call; 5 | import retrofit2.http.Body; 6 | import retrofit2.http.Header; 7 | import retrofit2.http.PUT; 8 | import retrofit2.http.Path; 9 | 10 | /** 11 | * Defines HTTP operations for interacting with an Elasticsearch server using Retrofit. 12 | * Supports operations such as inserting a single document and performing bulk insertions. 13 | */ 14 | public interface ElasticApi { 15 | 16 | /** 17 | * Inserts or updates a document in the specified Elasticsearch index. 18 | * 19 | * @param accessApiKeyHeader Authorization header containing the API key for Elasticsearch access. 20 | * @param index The Elasticsearch index where the document will be inserted or updated. 21 | * @param elasticDocument The document to insert or update, wrapped in an {@link ElasticDocument} object. 22 | * @return A {@link Call} object for the network request, with {@link ElasticResponse} as the expected response type. 23 | */ 24 | @PUT("/{index}/_doc") 25 | Call put( 26 | @Header("Authorization") String accessApiKeyHeader, 27 | @Path("index") String index, 28 | @Body ElasticDocument elasticDocument); 29 | 30 | 31 | /** 32 | * Performs a bulk operation in the specified Elasticsearch index, allowing for multiple documents 33 | * to be inserted or updated in a single request. 34 | * 35 | * @param accessApiKeyHeader Authorization header containing the API key for Elasticsearch access. 36 | * @param index The Elasticsearch index where the bulk operation will be performed. 37 | * @param elasticDocument A {@link RequestBody} object containing the bulk operation data. 38 | * @return A {@link Call} object for the network request, with {@link ElasticResponse} as the expected response type. 39 | */ 40 | @PUT("/{index}/_bulk") 41 | Call putBulk( 42 | @Header("Authorization") String accessApiKeyHeader, 43 | @Path("index") String index, 44 | @Body RequestBody elasticDocument); 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/ElasticDocument.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.TypeConverters; 5 | import com.google.gson.annotations.SerializedName; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | import java.util.List; 9 | import java.util.Locale; 10 | import java.util.TimeZone; 11 | 12 | /** 13 | * Abstract base class for all Elasticsearch documents generated by the agent. 14 | * This class defines common fields that are shared across different types of documents, 15 | * such as agent and host metadata, ensuring consistency in the data sent to Elasticsearch. 16 | * It is not used directly but serves as a superclass for specific document types 17 | * to inherit common properties and behaviors. 18 | */ 19 | public abstract class ElasticDocument { 20 | 21 | @SerializedName("@timestamp") 22 | protected String timestamp; 23 | 24 | // Agent metadata 25 | @SerializedName("agent.ephemeral_id") 26 | protected String agentEphemeralId; 27 | 28 | @SerializedName("agent.id") 29 | protected String agentId; 30 | 31 | @SerializedName("agent.name") 32 | protected String agentName; 33 | 34 | @SerializedName("agent.type") 35 | protected String agentType; 36 | 37 | @SerializedName("agent.version") 38 | protected String agentVersion; 39 | 40 | 41 | 42 | // Host metadata 43 | @SerializedName("host.architecture") 44 | protected String hostArchitecture; 45 | 46 | @SerializedName("host.hostname") 47 | protected String hostHostname; 48 | 49 | @SerializedName("host.id") 50 | protected String hostId; 51 | 52 | @SerializedName("host.ip") 53 | @ColumnInfo(name = "hostIp") 54 | @TypeConverters(AppConverters.class) 55 | protected List hostIp; 56 | 57 | @SerializedName("source.ip") 58 | @ColumnInfo(name = "sourceIp") 59 | @TypeConverters(AppConverters.class) 60 | protected List sourceIp; 61 | 62 | @SerializedName("host.mac") 63 | protected String hostMac; 64 | 65 | @SerializedName("host.name") 66 | protected String hostName; 67 | 68 | @SerializedName("host.os.build") 69 | protected String hostOsBuild; 70 | 71 | @SerializedName("host.os.family") 72 | protected String hostOsFamily; 73 | 74 | @SerializedName("host.os.kernel") 75 | protected String hostOsKernel; 76 | 77 | @SerializedName("host.os.name") 78 | protected String hostOsName; 79 | 80 | @SerializedName("host.os.name.text") 81 | protected String hostOsNameText; 82 | 83 | @SerializedName("host.os.platform") 84 | protected String hostOsPlatform; 85 | 86 | @SerializedName("host.os.version") 87 | protected String hostOsVersion; 88 | 89 | @SerializedName("host.os.type") 90 | protected String hostOsType; 91 | 92 | 93 | // Component metadata 94 | @SerializedName("component.id") 95 | protected String componentId; 96 | 97 | @SerializedName("component.old_state") 98 | protected String componentOldState; 99 | 100 | @SerializedName("component.state") 101 | protected String componentState; 102 | 103 | // Data stream metadata 104 | 105 | @SerializedName("data_stream.dataset") 106 | protected String dataStreamDataset; 107 | 108 | @SerializedName("data_stream.namespace") 109 | protected String dataStreamNamespace; 110 | 111 | @SerializedName("data_stream.type") 112 | protected String dataStreamType; 113 | 114 | @SerializedName("ecs.version") 115 | protected String ecsVersion; 116 | 117 | @SerializedName("elastic_agent.id") 118 | protected String elasticAgentId; 119 | 120 | @SerializedName("elastic_agent.snapshot") 121 | protected boolean elasticAgentSnapshot; 122 | 123 | @SerializedName("elastic_agent.version") 124 | protected String elasticAgentVersion; 125 | 126 | @SerializedName("elastic_agent.id_status") 127 | protected String elasticAgentIdStatus; 128 | 129 | @SerializedName("event.dataset") 130 | protected String eventDataset; 131 | 132 | 133 | /** 134 | * Default constructor initializing the document with a current timestamp. 135 | */ 136 | public ElasticDocument() { 137 | this.timestamp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).format(new Date()); 138 | } 139 | 140 | /** 141 | * Constructs an ElasticDocument using provided enrollment and policy data to populate fields. 142 | * This constructor is typically called by subclasses to ensure the base fields are initialized 143 | * with consistent agent and host metadata derived from the agent's current state. 144 | * 145 | * @param enrollmentData Data related to the agent's enrollment. 146 | * @param policyData Policy configurations applied to the agent. 147 | */ 148 | public ElasticDocument(FleetEnrollData enrollmentData, PolicyData policyData) { 149 | AgentMetadata metadata = AgentMetadata.getMetadataFromDeviceAndDB(enrollmentData.agentId, enrollmentData.hostname); 150 | 151 | // Parse agent metadata 152 | this.agentEphemeralId = "Unknown"; // TODO: Find out what this is 153 | this.agentId = enrollmentData.agentId; 154 | this.agentName = enrollmentData.hostname; 155 | this.agentType = "android"; 156 | this.agentVersion = BuildConfig.AGENT_VERSION; 157 | 158 | // Parse host metadata 159 | this.hostArchitecture = metadata.getLocal().host.arch; 160 | this.hostHostname = metadata.getLocal().host.hostname; 161 | this.hostId = metadata.getLocal().host.id; 162 | 163 | this.hostIp = metadata.getLocal().host.ip; 164 | this.sourceIp = metadata.getLocal().host.ip; 165 | 166 | this.hostMac = metadata.getLocal().host.mac.get(0); 167 | this.hostName = metadata.getLocal().host.name; 168 | this.hostOsType = "android"; 169 | this.hostOsBuild = metadata.getLocal().system.kernel; 170 | this.hostOsFamily = metadata.getLocal().system.family; 171 | this.hostOsKernel = metadata.getLocal().system.kernel; 172 | //this.hostOsName = metadata.getLocal().system.name; // DOES NOT WORK. Elastic does not accept this field to be set to "Android" 173 | //this.hostOsNameText = metadata.getLocal().system.name; // DOES NOT WORK. Elastic does not accept this field to be set to "Android" 174 | this.hostOsPlatform = metadata.getLocal().system.platform; 175 | this.hostOsVersion = metadata.getLocal().system.version; 176 | 177 | 178 | // Set the rest of the fields 179 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()); // Locale.US to Locale.getDefault() 180 | dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // This line ensures the time is in UTC 181 | this.timestamp = dateFormat.format(new Date()); 182 | this.componentId = "default"; 183 | this.componentOldState = "Healthy"; // TODO: Check if this is correct first 184 | this.componentState = "Healthy"; // True because otherwise the data would not be sent (at least in the current implementation) 185 | this.dataStreamDataset = policyData.dataStreamDataset; 186 | this.dataStreamNamespace = "default"; 187 | this.dataStreamType = "logs"; 188 | this.ecsVersion = "8.0.0"; // TODO: Make this a constant from build.gradle 189 | this.elasticAgentId = enrollmentData.agentId; 190 | this.elasticAgentSnapshot = false; // TODO: Maybe we can determine this from ES or Fleet 191 | this.elasticAgentVersion = BuildConfig.AGENT_VERSION; 192 | this.elasticAgentIdStatus = "verified"; // TODO: Check if we can determine this from ES or Fleet 193 | this.eventDataset = policyData.dataStreamDataset; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/ElasticResponse.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Represents a response from Elasticsearch, encapsulating details about the outcome 7 | * of an operation, such as indexing a document or executing a bulk request. It includes 8 | * information about any errors that occurred, as well as metrics like the time taken 9 | * for the operation to complete. 10 | */ 11 | public class ElasticResponse { 12 | 13 | private final String errors; 14 | private Error error; 15 | 16 | /** 17 | * Inner class representing detailed error information from Elasticsearch, 18 | * including the type of error and a descriptive reason. 19 | */ 20 | public static class Error { 21 | @SerializedName("type") 22 | private String type; 23 | 24 | @SerializedName("reason") 25 | private String reason; 26 | 27 | public String getType() { 28 | return type; 29 | } 30 | 31 | public String getReason() { 32 | return reason; 33 | } 34 | } 35 | 36 | /** 37 | * Constructs an instance of ElasticResponse, primarily for testing purposes, 38 | * allowing manual creation of response objects. 39 | * 40 | * @param errors A raw error message or description. 41 | * @param took The time taken for the operation, in milliseconds. 42 | */ 43 | public ElasticResponse(String errors, int took) { 44 | this.errors = errors; 45 | } 46 | 47 | public String getErrors() { 48 | return errors; 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetApi.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.http.Body; 5 | import retrofit2.http.GET; 6 | import retrofit2.http.Header; 7 | import retrofit2.http.POST; 8 | import retrofit2.http.Path; 9 | 10 | /** 11 | * Defines the HTTP API for interacting with the Fleet server. This interface includes methods 12 | * for checking the Fleet server's status, enrolling an agent with the server, and managing 13 | * agent check-ins and task acknowledgments. 14 | */ 15 | public interface FleetApi { 16 | 17 | /** 18 | * Retrieves the current status of the Fleet server. 19 | * 20 | * @return A {@link Call} object with {@link FleetStatusResponse} expected upon successful request. 21 | */ 22 | @GET("/api/status") 23 | Call getFleetStatus(); 24 | 25 | /** 26 | * Enrolls an agent with the Fleet server using a provided enrollment token. 27 | * 28 | * @param enrollmentTokenHeader The 'Authorization' header containing the enrollment token. 29 | * @param enrollRequest The enrollment request details. 30 | * @return A {@link Call} object with {@link FleetEnrollResponse} expected upon successful enrollment. 31 | */ 32 | @POST("/api/fleet/agents/enroll") 33 | Call enrollAgent(@Header("Authorization") String enrollmentTokenHeader, @Body FleetEnrollRequest enrollRequest); 34 | 35 | /** 36 | * Posts a check-in for an agent to the Fleet server, updating its status and receiving new tasks. 37 | * 38 | * @param accessApiKeyHeader The 'Authorization' header containing the agent's access API key. 39 | * @param id The unique identifier of the agent. 40 | * @param checkinRequest The check-in request details, including the agent's status and local metadata. 41 | * @return A {@link Call} object with {@link FleetCheckinResponse} expected upon successful check-in. 42 | */ 43 | @POST("/api/fleet/agents/{id}/checkin") 44 | Call postCheckin( 45 | @Header("Authorization") String accessApiKeyHeader, 46 | @Path("id") String id, 47 | @Body FleetCheckinRequest checkinRequest); 48 | 49 | /** 50 | * Posts an acknowledgment from an agent to the Fleet server, indicating that a task has been executed. 51 | * 52 | * @param accessApiKeyHeader The 'Authorization' header containing the agent's access API key. 53 | * @param id The unique identifier of the agent. 54 | * @param ackRequest The acknowledgment request details, typically including the task's unique ID and execution status. 55 | * @return A {@link Call} object with {@link AckResponse} expected upon successful acknowledgment. 56 | */ 57 | @POST("/api/fleet/agents/{id}/acks") 58 | Call postAck( 59 | @Header("Authorization") String accessApiKeyHeader, 60 | @Path("id") String id, 61 | @Body AckRequest ackRequest); 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetCheckinRequest.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import java.util.List; 5 | 6 | /** 7 | * Constructs a check-in request for the Fleet server, encapsulating the agent's 8 | * current status, local metadata, and additional information for potential actions 9 | * like upgrades. This class plays a pivotal role in the periodic communication between 10 | * the agent and the Fleet server, enabling the server to assess the agent's health, 11 | * issue new policies, or respond to the agent's state. 12 | */ 13 | public class FleetCheckinRequest { 14 | @SerializedName("status") 15 | private final String status; 16 | 17 | @SerializedName("ack_token") 18 | private final String ackToken; 19 | 20 | @SerializedName("local_metadata") 21 | private final AgentMetadata metadata; 22 | 23 | @SerializedName("message") 24 | private final String message; 25 | 26 | @SerializedName("components") 27 | private List components; 28 | 29 | @SerializedName("upgrade_details") 30 | private String upgradeDetails; 31 | 32 | /** 33 | * Constructs a FleetCheckinRequest object with the specified status, acknowledgment token, 34 | * agent metadata, and message. This constructor is used to create a check-in request 35 | * to be sent to the Fleet server, updating the agent's status and metadata. 36 | * 37 | * @param status The current status of the agent. 38 | * @param ackToken The acknowledgment token for the agent. 39 | * @param metadata The agent's local metadata. 40 | * @param message An optional message to include in the check-in request. 41 | */ 42 | public FleetCheckinRequest(String status, String ackToken, AgentMetadata metadata, String message) { 43 | this.status = status; 44 | this.ackToken = ackToken; 45 | this.metadata = metadata; 46 | this.message = message; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetCheckinWorker.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import androidx.annotation.NonNull; 5 | import androidx.work.Worker; 6 | import androidx.work.WorkerParameters; 7 | 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | /** 14 | * A background worker that handles periodic check-in with the Fleet server. 15 | * It fetches the current enrollment and policy data, then uses the FleetCheckinRepository 16 | * to perform the check-in operation. Depending on the check-in result and policy settings, 17 | * it might adjust the check-in frequency or mark the agent as unhealthy. 18 | */ 19 | public class FleetCheckinWorker extends Worker { 20 | private static final ExecutorService executor = Executors.newFixedThreadPool(4); 21 | 22 | /** 23 | * Initializes a new instance of the FleetCheckinWorker. 24 | * 25 | * @param context Application context. 26 | * @param workerParams Parameters for the work. 27 | */ 28 | public FleetCheckinWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { 29 | super(context, workerParams); 30 | } 31 | 32 | /** 33 | * Performs the check-in operation asynchronously. 34 | * 35 | * @return The result of the work, indicating success or retry requirements. 36 | */ 37 | @NonNull 38 | @Override 39 | public Result doWork() { 40 | AppLog.i("FleetCheckinWorker", "Performing check-in from background worker"); 41 | 42 | // Using FleetCheckinRepository for periodic check-in 43 | FleetCheckinRepository repository = new FleetCheckinRepository(null, null); 44 | 45 | // Obtain an instance of the AppDatabase 46 | AppDatabase db = AppDatabase.getDatabase(this.getApplicationContext()); 47 | 48 | // Synchronously fetch the enrollment data 49 | FleetEnrollData enrollmentData = db.enrollmentDataDAO().getEnrollmentInfoSync(1); 50 | AgentMetadata agentMetadata = AgentMetadata.getMetadataFromDeviceAndDB(enrollmentData.agentId, enrollmentData.hostname); 51 | PolicyData policyData = db.policyDataDAO().getPolicyDataSync(); 52 | AtomicBoolean finished = new AtomicBoolean(false); 53 | 54 | StatusCallback callback = success -> { 55 | // Create new thread to handle the callback 56 | executor.execute(() -> { 57 | try { 58 | if (!success) { 59 | 60 | AppStatisticsDataDAO statisticsData = db.statisticsDataDAO(); 61 | statisticsData.increaseTotalFailures(); 62 | 63 | // Use exponential backoff for the next check-in if enabled 64 | if (policyData.useBackoff) { 65 | // Calculate the intended backoff interval 66 | int intendedBackoff = getIntendedBackoff(policyData); 67 | policyData.backoffCheckinInterval = intendedBackoff; 68 | db.policyDataDAO().setBackoffCheckinInterval(intendedBackoff); 69 | 70 | // Set agent health status to unhealthy 71 | db.statisticsDataDAO().setAgentHealth("Unhealthy"); 72 | AppLog.w("FleetCheckinWorker", "Fleet checkin failed, increasing interval to " + policyData.backoffCheckinInterval + " seconds"); 73 | } 74 | } else { 75 | // If Elasticsearch PUT also succeeded, reset the agent health status 76 | if (policyData.backoffPutInterval == policyData.putInterval) { 77 | db.statisticsDataDAO().setAgentHealth("Healthy"); 78 | } 79 | // Reset the backoff interval 80 | policyData.backoffCheckinInterval = policyData.checkinInterval; 81 | db.policyDataDAO().resetBackoffCheckinInterval(); 82 | } 83 | } catch (Exception e) { 84 | AppLog.e("FleetCheckinWorker", "Unhandled app error during check-in worker: " + e.getMessage()); 85 | } 86 | 87 | // Schedule the next check-in. 88 | // Notice that we can't use a periodic worker, as the interval is dynamic and likely also under the minimum scheduling interval of 15 minutes. 89 | AppLog.i("FleetCheckinWorker", "Scheduling next Fleet checkin in " + policyData.backoffCheckinInterval + " seconds"); 90 | WorkScheduler.scheduleFleetCheckinWorker(getApplicationContext(), policyData.backoffCheckinInterval, TimeUnit.SECONDS, policyData.disableIfBatteryLow); 91 | finished.set(true); 92 | }); 93 | }; 94 | 95 | try { 96 | repository.checkinAgent(getApplicationContext(), enrollmentData, agentMetadata, callback); 97 | } catch (Exception e) { 98 | AppLog.e("FleetCheckinWorker", "Unhandled app error during check-in worker: " + e.getMessage()); 99 | callback.onCallback(false); 100 | } 101 | 102 | return Result.success(); 103 | } 104 | 105 | /** 106 | * Calculates the intended backoff interval based on the current policy data. 107 | * Normally, the backoff interval is doubled, but if a maximum backoff interval is set, 108 | * the minimum of the intended backoff and the maxBackoffInterval is used. 109 | * 110 | * @return The intended backoff interval in seconds. 111 | */ 112 | private static int getIntendedBackoff(PolicyData policyData) { 113 | int intendedBackoff; 114 | 115 | if (policyData.maxBackoffInterval == 0) { 116 | intendedBackoff = policyData.backoffCheckinInterval * 2; 117 | } else { 118 | intendedBackoff = Math.min(policyData.backoffCheckinInterval * 2, policyData.maxBackoffInterval); 119 | } 120 | return intendedBackoff; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetEnrollData.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.Entity; 5 | import androidx.room.PrimaryKey; 6 | 7 | /** 8 | * Represents the persistent data related to an agent's enrollment with a Fleet server. 9 | * This entity is stored in the local database and includes details necessary for managing 10 | * the agent's state and configuration as per the Fleet server's policy. 11 | */ 12 | @Entity 13 | public class FleetEnrollData { 14 | @PrimaryKey 15 | @ColumnInfo(name = "id") 16 | public int id; 17 | 18 | @ColumnInfo(name = "agent_id") 19 | public String agentId; 20 | 21 | @ColumnInfo(name = "is_enrolled") 22 | public boolean isEnrolled; 23 | 24 | @ColumnInfo(name = "hostname") 25 | public String hostname; 26 | 27 | @ColumnInfo(name = "fleet_url") 28 | public String fleetUrl; 29 | 30 | @ColumnInfo(name = "verify_cert") 31 | public boolean verifyCert; 32 | 33 | @ColumnInfo(name = "fleet_certificate") 34 | public String fleetCertificate; 35 | 36 | @ColumnInfo(name = "action") 37 | public String action; 38 | 39 | @ColumnInfo(name = "access_api_key_id") 40 | public String accessApiKeyId; 41 | 42 | @ColumnInfo(name = "active") 43 | public boolean active; 44 | 45 | @ColumnInfo(name = "enrolled_at") 46 | public String enrolledAt; 47 | 48 | @ColumnInfo(name = "policy_id") 49 | public String policyId; 50 | 51 | @ColumnInfo(name = "status") 52 | public String status; 53 | 54 | @ColumnInfo(name = "type") 55 | public String type; 56 | 57 | @ColumnInfo(name = "last_checkin") 58 | public String lastCheckin; 59 | 60 | @ColumnInfo(name = "last_policy_update") 61 | public String lastPolicyUpdate; 62 | 63 | @ColumnInfo(name = "policy") 64 | public String policy; 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetEnrollDataDAO.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.lifecycle.LiveData; 4 | import androidx.room.Dao; 5 | import androidx.room.Insert; 6 | import androidx.room.OnConflictStrategy; 7 | import androidx.room.Query; 8 | 9 | /** 10 | * Data Access Object (DAO) for managing FleetEnrollData entities within the Room database. 11 | * Provides methods for inserting, querying, and deleting enrollment information of an Elastic Agent. 12 | */ 13 | @Dao 14 | public interface FleetEnrollDataDAO { 15 | 16 | /** 17 | * Inserts a new FleetEnrollData record into the database or replaces an existing one 18 | * if a conflict occurs based on the primary key. 19 | * 20 | * @param enrollmentData The FleetEnrollData object to insert. 21 | */ 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | void insertEnrollmentInfo(FleetEnrollData enrollmentData); 24 | 25 | /** 26 | * Queries the database asynchronously for a FleetEnrollData record by its ID. 27 | * 28 | * @param id The ID of the enrollment record to find. 29 | * @return A LiveData object containing the FleetEnrollData record, if found. 30 | */ 31 | @Query("SELECT * FROM FleetEnrollData WHERE id = :id") 32 | LiveData getEnrollmentInfo(int id); 33 | 34 | /** 35 | * Queries the database synchronously for a FleetEnrollData record by its ID. 36 | * 37 | * @param id The ID of the enrollment record to find. 38 | * @return The FleetEnrollData record if found; null otherwise. 39 | */ 40 | @Query("SELECT * FROM FleetEnrollData WHERE id = :id") 41 | FleetEnrollData getEnrollmentInfoSync(int id); 42 | 43 | /** 44 | * Deletes all FleetEnrollData records from the database. 45 | */ 46 | @Query("DELETE FROM FleetEnrollData") 47 | void delete(); 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetEnrollRequest.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Represents an enrollment request sent to the Fleet server to enroll an agent. 7 | * This class contains all details required by the Fleet server to authenticate 8 | * and authorize the agent's enrollment request. 9 | */ 10 | public class FleetEnrollRequest { 11 | @SerializedName("type") 12 | private String type; 13 | 14 | @SerializedName("metadata") 15 | private AgentMetadata metadata; 16 | 17 | @SerializedName("enrollment_id") 18 | private String enrollmentId; 19 | 20 | @SerializedName("shared_id") 21 | private String sharedId; 22 | 23 | // Getters and Setters 24 | public String getType() { 25 | return type; 26 | } 27 | 28 | public void setType(String type) { 29 | this.type = type; 30 | } 31 | 32 | public void setMetadata(AgentMetadata metadata) { 33 | this.metadata = metadata; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetEnrollResponse.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import java.util.List; 5 | 6 | /** 7 | * Represents the response received from the Fleet server after enrolling an agent. 8 | * This class contains details about the enrollment status and the agent's access key that is later used for communication. 9 | * with the Fleet server. 10 | */ 11 | public class FleetEnrollResponse { 12 | private String action; 13 | private Item item; 14 | 15 | // Constructor (mostly for testing) 16 | public FleetEnrollResponse(String action, Item item) { 17 | this.action = action; 18 | this.item = item; 19 | } 20 | 21 | public static class Item { 22 | @SerializedName("access_api_key") 23 | private String accessApiKey; 24 | 25 | @SerializedName("access_api_key_id") 26 | private String accessApiKeyId; 27 | 28 | @SerializedName("active") 29 | 30 | private Boolean active; 31 | 32 | @SerializedName("enrolled_at") 33 | private String enrolledAt; 34 | 35 | @SerializedName("id") 36 | private String id; 37 | 38 | @SerializedName("policy_id") 39 | private String policyId; 40 | 41 | @SerializedName("status") 42 | private String status; 43 | 44 | @SerializedName("tags") 45 | private List tags; 46 | 47 | @SerializedName("type") 48 | private String type; 49 | 50 | /** 51 | * Constrctor only used for testing 52 | * @param accessApiKey The access API key 53 | * @param accessApiKeyId The access API key ID 54 | * @param active The active status 55 | * @param enrolledAt The enrollment date 56 | * @param id The ID 57 | * @param policyId The policy ID 58 | * @param status The status 59 | * @param tags The tags 60 | * @param type The type 61 | */ 62 | public Item(String accessApiKey, String accessApiKeyId, Boolean active, String enrolledAt, String id, String policyId, String status, List tags, String type) { 63 | this.accessApiKey = accessApiKey; 64 | this.accessApiKeyId = accessApiKeyId; 65 | this.active = active; 66 | this.enrolledAt = enrolledAt; 67 | this.id = id; 68 | this.policyId = policyId; 69 | this.status = status; 70 | this.tags = tags; 71 | this.type = type; 72 | } 73 | 74 | // Getters and Setters 75 | 76 | public String getAccessApiKey() { 77 | return accessApiKey; 78 | } 79 | 80 | public String getAccessApiKeyId() { 81 | return accessApiKeyId; 82 | } 83 | 84 | public Boolean getActive() { 85 | return active; 86 | } 87 | 88 | public String getEnrolledAt() { 89 | return enrolledAt; 90 | } 91 | 92 | public String getId() { 93 | return id; 94 | } 95 | 96 | public void setId(String id) { 97 | this.id = id; 98 | } 99 | 100 | public String getPolicyId() { 101 | return policyId; 102 | } 103 | 104 | public String getStatus() { 105 | return status; 106 | } 107 | 108 | public void setStatus(String status) { 109 | this.status = status; 110 | } 111 | } 112 | 113 | // Getters and Setters 114 | public String getAction() { 115 | return action; 116 | } 117 | 118 | public void setAction(String action) { 119 | this.action = action; 120 | } 121 | 122 | public Item getItem() { 123 | return item; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/FleetStatusResponse.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | /** 4 | * Represents the response received when querying the status of the Fleet server. 5 | * It contains the health and availability of the Fleet server. 6 | */ 7 | public class FleetStatusResponse { 8 | private String name; 9 | private String status; 10 | 11 | public FleetStatusResponse(String healthy) { 12 | this.status = healthy; 13 | } 14 | 15 | // Getters and setters 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public String getStatus() { 25 | return status; 26 | } 27 | 28 | public void setStatus(String status) { 29 | this.status = status; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/HelpActivity.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ClipData; 5 | import android.content.ClipboardManager; 6 | import android.content.Context; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.Button; 13 | import android.widget.EditText; 14 | import android.widget.LinearLayout; 15 | import android.widget.TextView; 16 | import android.widget.Toast; 17 | 18 | import androidx.appcompat.app.AppCompatActivity; 19 | import androidx.core.content.ContextCompat; 20 | 21 | import org.json.JSONObject; 22 | 23 | import java.net.MalformedURLException; 24 | import java.net.URISyntaxException; 25 | import java.net.URL; 26 | import java.text.MessageFormat; 27 | 28 | import de.swiftbird.elasticandroid.R.id; 29 | 30 | /** 31 | * HelpActivity provides a simple informational screen for the application. 32 | * It displays help for the end-user as well as administrators. It also displays the version of the application. 33 | * and its compatibility with the Elastic Agent. 34 | * Be aware that the static help text is written in the layout file. 35 | */ 36 | public class HelpActivity extends AppCompatActivity { 37 | @SuppressLint("SetTextI18n") 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_help); 42 | Button btnBack = findViewById(id.btnBack); 43 | TextView tvVersionInfo = findViewById(id.tvVersionInfo); 44 | tvVersionInfo.setText("App Version: " + BuildConfig.VERSION_NAME + "\nElastic Agent Compatibility: " + BuildConfig.AGENT_VERSION); 45 | 46 | btnBack.setOnClickListener(view -> { 47 | finish(); 48 | }); 49 | 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/LegalActivity.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.os.Bundle; 4 | import android.widget.Button; 5 | import android.widget.TextView; 6 | import androidx.appcompat.app.AppCompatActivity; 7 | 8 | /** 9 | * LegalActivity displays the legal disclaimer for the application. 10 | * It provides information about the software's warranty, liability, and prohibited uses. 11 | */ 12 | public class LegalActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_legal); 18 | Button btnBack = findViewById(R.id.btnBack); 19 | String legalDisclaimerText = getString(R.string.legal_disclaimer_text); 20 | 21 | TextView tvLegalText = findViewById(R.id.tvLegalText); 22 | tvLegalText.setText(getString(R.string.legal_disclaimer_text)); 23 | tvLegalText.setText(legalDisclaimerText); 24 | 25 | btnBack.setOnClickListener(view -> { 26 | finish(); 27 | }); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/LicenseActivity.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.widget.Button; 6 | import android.widget.TextView; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import com.google.android.gms.oss.licenses.OssLicensesMenuActivity; 9 | 10 | /** 11 | * LicenseActivity displays the license information for the application. 12 | * It provides information about the software's license, warranty, and liability. 13 | * It also provides a button to view the open source licenses. 14 | */ 15 | public class LicenseActivity extends AppCompatActivity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_licenses); 21 | Button btnBack = findViewById(R.id.btnBack); 22 | Button btnOSSLicenses = findViewById(R.id.btnOSSLicenses); 23 | TextView tvLicenseText = findViewById(R.id.tvLicenseText); 24 | String mitLicenseText = getString(R.string.mit_license_text); 25 | 26 | tvLicenseText.setText(mitLicenseText); 27 | 28 | btnOSSLicenses.setOnClickListener(view -> { 29 | OssLicensesMenuActivity.setActivityTitle("Open Source Licenses"); 30 | startActivity(new Intent(this, OssLicensesMenuActivity.class)); 31 | }); 32 | 33 | btnBack.setOnClickListener(view -> { 34 | finish(); 35 | }); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/LocationCompBuffer.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Insert; 5 | import androidx.room.Query; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Data Access Object (DAO) for handling the storage and retrieval of {@link LocationCompDocument} objects, 11 | * which represent network log events collected by the application. 12 | */ 13 | @Dao 14 | public interface LocationCompBuffer { 15 | 16 | /** 17 | * Inserts a single location document into the database. 18 | * 19 | * @param document The location document to insert. 20 | */ 21 | @Insert 22 | void insertDocument(LocationCompDocument document); 23 | 24 | /** 25 | * Retrieves all location documents from the database, ordered by their timestamp in ascending order. 26 | * 27 | * @return A list of all location documents. 28 | */ 29 | @Query("SELECT * FROM LocationCompDocument ORDER BY timestamp ASC") 30 | List getAllDocuments(); 31 | 32 | /** 33 | * Deletes all location documents from the database. 34 | */ 35 | @Query("DELETE FROM LocationCompDocument") 36 | void deleteAllDocuments(); 37 | 38 | /** 39 | * Retrieves the oldest location documents up to a specified limit. 40 | * 41 | * @param maxDocuments The maximum number of documents to retrieve. 42 | * @return A list of the oldest location documents, limited by maxDocuments. 43 | */ 44 | @Query("SELECT * FROM LocationCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments") 45 | List getOldestDocuments(int maxDocuments); 46 | 47 | /** 48 | * Counts the total number of location documents in the database. 49 | * 50 | * @return The count of location documents. 51 | */ 52 | @Query("SELECT COUNT(*) FROM LocationCompDocument") 53 | int getDocumentCount(); 54 | 55 | /** 56 | * Deletes the oldest location documents from the database, up to a specified number. 57 | * 58 | * @param maxDocuments The maximum number of oldest documents to delete. 59 | */ 60 | @Query("DELETE FROM LocationCompDocument WHERE id IN (SELECT id FROM LocationCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments)") 61 | void deleteOldestDocuments(int maxDocuments); 62 | 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/LocationForegroundService.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.Service; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.location.LocationManager; 10 | import android.os.IBinder; 11 | import androidx.core.app.NotificationCompat; 12 | 13 | /** 14 | * A foreground service designed to continuously track location updates in the background. 15 | * This service leverages the {@link LocationManager} to request periodic location updates. 16 | * It ensures the app remains alive and can perform operations even when the app is in the background. 17 | *

18 | * Upon starting, the service moves itself to the foreground state with a persistent notification, 19 | * which is a requirement from Android Oreo onwards to allow background location tracking. 20 | */ 21 | public class LocationForegroundService extends Service { 22 | // Channel ID for the notification 23 | private static final String CHANNEL_ID = "location_service"; 24 | 25 | // Default values for requesting location updates (in case they are not provided) 26 | private static final long MIN_TIME_MS = 30000; // 30 seconds 27 | private static final float MIN_DISTANCE_METERS = 10; // 10 meters 28 | 29 | private LocationManager locationManager; 30 | 31 | @Override 32 | public void onCreate() { 33 | super.onCreate(); 34 | locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); 35 | } 36 | 37 | /** 38 | * Starts the service in the foreground and requests location updates. 39 | * Parameters for requesting location updates (e.g., minimum time interval, minimum distance, 40 | * location provider) are extracted from the Intent passed to this method. 41 | * 42 | * @param intent The Intent supplied to {@link Context#startService}, containing the configuration for location updates. 43 | * @param flags Additional data about this start request. 44 | * @param startId A unique integer representing this specific request to start. 45 | * @return The return value indicates what semantics the system should use for the service's current started state. 46 | */ 47 | @Override 48 | public int onStartCommand(Intent intent, int flags, int startId) { 49 | startForeground(1, buildForegroundNotification()); 50 | long minTimeMs; 51 | float minDistanceMeters; 52 | String provider; 53 | 54 | try { 55 | // Get minTimeMs and minDistanceMeters from intent 56 | minTimeMs = intent.getLongExtra("minTimeMs", 30000); 57 | minDistanceMeters = intent.getFloatExtra("minDistanceMeters", 10); 58 | provider = intent.getStringExtra("provider"); 59 | } catch (Exception e) { 60 | AppLog.e("LocationForegroundService", "Failed to get minTimeMs, minDistanceMeters, or provider from intent. Using default values. Error: " + e.getMessage()); 61 | minTimeMs = MIN_TIME_MS; 62 | minDistanceMeters = MIN_DISTANCE_METERS; 63 | provider = null; 64 | } 65 | if(provider == null) { 66 | provider = LocationManager.GPS_PROVIDER; 67 | } 68 | 69 | AppLog.d("LocationForegroundService", "Requesting location updates with minTimeMs=" + minTimeMs + ", minDistanceMeters=" + minDistanceMeters + ", provider=" + provider); 70 | try { 71 | locationManager.requestLocationUpdates(provider, minTimeMs, minDistanceMeters, new LocationReceiver(this.getApplicationContext())); 72 | } catch (SecurityException e) { 73 | AppLog.w("LocationForegroundService", "Failed to request location updates: " + e.getMessage()); 74 | } 75 | 76 | return START_STICKY; 77 | } 78 | 79 | @Override 80 | public IBinder onBind(Intent intent) { 81 | return null; // Not used for this service 82 | } 83 | 84 | 85 | /** 86 | * Builds the persistent notification required for a foreground service. 87 | * This method sets up the notification channel and builds the notification that will 88 | * be shown as long as this service is in the foreground. 89 | * 90 | * @return The notification instance that describes this foreground service. 91 | */ 92 | private Notification buildForegroundNotification() { 93 | NotificationChannel channel; 94 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 95 | channel = new NotificationChannel(CHANNEL_ID, "Location Service", NotificationManager.IMPORTANCE_LOW); 96 | channel.setDescription("No sound"); 97 | channel.setSound(null, null); // No sound for this channel 98 | NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 99 | if (manager != null) { 100 | manager.createNotificationChannel(channel); 101 | } 102 | } 103 | 104 | // If this is a debug build we allow the notification to be swiped away 105 | boolean setOngoing = !BuildConfig.DEBUG; 106 | 107 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) 108 | .setContentTitle("Elastic Agent Android") 109 | .setContentText("Location service is running") 110 | .setSmallIcon(R.drawable.icon) 111 | .setPriority(NotificationCompat.PRIORITY_LOW) 112 | .setOngoing(setOngoing); 113 | return builder.build(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/LocationReceiver.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.annotation.NonNull; 4 | import android.content.Context; 5 | import android.os.Handler; 6 | import android.os.HandlerThread; 7 | 8 | /** 9 | * LocationReceiver is a custom {@link android.location.LocationListener} implementation designed to 10 | * handle location updates. When a new location is received, it processes the location data and 11 | * manages the creation and buffering of {@link LocationCompDocument} instances, which are then stored 12 | * for later use or transmission. 13 | * 14 | *

This class initiates a new Thread to handle each location update to ensure that the processing 15 | * does not interfere with the UI or other main thread operations, promoting smoother performance 16 | * and better user experience. 17 | */ 18 | public class LocationReceiver implements android.location.LocationListener{ 19 | private final Context context; 20 | private final Handler handler; 21 | private final LocationComp locationComp; 22 | 23 | /** 24 | * Constructs a LocationReceiver with the specified application context. 25 | * Using application context helps avoid potential memory leaks that could occur 26 | * if an activity context was used. 27 | * 28 | * @param context The application context used for database and logging operations. 29 | */ 30 | public LocationReceiver(Context context) { 31 | // Use application context to avoid potential memory leaks 32 | this.context = context.getApplicationContext(); 33 | HandlerThread handlerThread = new HandlerThread("LocationHandler"); 34 | handlerThread.start(); 35 | handler = new Handler(handlerThread.getLooper()); 36 | 37 | // Initialize the location component 38 | locationComp = LocationComp.getInstance(); 39 | locationComp.setup_light(context); 40 | } 41 | 42 | /** 43 | * Called when the location has changed. This method is triggered when a new location update. 44 | * It will execute the {@link #handleLocationUpdate(android.location.Location)} method on a new thread. 45 | * 46 | * @param location The new location, as a {@link android.location.Location} object. 47 | */ 48 | @Override 49 | public void onLocationChanged(@NonNull android.location.Location location) { 50 | // Handle location update on a new thread 51 | handler.post(() -> handleLocationUpdate(location)); 52 | } 53 | 54 | /** 55 | * This method retrieves or creates necessary components and database access objects, 56 | * creates a new {@link LocationCompDocument} with the location data, and then adds this 57 | * document to the buffer for later processing or transmission. 58 | * 59 | * @param location The location data to be processed. 60 | */ 61 | private void handleLocationUpdate(android.location.Location location) { 62 | AppLog.d("LocationReceiver", "Location changed: " + location); 63 | AppDatabase db = AppDatabase.getDatabase(context); 64 | PolicyData policyData = db.policyDataDAO().getPolicyDataSync(); 65 | FleetEnrollData enrollmentData = db.enrollmentDataDAO().getEnrollmentInfoSync(1); 66 | 67 | try { 68 | locationComp.addDocumentToBuffer(new LocationCompDocument(location, enrollmentData, policyData, context)); 69 | } catch (Exception e) { 70 | AppLog.e("LocationReceiver", "Error adding location document to buffer", e); 71 | } 72 | } 73 | 74 | @Override 75 | public void onStatusChanged(String provider, int status, android.os.Bundle extras) {} 76 | 77 | @Override 78 | public void onProviderEnabled(@NonNull String provider) {} 79 | 80 | @Override 81 | public void onProviderDisabled(@NonNull String provider) {} 82 | } 83 | 84 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/NetworkLogsCompBuffer.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Insert; 5 | import androidx.room.Query; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Data Access Object (DAO) for handling the storage and retrieval of {@link NetworkLogsCompDocument} objects, 11 | * which represent network log events collected by the Android operating system. 12 | * 13 | *

Functionality is similar to LocationCompBuffer.java so refer to that file for more details.

14 | */ 15 | @Dao 16 | public interface NetworkLogsCompBuffer { 17 | @Insert 18 | void insertDocument(NetworkLogsCompDocument document); 19 | 20 | @Query("SELECT * FROM NetworkLogsCompDocument ORDER BY timestamp ASC") 21 | List getAllDocuments(); 22 | 23 | @Query("DELETE FROM NetworkLogsCompDocument") 24 | void deleteAllDocuments(); 25 | 26 | @Query("SELECT * FROM NetworkLogsCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments") 27 | List getOldestDocuments(int maxDocuments); 28 | 29 | @Query("SELECT COUNT(*) FROM NetworkLogsCompDocument") 30 | int getDocumentCount(); 31 | 32 | @Query("DELETE FROM NetworkLogsCompDocument WHERE id IN (SELECT id FROM NetworkLogsCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments)") 33 | void deleteOldestDocuments(int maxDocuments); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/PolicyData.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.Entity; 5 | import androidx.room.PrimaryKey; 6 | 7 | /** 8 | * Represents the policy data entity stored in the local database. This entity encapsulates 9 | * all policy-related configurations received from the fleet server, including intervals 10 | * for check-ins and data submission, protection settings, and data stream configurations. 11 | * 12 | *

This entity is crucial for ensuring the app operates in alignment with the current 13 | * policy set by the fleet server, allowing for dynamic adjustment of behavior 14 | * based on received policies.

15 | * 16 | */ 17 | @Entity 18 | public class PolicyData { 19 | @PrimaryKey(autoGenerate = true) 20 | public int id; 21 | 22 | // Time when the policy was created on the fleet server. 23 | @ColumnInfo(name = "created_at") 24 | public String createdAt; 25 | 26 | // Revision number to track policy updates. 27 | @ColumnInfo(name = "revision") 28 | public int revision; 29 | 30 | // Flag indicating if device protection is enabled. 31 | @ColumnInfo(name = "protection_enabled") 32 | public boolean protectionEnabled; 33 | 34 | // Hash of the uninstallation token, used for secure removal. 35 | @ColumnInfo(name = "uninstall_token_hash") 36 | public String uninstallTokenHash; 37 | 38 | // Name of the input configured in the policy. 39 | @ColumnInfo(name = "input_name") 40 | public String inputName; 41 | 42 | // Determines whether users can unenroll the device. 43 | @ColumnInfo(name = "allow_user_unenroll") 44 | public boolean allowUserUnenroll; 45 | 46 | // Identifies the log package name- and version for data submission. 47 | @ColumnInfo(name = "log_package_name") 48 | public String logPackageName; 49 | 50 | @ColumnInfo(name = "log_package_version") 51 | public String logPackageVersion; 52 | 53 | // Identifies the data stream dataset for document submission. 54 | @ColumnInfo(name = "data_stream_dataset") 55 | public String dataStreamDataset; 56 | 57 | // Configures how old data can be before being ignored (not used). 58 | @ColumnInfo(name = "ignore_older") 59 | public String ignoreOlder; 60 | 61 | // Generic interval setting, context-specific use (not used). 62 | @ColumnInfo(name = "interval") 63 | public String interval; 64 | 65 | // Regular interval for check-in with the fleet server. 66 | @ColumnInfo(name = "checkin_interval") 67 | public int checkinInterval; 68 | 69 | // Backoff interval for check-in attempts after failures. 70 | @ColumnInfo(name = "backoff_checkin_interval") 71 | public int backoffCheckinInterval; 72 | 73 | // Interval for submitting data to the Elasticsearch data stream index. 74 | @ColumnInfo(name = "put_interval") 75 | public int putInterval; 76 | 77 | // Backoff interval for data submission after failures. 78 | @ColumnInfo(name = "backoff_put_interval") 79 | public int backoffPutInterval; 80 | 81 | // Maximum number of documents to submit per request. 82 | @ColumnInfo(name = "max_documents_per_request") 83 | public int maxDocumentsPerRequest; 84 | 85 | // Disable data submission when the device has low battery. 86 | @ColumnInfo(name = "disable_if_battery_low") 87 | public boolean disableIfBatteryLow; 88 | 89 | // Concatenated list of component paths included in the policy. 90 | @ColumnInfo(name = "paths") 91 | public String paths; 92 | 93 | // Elasticsearch host for data submission. 94 | @ColumnInfo(name = "hosts") 95 | public String hosts; 96 | 97 | // Fingerprint for SSL certificate verification. 98 | @ColumnInfo(name = "ssl_ca_trusted_fingerprint") 99 | public String sslCaTrustedFingerprint; 100 | 101 | // Full SSL certificate for establishing trust. 102 | @ColumnInfo(name = "ssl_ca_trusted_full") 103 | public String sslCaTrustedFull; 104 | 105 | // Flag to enable backoff strategy. 106 | @ColumnInfo(name = "use_backoff") 107 | public boolean useBackoff; 108 | 109 | // Maximum interval for backoff strategy. 110 | @ColumnInfo(name = "max_backoff_interval") 111 | public int maxBackoffInterval; 112 | 113 | // Enables backoff when the buffer is empty. 114 | @ColumnInfo(name = "backoff_on_empty_buffer") 115 | public boolean backoffOnEmptyBuffer; 116 | 117 | // Identifier for the output policy configuration. 118 | @ColumnInfo(name = "output_policy_id") 119 | public String outputPolicyId; 120 | 121 | // Last time the policy was updated. 122 | @ColumnInfo(name = "last_updated") 123 | public String lastUpdated; 124 | 125 | // Identifier for the check-in action, used for acknowledgments. 126 | @ColumnInfo(name = "checkin_action_id") 127 | public String actionId; 128 | } -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/PolicyDataDAO.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.lifecycle.LiveData; 4 | import androidx.room.Dao; 5 | import androidx.room.Insert; 6 | import androidx.room.OnConflictStrategy; 7 | import androidx.room.Query; 8 | 9 | /** 10 | * Data Access Object (DAO) for managing {@link PolicyData} in the application's database. 11 | * Provides methods to insert, query, update, and delete policy data, allowing for real-time 12 | * and synchronized management of operational policies received from the Fleet server. 13 | * 14 | *

Includes specialized methods for handling backoff strategies to manage network communication 15 | * retries efficiently, ensuring the Elastic Agent remains resilient under various network conditions 16 | * and server response scenarios.

17 | * 18 | *

Methods are annotated with Room annotations to define SQL queries and operations on the database.

19 | * 20 | */ 21 | @Dao 22 | public interface PolicyDataDAO { 23 | 24 | /** 25 | * Inserts a new {@link PolicyData} record into the database or replaces an existing one 26 | * if a conflict occurs based on the primary key. 27 | * 28 | * @param policyData The {@link PolicyData} object to insert. 29 | */ 30 | @Insert(onConflict = OnConflictStrategy.REPLACE) 31 | void insertPolicyData(PolicyData policyData); 32 | 33 | /** 34 | * Queries the database asynchronously for the {@link PolicyData} record. 35 | * 36 | * @return A LiveData object containing the {@link PolicyData} record. 37 | */ 38 | @Query("SELECT * FROM PolicyData") 39 | LiveData getPolicyData(); 40 | 41 | /** 42 | * Queries the database synchronously for the {@link PolicyData} record. 43 | * 44 | * @return The {@link PolicyData} record if found; null otherwise. 45 | */ 46 | @Query("SELECT * FROM PolicyData") 47 | PolicyData getPolicyDataSync(); 48 | 49 | /** 50 | * Updates the last updated timestamp and checkin action ID in the {@link PolicyData} record. 51 | * 52 | * @param lastUpdated The new last updated timestamp. 53 | * @param actionId The new checkin action ID. 54 | */ 55 | @Query("UPDATE PolicyData SET last_updated = :lastUpdated, checkin_action_id = :actionId") 56 | void refreshPolicyData(String lastUpdated, String actionId); 57 | 58 | /** 59 | * Deletes all {@link PolicyData} records from the database. 60 | */ 61 | @Query("DELETE FROM PolicyData") 62 | void delete(); 63 | 64 | /** 65 | * Increases the backoff put interval (for communicating with Elasticsearch) by doubling the current value. 66 | */ 67 | @Query("UPDATE PolicyData SET backoff_put_interval = backoff_put_interval * 2") 68 | void increaseBackoffPutInterval(); 69 | 70 | /** 71 | * Sets the backoff put interval (for communicating with Elasticsearch) to a specific value. 72 | * 73 | * @param interval The new backoff put interval value. 74 | */ 75 | @Query("UPDATE PolicyData SET backoff_put_interval = :interval") 76 | void setBackoffPutInterval(int interval); 77 | 78 | /** 79 | * Resets the backoff put (for communicating with Elasticsearch) interval to the default put interval value. 80 | */ 81 | @Query("UPDATE PolicyData SET backoff_put_interval = put_interval") 82 | void resetBackoffPutInterval(); 83 | 84 | /** 85 | * Increases the backoff checkin interval (for communicating with the Fleet server) by doubling the current value. 86 | */ 87 | @Query("UPDATE PolicyData SET backoff_checkin_interval = backoff_checkin_interval * 2") 88 | void increaseBackoffCheckinInterval(); 89 | 90 | /** 91 | * Sets the backoff checkin interval (for communicating with the Fleet server) to a specific value. 92 | * 93 | * @param interval The new backoff checkin interval value. 94 | */ 95 | @Query("UPDATE PolicyData SET backoff_checkin_interval = :interval") 96 | void setBackoffCheckinInterval(int interval); 97 | 98 | /** 99 | * Resets the backoff checkin interval (for communicating with the Fleet server) to the default checkin interval value. 100 | */ 101 | @Query("UPDATE PolicyData SET backoff_checkin_interval = checkin_interval") 102 | void resetBackoffCheckinInterval(); 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/SecurityLogsCompBuffer.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Insert; 5 | import androidx.room.Query; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Data Access Object (DAO) for handling the storage and retrieval of {@link SecurityLogsCompDocument} objects, 11 | * which represent security log events collected by the Android operating system. 12 | * 13 | *

Functionality is similar to LocationCompBuffer.java so we see the same methods there.

14 | */ 15 | @Dao 16 | public interface SecurityLogsCompBuffer { 17 | @Insert 18 | void insertDocument(SecurityLogsCompDocument document); 19 | 20 | @Query("SELECT * FROM SecurityLogsCompDocument ORDER BY timestamp ASC") 21 | List getAllDocuments(); 22 | 23 | @Query("DELETE FROM SecurityLogsCompDocument") 24 | void deleteAllDocuments(); 25 | 26 | // Get X oldest documents 27 | @Query("SELECT * FROM SecurityLogsCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments") 28 | List getOldestDocuments(int maxDocuments); 29 | 30 | // Count the number of documents in the buffer, return 0 if no documents 31 | @Query("SELECT COUNT(*) FROM SecurityLogsCompDocument") 32 | int getDocumentCount(); 33 | 34 | // Delete the oldest X documents 35 | 36 | @Query("DELETE FROM SecurityLogsCompDocument WHERE id IN (SELECT id FROM SecurityLogsCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments)") 37 | void deleteOldestDocuments(int maxDocuments); 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/SecurityLogsCompDocument.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.Entity; 5 | import androidx.room.PrimaryKey; 6 | 7 | import com.google.gson.annotations.SerializedName; 8 | 9 | /** 10 | * Represents a document for security log events within the Elastic Android application. This class extends the 11 | * {@link ElasticDocument} to include specific fields related to security events, such as log level, event tag, and the log message itself. 12 | * It's used to model and store information about security-related activities detected on the device, facilitating their later analysis 13 | * and processing. 14 | */ 15 | 16 | @Entity 17 | public class SecurityLogsCompDocument extends ElasticDocument { 18 | @PrimaryKey(autoGenerate = true) 19 | @ColumnInfo(name = "id") 20 | public int id; 21 | 22 | @SerializedName("event.action") 23 | @ColumnInfo(name = "event_action") 24 | public String eventAction = "security"; 25 | 26 | @SerializedName("event.category") 27 | @ColumnInfo(name = "event_category") 28 | public String eventCategory = "log"; 29 | 30 | @SerializedName("log.level") 31 | @ColumnInfo(name = "log_level") 32 | public String logLevel; 33 | 34 | @SerializedName("tag") 35 | @ColumnInfo(name = "tag") 36 | public String tag; 37 | 38 | @SerializedName("message") 39 | @ColumnInfo(name = "message") 40 | public String message; 41 | 42 | // Constructor using superclass constructor and setting own fields 43 | public SecurityLogsCompDocument() {} 44 | public SecurityLogsCompDocument(FleetEnrollData fleetEnrollData, PolicyData policyData, String logLevel, String tag, String message) { 45 | super(fleetEnrollData, policyData); 46 | this.logLevel = logLevel; 47 | this.tag = tag; 48 | this.message = message; 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/SelfLogComp.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import java.util.List; 6 | 7 | /** 8 | * Manages the collection and storage of internal application logs for the Elastic Android application. 9 | * This component facilitates the recording of operational events, errors, and informational messages within the app, 10 | * helping in debugging and monitoring activities. 11 | * 12 | *

Logs are captured and stored persistently using a Room database, allowing for their retrieval and analysis over time. 13 | * The {@link SelfLogCompDocument} class represents individual log entries, which are managed by this component.

14 | * 15 | *

The {@link AppLog} class is utilized to add logs to the buffer. It abstracts the complexity of directly interacting with 16 | * the logging mechanism, providing a simple interface for recording logs from anywhere within the application.

17 | * 18 | *

For method documentation, refer to the Component interface. 19 | */ 20 | public class SelfLogComp implements Component { 21 | 22 | private static SelfLogComp selfLogComp; 23 | private SelfLogCompBuffer buffer; 24 | private AppStatisticsDataDAO statistic; 25 | 26 | public static synchronized SelfLogComp getInstance() { 27 | // Singleton pattern 28 | if (selfLogComp == null) { 29 | selfLogComp = new SelfLogComp(); 30 | } 31 | return selfLogComp; 32 | } 33 | 34 | @Override 35 | public boolean setup(Context context, FleetEnrollData enrollmentData, PolicyData policyData, String subComponent) { 36 | // Initialize Room database and get the DAO 37 | AppDatabase db = AppDatabase.getDatabase(context); 38 | buffer = db.selfLogCompBuffer(); 39 | statistic = db.statisticsDataDAO(); 40 | return true; 41 | } 42 | 43 | @Override 44 | public void addDocumentToBuffer(ElasticDocument document) { 45 | if (document instanceof SelfLogCompDocument) { 46 | if (buffer != null) { 47 | buffer.insertDocument((SelfLogCompDocument) document); 48 | statistic.increaseCombinedBufferSize(1); 49 | } else { 50 | Log.e("SelfLogComp", "Buffer not initialized"); 51 | throw new IllegalStateException("SelfLogComp buffer has not been initialized."); 52 | } 53 | } else { 54 | Log.e("SelfLogComp", "Invalid document type provided"); 55 | throw new IllegalArgumentException("Only SelfLogCompDocument instances can be added to the buffer."); 56 | } 57 | } 58 | 59 | 60 | @Override 61 | public List getDocumentsFromBuffer(int maxDocuments) { 62 | int toIndex = Math.min(maxDocuments, buffer.getDocumentCount()); 63 | List logBuffer = buffer.getOldestDocuments(toIndex); 64 | buffer.deleteOldestDocuments(toIndex); 65 | 66 | @SuppressWarnings("unchecked") // Safe cast 67 | List result = (List) logBuffer; 68 | return result; 69 | } 70 | 71 | public int getDocumentsInBufferCount() { 72 | return buffer.getDocumentCount(); 73 | } 74 | 75 | @Override 76 | public String getPathName() { 77 | return "self-log"; 78 | } 79 | 80 | @Override 81 | public void disable(Context context, FleetEnrollData enrollmentData, PolicyData policyData) { 82 | try { 83 | AppLog.shutdownLogger(); 84 | this.statistic = null; 85 | this.buffer = null; 86 | } catch (Exception e) { 87 | Log.e("SelfLogComp", "Failed to shut down logger properly", e); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/SelfLogCompBuffer.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Insert; 5 | import androidx.room.Query; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Data Access Object (DAO) for handling the storage and retrieval of {@link SelfLogCompDocument} objects, 11 | * which represent log events collected by the agent itself. 12 | * 13 | *

Functionality is similar to LocationCompBuffer.java so we will not repeat the documentation here. 14 | */ 15 | @Dao 16 | public interface SelfLogCompBuffer { 17 | @Insert 18 | void insertDocument(SelfLogCompDocument document); 19 | 20 | @Query("SELECT * FROM SelfLogCompDocument ORDER BY timestamp ASC") 21 | List getAllDocuments(); 22 | 23 | @Query("DELETE FROM SelfLogCompDocument") 24 | void deleteAllDocuments(); 25 | 26 | // Get X oldest documents 27 | @Query("SELECT * FROM SelfLogCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments") 28 | List getOldestDocuments(int maxDocuments); 29 | 30 | // Count the number of documents in the buffer, return 0 if no documents 31 | @Query("SELECT COUNT(*) FROM SelfLogCompDocument") 32 | int getDocumentCount(); 33 | 34 | // Delete the oldest X documents 35 | 36 | @Query("DELETE FROM SelfLogCompDocument WHERE id IN (SELECT id FROM SelfLogCompDocument ORDER BY timestamp ASC LIMIT :maxDocuments)") 37 | void deleteOldestDocuments(int maxDocuments); 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/SelfLogCompDocument.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import androidx.room.ColumnInfo; 4 | import androidx.room.Entity; 5 | import androidx.room.PrimaryKey; 6 | import com.google.gson.annotations.SerializedName; 7 | 8 | /** 9 | * Represents a document for self-logging within the Elastic Android application. This document captures internal log messages 10 | * generated by the application, including operational events, errors, and informational messages. 11 | * 12 | *

Each log entry is characterized by its level (e.g., INFO, DEBUG, ERROR), a tag that identifies the log's source or category, 13 | * and the log message itself. These attributes facilitate the organization and retrieval of logs for analysis and debugging purposes.

14 | * 15 | *

Instances of this class are created and managed by the {@link SelfLogComp}, which collects, stores, and manages access 16 | * to these log documents. The {@link AppLog} class, in conjunction with {@link SelfLogComp}, is used to add entries to the log, 17 | * demonstrating a decoupled approach to logging within the application.

18 | * 19 | *

Note: The {@code eventAction} and {@code eventCategory} fields are preset to "syslog" and "log" respectively.

20 | */ 21 | @Entity 22 | public class SelfLogCompDocument extends ElasticDocument { 23 | @PrimaryKey(autoGenerate = true) 24 | @ColumnInfo(name = "id") 25 | public int id; 26 | 27 | @SerializedName("event.action") 28 | @ColumnInfo(name = "event_action") 29 | public String eventAction = "syslog"; 30 | 31 | @SerializedName("event.category") 32 | @ColumnInfo(name = "event_category") 33 | public String eventCategory = "log"; 34 | 35 | @SerializedName("log.level") 36 | @ColumnInfo(name = "log_level") 37 | public String logLevel; 38 | 39 | @SerializedName("tag") 40 | @ColumnInfo(name = "tag") 41 | public String tag; 42 | 43 | @SerializedName("message") 44 | @ColumnInfo(name = "message") 45 | public String message; 46 | 47 | // Constructor using superclass constructor and setting own fields 48 | public SelfLogCompDocument() {} 49 | public SelfLogCompDocument(FleetEnrollData fleetEnrollData, PolicyData policyData, String logLevel, String tag, String message) { 50 | super(fleetEnrollData, policyData); 51 | this.logLevel = logLevel; 52 | this.tag = tag; 53 | this.message = message; 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/StatusCallback.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | /** 4 | * Callback interface for asynchronous operations that return a status. 5 | */ 6 | public interface StatusCallback { 7 | void onCallback(boolean success); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/de/swiftbird/elasticandroid/WorkScheduler.java: -------------------------------------------------------------------------------- 1 | package de.swiftbird.elasticandroid; 2 | 3 | import android.content.Context; 4 | import androidx.work.Constraints; 5 | import androidx.work.ExistingWorkPolicy; 6 | import androidx.work.NetworkType; 7 | import androidx.work.OneTimeWorkRequest; 8 | import androidx.work.WorkManager; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * Manages the scheduling of background tasks related to fleet check-in and Elasticsearch data transmission within the Elastic Android application. 13 | * Utilizes the Android WorkManager API to schedule and manage these tasks under specified constraints, such as network availability. 14 | * 15 | *

Both tasks are crucial for maintaining the agent's operational status and ensuring data consistency and availability for analytics and monitoring purposes.

16 | * 17 | *

Tasks are scheduled as unique, one-time work requests with a defined initial delay and network connectivity requirement. This approach ensures that tasks are executed in an efficient manner, respecting device constraints and optimizing for battery life.

18 | *

The scheduler also provides the ability to cancel all scheduled tasks, offering control over task execution and resource management.

19 | */ 20 | public class WorkScheduler { 21 | protected static final String FLEET_CHECKIN_WORK_NAME = "fleet_checkin"; 22 | protected static final String ELASTICSEARCH_PUT_WORK_NAME = "elasticsearch-put"; 23 | 24 | /** 25 | * Schedules a one-time fleet check-in work task with a specified delay and under network connectivity constraints. 26 | * This task is crucial for maintaining the agent's communication with the fleet server, allowing it to report its status and receive commands. 27 | * 28 | * @param context The application context, used to access the WorkManager instance. 29 | * @param interval The delay before the task is executed, specified in the units provided by the {@code timeUnit} parameter. 30 | * @param timeUnit The time unit for the {@code interval} parameter, e.g., {@link TimeUnit#MINUTES}. 31 | */ 32 | public static void scheduleFleetCheckinWorker(Context context, long interval, TimeUnit timeUnit, boolean constraintBatteryNotLow) { 33 | AppLog.i("WorkScheduler", "Scheduling fleet check-in worker with interval " + interval + " " + timeUnit.toString()); 34 | Constraints constraints = new Constraints.Builder() 35 | .setRequiredNetworkType(NetworkType.CONNECTED) 36 | .setRequiresBatteryNotLow(constraintBatteryNotLow) 37 | .build(); 38 | 39 | OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(FleetCheckinWorker.class) 40 | .setInitialDelay(interval, timeUnit) 41 | .setConstraints(constraints) 42 | .addTag(FLEET_CHECKIN_WORK_NAME); 43 | 44 | OneTimeWorkRequest workRequest = builder.build(); 45 | // We need to use unique work and not a periodic worker, as the interval is dynamic and likely under the minimum scheduling interval of 15 minutes. 46 | WorkManager.getInstance(context).enqueueUniqueWork(FLEET_CHECKIN_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest); 47 | } 48 | 49 | /** 50 | * Schedules a one-time Elasticsearch document upload work task with a specified delay and under network connectivity constraints. 51 | * This task enables the application to transmit stored data to Elasticsearch, supporting data analysis and monitoring efforts. 52 | * 53 | * @param context The application context, used to access the WorkManager instance. 54 | * @param interval The delay before the task is executed, specified in the units provided by the {@code timeUnit} parameter. 55 | * @param timeUnit The time unit for the {@code interval} parameter, e.g., {@link TimeUnit#MINUTES}. 56 | */ 57 | public static void scheduleElasticsearchWorker(Context context, long interval, TimeUnit timeUnit, boolean constraintBatteryNotLow) { 58 | AppLog.i("WorkScheduler", "Scheduling Elasticsearch put worker with interval " + interval + " " + timeUnit.toString()); 59 | Constraints constraints = new Constraints.Builder() 60 | .setRequiredNetworkType(NetworkType.CONNECTED) 61 | .setRequiresBatteryNotLow(constraintBatteryNotLow) 62 | .build(); 63 | 64 | OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(ElasticWorker.class) 65 | .setInitialDelay(interval, timeUnit) 66 | .setConstraints(constraints) 67 | .addTag(ELASTICSEARCH_PUT_WORK_NAME); 68 | 69 | OneTimeWorkRequest workRequest = builder.build(); 70 | // We need to use unique work and not a periodic worker, as the interval is dynamic and likely under the minimum scheduling interval of 15 minutes. 71 | WorkManager.getInstance(context).enqueueUniqueWork(ELASTICSEARCH_PUT_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest); 72 | } 73 | 74 | /** 75 | * Cancels all work tasks scheduled by the WorkManager. 76 | * This method provides a way to halt all scheduled background tasks, useful in scenarios requiring a cessation of activity, such as unenrollment or when resetting the work schedule. 77 | * 78 | * @param context The application context, used to access the WorkManager instance. 79 | */ 80 | public static void cancelAllWork(Context context) { 81 | AppLog.i("WorkScheduler", "Cancelling all work"); 82 | WorkManager.getInstance(context).cancelAllWork(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bordered_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edgy_notch_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbird07/elastic-agent-android/799a8b79f42009a860d5b5c83e0f8a4897ac6a39/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_enrollment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 21 | 22 | 27 | 28 |