Browse Source

Release

master
ocsnetworks 5 months ago
commit
e344b8c5a1
  1. 15
      .gitignore
  2. 3
      .idea/.gitignore
  3. 1
      .idea/.name
  4. 6
      .idea/compiler.xml
  5. 23
      .idea/gradle.xml
  6. 25
      .idea/jarRepositories.xml
  7. 9
      .idea/misc.xml
  8. 6
      .idea/vcs.xml
  9. 1
      HEAD
  10. 201
      LICENSE
  11. 11
      README.md
  12. 1
      app/.gitignore
  13. 67
      app/build.gradle
  14. 39
      app/google-services.json
  15. 26
      app/proguard-rules.pro
  16. BIN
      app/release/app-release.aab
  17. BIN
      app/release/app-release.apk
  18. BIN
      app/release/meshcentral-agent.apk
  19. 18
      app/release/output-metadata.json
  20. 24
      app/src/androidTest/java/com/meshcentral/agent/ExampleInstrumentedTest.kt
  21. 59
      app/src/main/AndroidManifest.xml
  22. BIN
      app/src/main/ic_launcher-playstore.png
  23. 85
      app/src/main/java/com/meshcentral/agent/AuthFragment.kt
  24. 627
      app/src/main/java/com/meshcentral/agent/MainActivity.kt
  25. 358
      app/src/main/java/com/meshcentral/agent/MainFragment.kt
  26. 1009
      app/src/main/java/com/meshcentral/agent/MeshAgent.kt
  27. 269
      app/src/main/java/com/meshcentral/agent/MeshFirebaseMessagingService.kt
  28. 746
      app/src/main/java/com/meshcentral/agent/MeshTunnel.kt
  29. 49
      app/src/main/java/com/meshcentral/agent/NotificationUtils.kt
  30. 151
      app/src/main/java/com/meshcentral/agent/ScannerFragment.kt
  31. 522
      app/src/main/java/com/meshcentral/agent/ScreenCaptureService.kt
  32. 29
      app/src/main/java/com/meshcentral/agent/SettingsFragment.kt
  33. 85
      app/src/main/java/com/meshcentral/agent/WebViewFragment.kt
  34. 30
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  35. 5
      app/src/main/res/drawable/ic_baseline_settings_24.xml
  36. 13
      app/src/main/res/drawable/ic_camera.xml
  37. 10
      app/src/main/res/drawable/ic_cloud.xml
  38. 170
      app/src/main/res/drawable/ic_launcher_background.xml
  39. 11
      app/src/main/res/drawable/ic_message.xml
  40. 84
      app/src/main/res/layout-land/fragment_auth.xml
  41. 58
      app/src/main/res/layout-land/main_fragment.xml
  42. 35
      app/src/main/res/layout/activity_main.xml
  43. 19
      app/src/main/res/layout/content_main.xml
  44. 84
      app/src/main/res/layout/fragment_auth.xml
  45. 57
      app/src/main/res/layout/main_fragment.xml
  46. 44
      app/src/main/res/layout/scanner_fragment.xml
  47. 16
      app/src/main/res/layout/webview_fragment.xml
  48. 45
      app/src/main/res/menu/menu_main.xml
  49. 5
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  50. 5
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  51. 5
      app/src/main/res/mipmap-anydpi-v26/ic_user.xml
  52. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  53. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  54. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  55. BIN
      app/src/main/res/mipmap-hdpi/ic_user_foreground.png
  56. BIN
      app/src/main/res/mipmap-hdpi/ic_users_foreground.png
  57. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  58. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  59. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  60. BIN
      app/src/main/res/mipmap-mdpi/ic_user_foreground.png
  61. BIN
      app/src/main/res/mipmap-mdpi/ic_users_foreground.png
  62. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  63. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  64. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  65. BIN
      app/src/main/res/mipmap-xhdpi/ic_user_foreground.png
  66. BIN
      app/src/main/res/mipmap-xhdpi/ic_users_foreground.png
  67. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  68. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  69. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  70. BIN
      app/src/main/res/mipmap-xxhdpi/ic_user_foreground.png
  71. BIN
      app/src/main/res/mipmap-xxhdpi/ic_users_foreground.png
  72. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  73. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  74. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  75. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_user_foreground.png
  76. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_users_foreground.png
  77. 63
      app/src/main/res/navigation/nav_graph.xml
  78. 16
      app/src/main/res/values-night/themes.xml
  79. 12
      app/src/main/res/values/arrays.xml
  80. 13
      app/src/main/res/values/colors.xml
  81. 3
      app/src/main/res/values/dimens.xml
  82. 4
      app/src/main/res/values/ic_launcher_background.xml
  83. 4
      app/src/main/res/values/ic_user_white.xml
  84. 4
      app/src/main/res/values/ic_white.xml
  85. 55
      app/src/main/res/values/strings.xml
  86. 23
      app/src/main/res/values/themes.xml
  87. 15
      app/src/main/res/xml/root_preferences.xml
  88. 17
      app/src/test/java/com/meshcentral/agent/ExampleUnitTest.kt
  89. 27
      build.gradle
  90. 6
      config
  91. 1
      description
  92. 25
      gradle.properties
  93. BIN
      gradle/wrapper/gradle-wrapper.jar
  94. 6
      gradle/wrapper/gradle-wrapper.properties
  95. 172
      gradlew
  96. 84
      gradlew.bat
  97. 15
      hooks/applypatch-msg.sample
  98. 24
      hooks/commit-msg.sample
  99. 114
      hooks/fsmonitor-watchman.sample
  100. 8
      hooks/post-update.sample
  101. Some files were not shown because too many files have changed in this diff Show More

15
.gitignore vendored

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore vendored

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name

@ -0,0 +1 @@ @@ -0,0 +1 @@
MeshCentral Agent

6
.idea/compiler.xml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

23
.idea/gradle.xml

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="disableWrapperSourceDistributionNotification" value="true" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>
</project>

25
.idea/jarRepositories.xml

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

9
.idea/misc.xml

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1
HEAD

@ -0,0 +1 @@ @@ -0,0 +1 @@
ref: refs/heads/master

201
LICENSE

@ -0,0 +1,201 @@ @@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

11
README.md

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
# MeshCentral Agent for Android
This is the MeshCentral Agent for Android. It's a completely different code base from the agent used on Windows, Linux, macOS and FreeBSD. You can pair the agent to the server by scanning a QR code. Once paired, you can connect
to the server and see the device show up, see the battery state, device details and download pictures, audio and video files.
For more information, [visit MeshCentral.com](https://www.meshcentral.com).
## Social Media
[Reddit](https://www.reddit.com/r/MeshCentral/)
[Twitter](https://twitter.com/MeshCentral)
[BlogSpot](https://meshcentral2.blogspot.com/)

1
app/.gitignore vendored

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

67
app/build.gradle

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.gms.google-services'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.meshcentral.agent2"
minSdkVersion 23
targetSdkVersion 30
versionCode 18
versionName "1.0.15"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
//minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
//shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
//implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'com.budiyev.android:code-scanner:2.1.0'
implementation 'com.karumi:dexter:6.2.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.firebase:firebase-messaging:20.1.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
//implementation 'org.webrtc:google-webrtc:1.0.32006'
//testImplementation 'junit:junit:4.13.1'
//androidTestImplementation 'androidx.test.ext:junit:1.1.2'
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

39
app/google-services.json

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "79377772889",
"project_id": "meshcentral-agent",
"storage_bucket": "meshcentral-agent.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:79377772889:android:74f2dcefa37e87b8fb1c48",
"android_client_info": {
"package_name": "com.meshcentral.agent2"
}
},
"oauth_client": [
{
"client_id": "79377772889-r4m765ei5l6ha6jkjscaulbc2jovkmf1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDg6qs9s7zg1jlccoVMWkeeJ7nbddA5Kc0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "79377772889-r4m765ei5l6ha6jkjscaulbc2jovkmf1.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

26
app/proguard-rules.pro vendored

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

BIN
app/release/app-release.aab

Binary file not shown.

BIN
app/release/app-release.apk

Binary file not shown.

BIN
app/release/meshcentral-agent.apk

Binary file not shown.

18
app/release/output-metadata.json

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
{
"version": 2,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.meshcentral.agent2",
"variantName": "processReleaseResources",
"elements": [
{
"type": "SINGLE",
"filters": [],
"versionCode": 18,
"versionName": "1.0.15",
"outputFile": "app-release.apk"
}
]
}

24
app/src/androidTest/java/com/meshcentral/agent/ExampleInstrumentedTest.kt

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
package com.meshcentral.agent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.meshcentral.agent", appContext.packageName)
}
}

59
app/src/main/AndroidManifest.xml

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.meshcentral.agent">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MeshCentralAgent">
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"></activity>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.MeshCentralAgent.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<data android:scheme="mc" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<service
android:name=".MeshFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service
android:name=".ScreenCaptureService"
android:foregroundServiceType="mediaProjection" />
</application>
</manifest>

BIN
app/src/main/ic_launcher-playstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

85
app/src/main/java/com/meshcentral/agent/AuthFragment.kt

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
package com.meshcentral.agent
import android.os.Bundle
import android.os.CountDownTimer
import android.util.Base64
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.navigation.fragment.findNavController
import java.lang.Exception
class AuthFragment : Fragment() {
var countDownTimer : CountDownTimer? = null
override fun onCreate(savedInstanceState: Bundle?) {
println("onCreate-auth");
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
println("onCreateView-auth");
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_auth, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
println("onViewCreated-auth");
super.onViewCreated(view, savedInstanceState)
authFragment = this
visibleScreen = 4;
// Set authentication code
var t:TextView = view.findViewById<Button>(R.id.authTopText2) as TextView
t.text = "000000"
if (g_auth_url != null) {
var authCode: String? = g_auth_url?.getQueryParameter("code")
if (authCode != null) {
t.text = String(Base64.decode(authCode, Base64.DEFAULT), charset("UTF-8"))
}
}
// Set authentication progress bar
var p:ProgressBar = view.findViewById<Button>(R.id.authProgressBar) as ProgressBar
p.progress = 100
countDownTimer = object : CountDownTimer(60000, 600) {
override fun onTick(millisUntilFinished: Long) {
var p:ProgressBar = view.findViewById<Button>(R.id.authProgressBar) as ProgressBar
if (p.progress > 0) { p.progress = p.progress - 1 }
}
override fun onFinish() {
countDownTimer = null
exit()
}
}.start()
view.findViewById<Button>(R.id.authAcceptButton).setOnClickListener {
if ((meshAgent != null) && (g_auth_url != null)) { meshAgent?.send2faAuth(g_auth_url!!, true) }
g_auth_url = null
exit()
}
view.findViewById<Button>(R.id.authRejectButton).setOnClickListener {
if ((meshAgent != null) && (g_auth_url != null)) { meshAgent?.send2faAuth(g_auth_url!!, false) }
g_auth_url = null
exit()
}
}
fun exit() {
if (countDownTimer != null) {
countDownTimer?.cancel()
countDownTimer = null
}
try {
findNavController().navigate(R.id.action_authFragment_to_FirstFragment)
} catch (ex: Exception) {}
}
}

627
app/src/main/java/com/meshcentral/agent/MainActivity.kt

@ -0,0 +1,627 @@ @@ -0,0 +1,627 @@
package com.meshcentral.agent
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.projection.MediaProjectionManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.text.InputType
import android.util.Base64
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.firebase.iid.FirebaseInstanceId
import okio.ByteString.Companion.toByteString
import org.spongycastle.asn1.x500.X500Name
import org.spongycastle.cert.X509v3CertificateBuilder
import org.spongycastle.cert.jcajce.JcaX509CertificateConverter
import org.spongycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.spongycastle.jce.provider.BouncyCastleProvider
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.ByteArrayInputStream
import java.lang.Exception
import java.math.BigInteger
import java.security.*
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.absoluteValue
// You can hardcode a server connection string into this application by setting this string.
// Make sure to replace all $ with \$ if your link string contains the $ character
// Once set, the resulting APK will be hard coded and users can't unset this value.
val hardCodedServerLink : String? = null
//val hardCodedServerLink : String? = "mc://central.mesh.meshcentral.com,2ZNi1e2Lrqi\$nnQ7NLJCJWNwxGD9ZstiNzxs\$LIE1tcHQD45bPDvbcKzpC9zUTX9,7b4b43cdad850135f36ab31124b52e47c167fba055ce800267a4dc89fe0e581c"
// User interface values
var g_mainActivity : MainActivity? = null
var mainFragment : MainFragment? = null
var scannerFragment : ScannerFragment? = null
var webFragment : WebViewFragment? = null
var authFragment : AuthFragment? = null
var settingsFragment: SettingsFragment? = null
var visibleScreen : Int = 1
// Server connection values
var serverLink : String? = null
var meshAgent : MeshAgent? = null
var agentCertificate : X509Certificate? = null
var agentCertificateKey : PrivateKey? = null
var pageUrl : String? = null
var cameraPresent : Boolean = false
var pendingActivities : ArrayList<PendingActivityData> = ArrayList<PendingActivityData>()
var pushMessagingToken : String? = null
var g_autoConnect : Boolean = true
var g_userDisconnect : Boolean = false // Indicate user initiated disconnection
var g_retryTimer: CountDownTimer? = null
// Remote desktop values
var g_ScreenCaptureService : ScreenCaptureService? = null
var g_desktop_imageType : Int = 1
var g_desktop_compressionLevel : Int = 40
var g_desktop_scalingLevel : Int = 1024
var g_desktop_frameRateLimiter : Int = 100
// Two-factor authentication values
var g_auth_url : Uri? = null
class MainActivity : AppCompatActivity() {
var alert : AlertDialog? = null
lateinit var notificationChannel: NotificationChannel
lateinit var notificationManager: NotificationManager
lateinit var builder: Notification.Builder
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.insertProviderAt(BouncyCastleProvider(), 1)
}
override fun onCreate(savedInstanceState: Bundle?) {
g_mainActivity = this
val sharedPreferences = getSharedPreferences("meshagent", Context.MODE_PRIVATE)
if (hardCodedServerLink != null) {
// Use the hard coded server link
serverLink = hardCodedServerLink
} else {
// Use the configurable server link
serverLink = sharedPreferences?.getString("qrmsh", null)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//var toolbar = g_mainActivity?.findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(findViewById(R.id.toolbar))
// Setup notification manager
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Register to get battery events
val intentFilter = IntentFilter()
intentFilter.addAction(Intent.ACTION_POWER_CONNECTED)
intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED)
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryInfoReceiver, intentFilter)
// Check if this device has a camera
cameraPresent = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
// Setup push notifications
//println("Asking for token")
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener(this
) { instanceIdResult ->
pushMessagingToken = instanceIdResult.token
//println("messagingToken: $pushMessagingToken")
}
// See if we there open by a notification with a URL
var intentUrl : String? = intent.getStringExtra("url")
//println("Main Activity Create URL: $intentUrl")
if (intentUrl != null) {
intent.removeExtra("url")
if (intentUrl.toLowerCase().startsWith("2fa://")) {
// if there is no server link, ignore this
if (serverLink != null) {
// This activity was created by a 2FA message
g_auth_url = Uri.parse(intentUrl)
// If not connected, connect to the server now.
if (meshAgent == null) {
toggleAgentConnection(false);
} else {
// Switch to 2FA auth screen
if (mainFragment != null) {
mainFragment?.moveToAuthPage()
}
}
}
} else if (intentUrl.toLowerCase().startsWith("http://") || intentUrl.toLowerCase().startsWith("https://")) {
// Open an HTTP or HTTPS URL.
var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(intentUrl));
startActivity(getintent);
}
}
// Activate the settings
settingsChanged()
if (g_autoConnect && !g_userDisconnect && (meshAgent == null)) {
toggleAgentConnection(false)
}
}
private fun sendConsoleMessage(msg: String) {
if (meshAgent != null) { meshAgent?.sendConsoleResponse(msg, null) }
}
private val batteryInfoReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (meshAgent != null) { meshAgent?.batteryStateChanged(intent) }
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
var item1 = menu.findItem(R.id.action_setup_server);
item1.isVisible = (visibleScreen == 1) && (hardCodedServerLink == null);
item1.isEnabled = cameraPresent;
var item2 = menu.findItem(R.id.action_clear_server);
item2.isVisible = (visibleScreen == 1) && (serverLink != null) && (hardCodedServerLink == null);
var item3 = menu.findItem(R.id.action_close);
item3.isVisible = (visibleScreen != 1);
var item4 = menu.findItem(R.id.action_sharescreen);
item4.isVisible = false // (g_ScreenCaptureService == null) && (meshAgent != null) && (meshAgent!!.state == 3)
var item5 = menu.findItem(R.id.action_stopscreensharing);
item5.isVisible = (g_ScreenCaptureService != null)
var item6 = menu.findItem(R.id.action_manual_setup_server);
item6.isVisible = (visibleScreen == 1) && (serverLink == null) && (hardCodedServerLink == null)
var item7 = menu.findItem(R.id.action_testAuth);
item7.isVisible = false //(visibleScreen == 1) && (serverLink != null);
var item8 = menu.findItem(R.id.action_settings);
item8.isVisible = (visibleScreen == 1)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
if ((item.itemId == R.id.action_setup_server) && (hardCodedServerLink == null)) {
// Move to QR code reader if a camera is present
if ((mainFragment != null) && cameraPresent) mainFragment?.moveToScanner()
}
if ((item.itemId == R.id.action_clear_server) && (hardCodedServerLink == null)) {
// Remove the server
confirmServerClear()
}
if (item.itemId == R.id.action_close) {
// Close
returnToMainScreen()
}
if (item.itemId == R.id.action_sharescreen) {
// Start projection
startProjection()
}
if (item.itemId == R.id.action_stopscreensharing) {
// Stop projection
stopProjection()
}
if ((item.itemId == R.id.action_manual_setup_server) && (hardCodedServerLink == null)) {
// Manually setup the server pairing
promptForServerLink()
}
if (item.itemId == R.id.action_testAuth) {
// Move to authentication screen
if (mainFragment != null) mainFragment?.moveToAuthPage()
}
if (item.itemId == R.id.action_settings) {
// Move to settings screen
if (mainFragment != null) mainFragment?.moveToSettingsPage()
}
return when(item.itemId) {
R.id.action_setup_server -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
g_mainActivity = null
if (alert != null) {
alert?.dismiss()
alert = null
}
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
println("onActivityResult, requestCode: $requestCode, resultCode: $resultCode, data: ${data.toString()}")
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MainActivity.Companion.REQUEST_CODE) {
if (resultCode == RESULT_OK) {
startService(com.meshcentral.agent.ScreenCaptureService.getStartIntent(this, resultCode, data))
return
}
}
var pad : PendingActivityData? = null
for (b in pendingActivities) { if (b.id == requestCode) { pad = b } }
if (pad != null) {
if (resultCode == Activity.RESULT_OK) {
println("Approved: ${pad.url}, ${pad.where}, ${pad.args}")
pad.tunnel.deleteFileEx(pad)
} else {
println("Denied: ${pad.url}, ${pad.where}, ${pad.args}")
pad.tunnel.deleteFileEx(pad)
}
pendingActivities.remove(pad)
}
}
fun setMeshServerLink(x: String?) {
if ((serverLink == x) || (hardCodedServerLink != null)) return
if (meshAgent != null) { // Stop the agent
meshAgent?.Stop()
meshAgent = null
}
serverLink = x
val sharedPreferences = getSharedPreferences("meshagent", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("qrmsh", x).apply()
mainFragment?.refreshInfo()
g_userDisconnect = false
if (g_autoConnect) { toggleAgentConnection(false) }
}
// Open a URL in the web view fragment
fun openUrl(xpageUrl: String) : Boolean {
if (visibleScreen == 2) return false
pageUrl = xpageUrl;
if (visibleScreen == 1) {
if (mainFragment != null) mainFragment?.moveToWebPage(xpageUrl)
} else {
this.runOnUiThread {
if (webFragment != null) webFragment?.navigate(xpageUrl)
}
}
return true
}
fun returnToMainScreen() {
this.runOnUiThread {
if (visibleScreen == 2) {
if (scannerFragment != null) scannerFragment?.exit()
} else if (visibleScreen == 3) {
if (webFragment != null) webFragment?.exit()
} else if (visibleScreen == 4) {
if (authFragment != null) authFragment?.exit()
} else if (visibleScreen == 5) {
if (settingsFragment != null) settingsFragment?.exit()
}
}
}
fun agentStateChanged() {
this.runOnUiThread {
if ((meshAgent != null) && (meshAgent?.state == 0)) {
meshAgent = null
}
if (((meshAgent != null) && (meshAgent?.state == 2)) || (g_userDisconnect) || (!g_autoConnect)) stopRetryTimer()
else if ((meshAgent == null) && (!g_userDisconnect) && (g_autoConnect) && (g_retryTimer == null)) startRetryTimer()
mainFragment?.refreshInfo()
}
}
fun refreshInfo() {
this.runOnUiThread {
mainFragment?.refreshInfo()
}
}
fun confirmServerClear() {
if (hardCodedServerLink != null) return
if (alert != null) {
alert?.dismiss()
alert = null
}
val builder = AlertDialog.Builder(this)
builder.setTitle("MeshCentral Server")
builder.setMessage("Clear server setup?")
builder.setPositiveButton(android.R.string.ok) { _, _ ->
this.setMeshServerLink(null)
}
builder.setNeutralButton(android.R.string.cancel) { _, _ -> }
alert = builder.show()
}
fun showAlertMessage(title: String, msg: String) {
if (alert != null) {
alert?.dismiss()
alert = null
}
this.runOnUiThread {
val builder = AlertDialog.Builder(this)
builder.setTitle(title)
builder.setMessage(msg)
builder.setPositiveButton(android.R.string.ok) { _, _ -> {} }
alert = builder.show()
}
}
fun showToastMessage(msg: String) {
this.runOnUiThread {
var toast = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG)
toast?.setGravity(Gravity.CENTER, 0, 300)
toast?.show()
}
}
fun getServerHost() : String? {
if (serverLink == null) return null
var x : List<String> = serverLink!!.split(',')
var serverHost = x[0]
return serverHost.substring(5)
}
fun getServerHash() : String? {
if (serverLink == null) return null
var x : List<String> = serverLink!!.split(',')
return x[1]
}
fun getDevGroup() : String? {
if (serverLink == null) return null
var x : List<String> = serverLink!!.split(',')
return x[2]
}
fun isAgentDisconnected() : Boolean {
return (meshAgent == null)
}
fun toggleAgentConnection(userInitiated : Boolean) {
//println("toggleAgentConnection")
if ((meshAgent == null) && (serverLink != null)) {
// Create and connect the agent
if (agentCertificate == null) {
val sharedPreferences = getSharedPreferences("meshagent", Context.MODE_PRIVATE)
var certb64 : String? = sharedPreferences?.getString("agentCert", null)
var keyb64 : String? = sharedPreferences?.getString("agentKey", null)
if ((certb64 == null) || (keyb64 == null)) {
//println("Generating new certificates...")
// Generate an RSA key pair
val keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048, SecureRandom())
val keypair = keyGen.generateKeyPair()
// Generate Serial Number
var serial : BigInteger = BigInteger("12345678");
try { serial = BigInteger.valueOf(Random().nextInt().toLong().absoluteValue) } catch (ex: Exception) {}
// Create self signed certificate
val builder: X509v3CertificateBuilder = JcaX509v3CertificateBuilder(
X500Name("CN=android.agent.meshcentral.com"), // issuer authority
serial, // serial number of certificate
Date(System.currentTimeMillis() - 86400000L * 365), // start of validity
Date(253402300799000L), // end of certificate validity
X500Name("CN=android.agent.meshcentral.com"), // subject name of certificate
keypair.public) // public key of certificate
agentCertificate = JcaX509CertificateConverter().setProvider("SC").getCertificate(builder
.build(JcaContentSignerBuilder("SHA256withRSA").build(keypair.private))) // Private key of signing authority , here it is self signed
agentCertificateKey = keypair.private
// Save the certificate and key
sharedPreferences?.edit()?.putString("agentCert", Base64.encodeToString(agentCertificate?.encoded, Base64.DEFAULT))?.apply()
sharedPreferences?.edit()?.putString("agentKey", Base64.encodeToString(agentCertificateKey?.encoded, Base64.DEFAULT))?.apply()
} else {
//println("Loading certificates...")
agentCertificate = CertificateFactory.getInstance("X509").generateCertificate(
ByteArrayInputStream(Base64.decode(certb64 as String, Base64.DEFAULT))
) as X509Certificate
val keySpec = PKCS8EncodedKeySpec(Base64.decode(keyb64 as String, Base64.DEFAULT))
agentCertificateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec)
}
//println("Cert: ${agentCertificate.toString()}")
//println("XKey: ${agentCertificateKey.toString()}")
}
if (!userInitiated) {
meshAgent = MeshAgent(this, getServerHost()!!, getServerHash()!!, getDevGroup()!!)
meshAgent?.Start()
} else {
if (g_autoConnect) {
if (g_userDisconnect) {
// We are not trying to connect, switch to connecting
g_userDisconnect = false
meshAgent =
MeshAgent(this, getServerHost()!!, getServerHash()!!, getDevGroup()!!)
meshAgent?.Start()
} else {
// We are trying to connect, switch to not trying
g_userDisconnect = true
}
} else {
// We are not in auto connect mode, try to connect
g_userDisconnect = true
meshAgent =
MeshAgent(this, getServerHost()!!, getServerHash()!!, getDevGroup()!!)
meshAgent?.Start()
}
}
} else if (meshAgent != null) {
// Stop the agent
if (userInitiated) { g_userDisconnect = true }
stopProjection()
meshAgent?.Stop()
meshAgent = null
}
mainFragment?.refreshInfo()
}
fun showNotification(title: String?, body: String?, url: String?) {
//println("showNotification: $title, $body")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
if (url != null) { intent.putExtra("url", url!!); }
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationChannel = NotificationChannel(getString(R.string.default_notification_channel_id), "MeshCentral Agent Channel", NotificationManager.IMPORTANCE_DEFAULT)
notificationChannel.lightColor = Color.BLUE
notificationChannel.enableVibration(true)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(this, getString(com.meshcentral.agent.R.string.default_notification_channel_id))
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
//.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
.setContentIntent(pendingIntent)
}
// Add notification
notificationManager.notify(0, builder.build())
}
fun isMshStringValid(x: String):Boolean {
if (x.startsWith("mc://") == false) return false
var xs = x.split(',')
if (xs.count() < 3) return false
if (xs[0].length < 8) return false
if (xs[1].length < 3) return false
if (xs[2].length < 3) return false
if (xs[0].indexOf('.') == -1) return false
return true
}
// Show alert asking for server pairing link
fun promptForServerLink() {
if (hardCodedServerLink != null) return
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("Server Pairing Link")
// Set up the input
val input = EditText(this)
input.inputType = InputType.TYPE_CLASS_TEXT
builder.setView(input)
// Set up the buttons
builder.setPositiveButton(android.R.string.ok) { _, _ ->
var link = input.text.toString()
println("LINK: $link")
if (isMshStringValid(link)) {
setMeshServerLink(link)
} else {
indicateInvalidLink()
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, which -> dialog.cancel() }
builder.show()
}
// Show alert that server pairing link is invalid
fun indicateInvalidLink() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("Invalid Server Pairing Link")
// Set up the buttons
builder.setPositiveButton(android.R.string.ok) { dialog, which -> dialog.cancel() }
builder.show()
}
// Start screen sharing
fun startProjection() {
if ((g_ScreenCaptureService != null) || (meshAgent == null) || (meshAgent!!.state != 3)) return
if (meshAgent != null) {
meshAgent!!.sendConsoleResponse("Asking for display consent", sessionid = null)
}
val mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
startActivityForResult(mProjectionManager.createScreenCaptureIntent(), MainActivity.Companion.REQUEST_CODE)
}
// Stop screen sharing
fun stopProjection() {
if (g_ScreenCaptureService == null) return
startService(com.meshcentral.agent.ScreenCaptureService.getStopIntent(this))
}
fun settingsChanged() {
this.runOnUiThread {
val pm: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
g_autoConnect = pm.getBoolean("pref_autoconnect", false)
g_userDisconnect = false
if (g_autoConnect == false) {
if (g_retryTimer != null) {
stopRetryTimer()
mainFragment?.refreshInfo()
}
} else {
if ((meshAgent == null) && (!g_userDisconnect) && (g_retryTimer == null)) {
toggleAgentConnection(false)
}
}
}
}
// Start the connection retry timer, try to connect the agent every 10 seconds
private fun startRetryTimer() {
this.runOnUiThread {
if (g_retryTimer == null) {
g_retryTimer = object : CountDownTimer(120000000, 10000) {
override fun onTick(millisUntilFinished: Long) {
println("onTick!!!")
if ((meshAgent == null) && (!g_userDisconnect)) {
toggleAgentConnection(false)
}
}
override fun onFinish() {
println("onFinish!!!")
stopRetryTimer()
startRetryTimer()
}
}
g_retryTimer?.start()
}
}
}
// Stop the connection retry timer
private fun stopRetryTimer() {
this.runOnUiThread {
if (g_retryTimer != null) {
g_retryTimer?.cancel()
g_retryTimer = null
}
}
}
companion object {
private const val REQUEST_CODE = 100
}
}

358
app/src/main/java/com/meshcentral/agent/MainFragment.kt

@ -0,0 +1,358 @@ @@ -0,0 +1,358 @@
package com.meshcentral.agent
import android.Manifest
import android.R.attr.*
import android.app.AlertDialog
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class MainFragment : Fragment(), MultiplePermissionsListener {
var alert : AlertDialog? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainFragment = this
visibleScreen = 1;
refreshInfo()
view.findViewById<Button>(R.id.agentActionButton).setOnClickListener {
var serverLink = serverLink;
if (serverLink == null) {
// Setup the server
if (cameraPresent) {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
} else {
g_mainActivity!!.promptForServerLink()
}
} else {
if ((activity as MainActivity).isAgentDisconnected() == false) {
(activity as MainActivity).toggleAgentConnection(true)
} else {
// Perform action on the agent
Dexter.withContext(context)
.withPermissions(
//Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.withListener(this)
.check()
}
}
}
// Check if the app was called using a URL link
if ((activity != null) && ((activity as MainActivity).intent != null) && ((activity as MainActivity).intent.data != null)) {
var data: Uri? = (activity as MainActivity).intent.data;
if (data != null && data.isHierarchical()) {
var uri: String? = (activity as MainActivity).intent.dataString;
if ((uri != null) && (isMshStringValid(uri))) {
confirmServerSetup(uri)
}
}
}
}
fun isMshStringValid(x: String):Boolean {
if (x.startsWith("mc://") == false) return false
var xs = x.split(',')
if (xs.count() < 3) return false
if (xs[0].length < 8) return false
if (xs[1].length < 3) return false
if (xs[2].length < 3) return false
if (xs[0].indexOf('.') == -1) return false
return true
}
fun moveToScanner() {
println("moveToScanner $visibleScreen")
if (visibleScreen == 1) { findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) }
}
fun moveToWebPage(pageUrl: String) {
println("moveToWebPage $visibleScreen")
if (visibleScreen == 1) { findNavController().navigate(R.id.action_FirstFragment_to_webViewFragment) }
}
fun moveToAuthPage() {
println("moveToAuthPage $visibleScreen")
if (visibleScreen == 1) { findNavController().navigate(R.id.action_FirstFragment_to_authFragment) }
}
fun moveToSettingsPage() {
println("moveToSettingsPage $visibleScreen")
if (visibleScreen == 1) { findNavController().navigate(R.id.action_FirstFragment_to_settingsFragment) }
}
private fun getStringEx(resId: Int) : String {
try { return getString(resId); } catch (ex: Exception) {}
return "";
}
fun refreshInfo() {
var showServerTitle : String? = null
var showServerLogo : Int = 0 // 0 = Default, 1 = User, 2 = Users, 3 = Custom
view?.findViewById<TextView>(R.id.serverNameTextView)?.text = getServerHost(serverLink)
if (serverLink == null) {
// Server not setup
view?.findViewById<ImageView>(R.id.mainImageView)?.alpha = 0.4F
view?.findViewById<TextView>(R.id.agentStatusTextview)?.text = getStringEx(R.string.no_server_setup)
view?.findViewById<TextView>(R.id.agentActionButton)?.text = getStringEx(R.string.setup_server)
//view?.findViewById<TextView>(R.id.agentActionButton)?.isEnabled = cameraPresent
if (visibleScreen == 4) {
authFragment?.exit()
}
} else {
// Server is setup, display state of the agent
var state: Int = 0;
if (meshAgent != null) {
state = meshAgent!!.state;
}
view?.findViewById<TextView>(R.id.agentActionButton)?.isEnabled = true
if ((state == 0) || (state == null)) {
if (g_retryTimer != null) {
// Trying to connect
view?.findViewById<ImageView>(R.id.mainImageView)?.alpha = 0.5F
view?.findViewById<TextView>(R.id.agentStatusTextview)?.text =
getStringEx(R.string.connecting)
view?.findViewById<TextView>(R.id.agentActionButton)?.text =
getStringEx(R.string.disconnect)
} else {
// Disconnected
view?.findViewById<ImageView>(R.id.mainImageView)?.alpha = 0.5F
view?.findViewById<TextView>(R.id.agentStatusTextview)?.text =
getStringEx(R.string.disconnected)
view?.findViewById<TextView>(R.id.agentActionButton)?.text =
getStringEx(R.string.connect)
}
if (visibleScreen == 4) {
authFragment?.exit()
}
} else if (state == 1) {
<