Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/android-pr-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Android PR Build

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read

concurrency:
group: android-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build:
name: Build and test
runs-on: ubuntu-latest

steps:
- name: Check out source
uses: actions/checkout@v5

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5

# No signing secrets on pull requests (and none on fork PRs at all); the release build
# falls back to debug signing. assembleRelease still runs so R8 / resource shrinking break
# the check here rather than after merge.
- name: Build and test
run: |
chmod +x ./gradlew
./gradlew --no-daemon testDebugUnitTest assembleDebug assembleRelease

- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: meshcentral-agent-debug
path: app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/git_toolbox_prj.xml
.vscode/
.DS_Store
/build
/release
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ link, or entering the link manually. After enrollment, the app maintains an
authenticated connection to the server and can:

- Report device, network, storage, and battery information.
- Share the device screen after Android MediaProjection consent.
- Share the device screen, and control it once the bundled accessibility
service is enabled.
- Browse and transfer media and files available to the app.
- Receive server notifications and a limited set of console commands.
- Approve or reject MeshCentral push-based two-factor authentication requests.

Remote desktop is currently **view only**. The app can stream the display, but
it cannot tap, swipe, type, or otherwise control the device. Android displays a
foreground notification while screen sharing is active, and the user can deny
or stop capture at any time.
Remote desktop works two ways. Without extra setup it is **view only** through
Android's screen-capture consent dialog. Once the device user enables the
bundled Accessibility Remote Control service, the agent captures the screen in
the background and injects taps, drags, long presses, scrolling and typing, so
an operator can control the device unattended. Android shows a persistent
notification while a session is active, consent prompts follow the server's
policy and the app's Automatic Consent setting, and the user can deny or stop
sharing at any time.

## Install

Expand Down
12 changes: 12 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ plugins {
id 'com.google.gms.google-services'
}

def meshDevBuild = (project.findProperty("meshDevBuild") ?: new Date().format("yyyyMMddHHmm")).toString()

def releaseKeystorePath = System.getenv('ANDROID_KEYSTORE_PATH')
def releaseKeystorePassword = System.getenv('ANDROID_KEYSTORE_PASSWORD')
def releaseKeyAlias = System.getenv('ANDROID_KEY_ALIAS')
Expand All @@ -28,11 +30,21 @@ android {
versionCode 30
versionName "1.0.23"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
def enterpriseEnforced = (project.findProperty("meshEnterpriseEnforced") ?: "false").toString().toBoolean()
buildConfigField "boolean", "ENTERPRISE_ENFORCED", enterpriseEnforced.toString()
// Optional "All files access" (MANAGE_EXTERNAL_STORAGE) for full file transfer on Android 11+.
// Off by default: Google Play only accepts that permission with an approved use-case
// declaration, so declare it just for enterprise or sideloaded builds. When off, the
// placeholder resolves to a permission the manifest already has, so nothing new is added.
def allFilesAccess = (project.findProperty("meshAllFilesAccess") ?: "false").toString().toBoolean()
buildConfigField "boolean", "ALL_FILES_ACCESS", allFilesAccess.toString()
manifestPlaceholders["allFilesAccessPermission"] = allFilesAccess ? "android.permission.MANAGE_EXTERNAL_STORAGE" : "android.permission.INTERNET"
}

buildTypes {
debug {
debuggable true
versionNameSuffix "-dev.$meshDevBuild"
}
release {
// Enables code shrinking, obfuscation, and optimization for only
Expand Down
52 changes: 51 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,30 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<!-- MANAGE_EXTERNAL_STORAGE only when built with -PmeshAllFilesAccess=true, see app/build.gradle. -->
<uses-permission android:name="${allFilesAccessPermission}" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Wake the display for a remote session; Android 14+ only honours that with TURN_SCREEN_ON. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.TURN_SCREEN_ON" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
<queries>
<!-- Which launcher is in front, for the app drawer fallback. -->
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<!-- Every launchable app, so the software inventory works without QUERY_ALL_PACKAGES. -->
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -60,6 +80,36 @@
</service>
<service
android:name=".ScreenCaptureService"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<service
android:name=".AgentForegroundService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="persistent_remote_management_agent_connection" />
</service>
<service
android:name=".MeshAccessibilityService"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/mesh_accessibility_service" />
</service>
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>
Loading