commit
e344b8c5a1
109 changed files with 6359 additions and 0 deletions
@ -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 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
# Default ignored files |
||||
/shelf/ |
||||
/workspace.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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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. |
@ -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/) |
@ -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' |
||||
} |
@ -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" |
||||
} |
@ -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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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" |
||||
} |
||||
] |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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> |
After Width: | Height: | Size: 260 KiB |
@ -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) {} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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) { |
||||