Merge pull request #138 from amnezia-vpn/feature/android_config_export_import
Export/import of configuration files on Android
This commit is contained in:
commit
7345f464a5
15 changed files with 568 additions and 147 deletions
|
@ -1,5 +1,10 @@
|
|||
<?xml version="1.0"?>
|
||||
<manifest package="org.amnezia.vpn" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="-- %%INSERT_VERSION_NAME%% --" android:versionCode="-- %%INSERT_VERSION_CODE%% --" android:installLocation="auto">
|
||||
<manifest
|
||||
package="org.amnezia.vpn"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionName="-- %%INSERT_VERSION_NAME%% --"
|
||||
android:versionCode="-- %%INSERT_VERSION_CODE%% --"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
@ -18,12 +23,72 @@
|
|||
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
|
||||
<application android:name=".qt.AmneziaApp" android:hardwareAccelerated="true" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name=".qt.VPNActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleInstance" android:taskAffinity="" android:theme="@style/splashScreenTheme">
|
||||
<application
|
||||
android:name=".qt.AmneziaApp"
|
||||
android:hardwareAccelerated="true"
|
||||
android:label="-- %%INSERT_APP_NAME%% --"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@drawable/icon">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
|
||||
android:name=".qt.VPNActivity"
|
||||
android:label="-- %%INSERT_APP_NAME%% --"
|
||||
android:screenOrientation="unspecified"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/splashScreenTheme">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType= "*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType= "*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="AmneziaVPN">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:mimeType= "*/*"/>
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/>
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Application arguments -->
|
||||
<!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ -->
|
||||
<!-- Application arguments -->
|
||||
|
@ -79,10 +144,17 @@
|
|||
<!-- extract android style -->
|
||||
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
||||
</activity>
|
||||
<service android:name=".VPNService" android:permission="android.permission.BIND_VPN_SERVICE" android:process=":QtOnlyProcess">
|
||||
|
||||
<service
|
||||
android:name=".VPNService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||
|
@ -95,10 +167,24 @@
|
|||
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper" android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
|
||||
<service
|
||||
android:name=".qt.VPNPermissionHelper"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="true">
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
|
||||
</service>
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.amnezia.vpn.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider"/>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
4
client/android/res/xml/fileprovider.xml
Normal file
4
client/android/res/xml/fileprovider.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="/" />
|
||||
</paths>
|
5
client/android/src/org/amnezia/vpn/IPCContract.kt
Normal file
5
client/android/src/org/amnezia/vpn/IPCContract.kt
Normal file
|
@ -0,0 +1,5 @@
|
|||
package org.amnezia.vpn
|
||||
|
||||
const val IMPORT_COMMAND_CODE = 1
|
||||
const val IMPORT_ACTION_CODE = "import_action"
|
||||
const val IMPORT_CONFIG_KEY = "CONFIG_DATA_KEY"
|
|
@ -15,6 +15,8 @@ import android.os.*
|
|||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.FileProvider
|
||||
import com.wireguard.android.util.SharedLibraryLoader
|
||||
import com.wireguard.config.*
|
||||
import com.wireguard.crypto.Key
|
||||
|
@ -151,6 +153,31 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
|||
private var flags = 0
|
||||
private var startId = 0
|
||||
|
||||
private lateinit var mMessenger: Messenger
|
||||
|
||||
internal class ExternalConfigImportHandler(
|
||||
context: Context,
|
||||
private val serviceBinder: VPNServiceBinder,
|
||||
private val applicationContext: Context = context.applicationContext
|
||||
) : Handler() {
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
IMPORT_COMMAND_CODE -> {
|
||||
val data = msg.data.getString(IMPORT_CONFIG_KEY)
|
||||
|
||||
if (data != null) {
|
||||
serviceBinder.importConfig(data)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
super.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
if (mAlreadyInitialised) {
|
||||
return
|
||||
|
@ -188,6 +215,14 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
|||
*/
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
Log.v(tag, "Aman: onBind....................")
|
||||
|
||||
if (intent.action != null && intent.action == IMPORT_ACTION_CODE) {
|
||||
Log.v(tag, "Service bind for import of config")
|
||||
mMessenger = Messenger(ExternalConfigImportHandler(this, mBinder))
|
||||
return mMessenger.binder
|
||||
}
|
||||
|
||||
Log.v(tag, "Regular service bind")
|
||||
when (mProtocol) {
|
||||
"shadowsocks" -> {
|
||||
when (intent.action) {
|
||||
|
@ -840,4 +875,44 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
|||
override fun close() = Os.close(fd)
|
||||
}
|
||||
|
||||
fun saveAsFile(configContent: String?, suggestedFileName: String): String {
|
||||
val rootDirPath = cacheDir.absolutePath
|
||||
val rootDir = File(rootDirPath)
|
||||
|
||||
if (!rootDir.exists()) {
|
||||
rootDir.mkdirs()
|
||||
}
|
||||
|
||||
val fileName = if (!TextUtils.isEmpty(suggestedFileName)) suggestedFileName else "amnezia.cfg"
|
||||
|
||||
val file = File(rootDir, fileName)
|
||||
|
||||
try {
|
||||
file.bufferedWriter().use { out -> out.write(configContent) }
|
||||
return file.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
fun shareFile(attachmentFile: String?) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/*"
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val file = File(attachmentFile)
|
||||
val uri = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
val createChooser = Intent.createChooser(intent, "Config sharing")
|
||||
createChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(createChooser)
|
||||
} catch (e: Exception) {
|
||||
Log.i(tag, e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
private val tag = "VPNServiceBinder"
|
||||
private var mListener: IBinder? = null
|
||||
private var mResumeConfig: JSONObject? = null
|
||||
private var mImportedConfig: String? = null
|
||||
|
||||
/**
|
||||
* The codes this Binder does accept in [onTransact]
|
||||
|
@ -31,6 +32,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
const val resumeActivate = 7
|
||||
const val setNotificationText = 8
|
||||
const val setFallBackNotification = 9
|
||||
const val shareConfig = 10
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,6 +97,16 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
obj.put("connected", mService.isUp)
|
||||
obj.put("time", mService.connectionTime)
|
||||
dispatchEvent(EVENTS.init, obj.toString())
|
||||
|
||||
////
|
||||
if (mImportedConfig != null) {
|
||||
Log.i(tag, "register: config not null")
|
||||
dispatchEvent(EVENTS.configImport, mImportedConfig)
|
||||
mImportedConfig = null
|
||||
} else {
|
||||
Log.i(tag, "register: config is null")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -108,18 +120,36 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
dispatchEvent(EVENTS.backendLogs, Log.getContent())
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.requestCleanupLog -> {
|
||||
Log.clearFile()
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.setNotificationText -> {
|
||||
NotificationUtil.update(data)
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.setFallBackNotification -> {
|
||||
NotificationUtil.saveFallBackMessage(data, mService)
|
||||
return true
|
||||
}
|
||||
|
||||
ACTIONS.shareConfig -> {
|
||||
val byteArray = data.createByteArray()
|
||||
val json = byteArray?.let { String(it) }
|
||||
val config = JSONObject(json)
|
||||
val configContent = config.getString("data")
|
||||
val suggestedName = config.getString("suggestedName")
|
||||
|
||||
val filePath = mService.saveAsFile(configContent, suggestedName)
|
||||
Log.i(tag, "save file: $filePath")
|
||||
|
||||
mService.shareFile(filePath)
|
||||
return true
|
||||
}
|
||||
|
||||
IBinder.LAST_CALL_TRANSACTION -> {
|
||||
Log.e(tag, "The OS Requested to shut down the VPN")
|
||||
this.mService.turnOff()
|
||||
|
@ -132,6 +162,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -166,5 +197,23 @@ class VPNServiceBinder(service: VPNService) : Binder() {
|
|||
const val statisticUpdate = 3
|
||||
const val backendLogs = 4
|
||||
const val activationError = 5
|
||||
const val configImport = 6
|
||||
}
|
||||
|
||||
fun importConfig(config: String) {
|
||||
val obj = JSONObject()
|
||||
obj.put("config", config)
|
||||
|
||||
val resultString = obj.toString()
|
||||
|
||||
Log.i(tag, "Transact import config request")
|
||||
|
||||
if (mListener != null) {
|
||||
Log.i(tag, "binder alive")
|
||||
dispatchEvent(EVENTS.configImport, resultString)
|
||||
} else {
|
||||
Log.i(tag, "binder NOT alive")
|
||||
mImportedConfig = resultString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.amnezia.vpn.qt;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
public class VPNActivity extends org.qtproject.qt5.android.bindings.QtActivity {
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
// TODO finalize
|
||||
// https://github.com/mozilla-mobile/mozilla-vpn-client/blob/6acff5dd9f072380a04c3fa12e9f3c98dbdd7a26/src/platforms/android/androidvpnactivity.h
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// super.onBackPressed();
|
||||
try {
|
||||
if (!handleBackButton()) {
|
||||
// Move the activity into paused state if back button was pressed
|
||||
moveTaskToBack(true);
|
||||
// finish();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if MVPN has handled the back button
|
||||
native boolean handleBackButton();
|
||||
}
|
196
client/android/src/org/amnezia/vpn/qt/VPNActivity.kt
Normal file
196
client/android/src/org/amnezia/vpn/qt/VPNActivity.kt
Normal file
|
@ -0,0 +1,196 @@
|
|||
package org.amnezia.vpn.qt;
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.amnezia.vpn.VPNService
|
||||
import org.amnezia.vpn.VPNServiceBinder
|
||||
import org.amnezia.vpn.IMPORT_COMMAND_CODE
|
||||
import org.amnezia.vpn.IMPORT_ACTION_CODE
|
||||
import org.amnezia.vpn.IMPORT_CONFIG_KEY
|
||||
import org.qtproject.qt5.android.bindings.QtActivity
|
||||
import java.io.*
|
||||
|
||||
class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() {
|
||||
|
||||
private var configString: String? = null
|
||||
private var vpnServiceBinder: Messenger? = null
|
||||
private var isBound = false
|
||||
|
||||
private val TAG = "VPNActivity"
|
||||
private val STORAGE_PERMISSION_CODE = 42
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val newIntent = intent
|
||||
val newIntentAction = newIntent.action
|
||||
|
||||
if (newIntent != null && newIntentAction != null) {
|
||||
configString = processIntent(newIntent, newIntentAction)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onNewIntent(newIntent: Intent) {
|
||||
intent = newIntent
|
||||
|
||||
val newIntentAction = newIntent.action
|
||||
|
||||
if (newIntent != null && newIntentAction != null && newIntentAction != Intent.ACTION_MAIN) {
|
||||
if (isReadStorageAllowed()) {
|
||||
configString = processIntent(newIntent, newIntentAction)
|
||||
} else {
|
||||
requestStoragePermission()
|
||||
}
|
||||
}
|
||||
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (configString != null && !isBound) {
|
||||
bindVpnService()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (vpnServiceBinder != null && isBound) {
|
||||
unbindService(connection)
|
||||
isBound = false
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun isReadStorageAllowed(): Boolean {
|
||||
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
return permissionStatus == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun requestStoragePermission() {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
|
||||
if (requestCode == STORAGE_PERMISSION_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Storage read permission granted")
|
||||
|
||||
if (configString != null) {
|
||||
bindVpnService()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindVpnService() {
|
||||
try {
|
||||
val intent = Intent(this, VPNService::class.java)
|
||||
intent.action = IMPORT_ACTION_CODE
|
||||
|
||||
bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent, action: String): String? {
|
||||
val scheme = intent.scheme
|
||||
|
||||
if (scheme == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
|
||||
val resolver = contentResolver
|
||||
|
||||
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
|
||||
val uri = intent.data
|
||||
val name: String? = getContentName(resolver, uri)
|
||||
|
||||
Log.d(TAG, "Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
|
||||
|
||||
val input = resolver.openInputStream(uri!!)
|
||||
|
||||
return input?.bufferedReader()?.use(BufferedReader::readText)
|
||||
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
|
||||
val uri = intent.data
|
||||
val name = uri!!.lastPathSegment
|
||||
|
||||
Log.d(TAG, "File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
|
||||
|
||||
val input = resolver.openInputStream(uri)
|
||||
|
||||
return input?.bufferedReader()?.use(BufferedReader::readText)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
|
||||
val cursor = resolver!!.query(uri!!, null, null, null, null)
|
||||
|
||||
cursor.use {
|
||||
cursor!!.moveToFirst()
|
||||
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||
return if (nameIndex >= 0) {
|
||||
return cursor.getString(nameIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var connection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
||||
vpnServiceBinder = Messenger(binder)
|
||||
|
||||
if (configString != null) {
|
||||
val msg: Message = Message.obtain(null, IMPORT_COMMAND_CODE, 0, 0)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(IMPORT_CONFIG_KEY, configString!!)
|
||||
msg.data = bundle
|
||||
|
||||
try {
|
||||
vpnServiceBinder?.send(msg)
|
||||
} catch (e: RemoteException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
configString = null
|
||||
}
|
||||
|
||||
isBound = true
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
vpnServiceBinder = null
|
||||
isBound = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
|
@ -263,9 +263,19 @@ android {
|
|||
android/gradlew.bat \
|
||||
android/gradle.properties \
|
||||
android/res/values/libs.xml \
|
||||
android/res/xml/fileprovider.xml \
|
||||
android/src/org/amnezia/vpn/AuthHelper.java \
|
||||
android/src/org/amnezia/vpn/IPCContract.kt \
|
||||
android/src/org/amnezia/vpn/NotificationUtil.kt \
|
||||
android/src/org/amnezia/vpn/OpenVPNThreadv3.kt \
|
||||
android/src/org/amnezia/vpn/Prefs.kt \
|
||||
android/src/org/amnezia/vpn/VpnLogger.kt \
|
||||
android/src/org/amnezia/vpn/VpnService.kt \
|
||||
android/src/org/amnezia/vpn/VpnServiceBinder.kt \
|
||||
android/src/org/amnezia/vpn/qt/AmneziaApp.kt \
|
||||
android/src/org/amnezia/vpn/qt/PackageManagerHelper.java \
|
||||
android/src/org/amnezia/vpn/qt/VPNActivity.kt \
|
||||
android/src/org/amnezia/vpn/qt/VPNApplication.java \
|
||||
android/src/org/amnezia/vpn/qt/VPNPermissionHelper.kt
|
||||
|
||||
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
|
||||
|
|
|
@ -16,19 +16,21 @@
|
|||
|
||||
#include "android_controller.h"
|
||||
#include "core/errorstrings.h"
|
||||
#include "ui/pages_logic/StartPageLogic.h"
|
||||
|
||||
// Binder Codes for VPNServiceBinder
|
||||
// See also - VPNServiceBinder.kt
|
||||
// Actions that are Requestable
|
||||
const int ACTION_ACTIVATE = 1;
|
||||
const int ACTION_DEACTIVATE = 2;
|
||||
const int ACTION_REGISTERLISTENER = 3;
|
||||
const int ACTION_REGISTER_LISTENER = 3;
|
||||
const int ACTION_REQUEST_STATISTIC = 4;
|
||||
const int ACTION_REQUEST_GET_LOG = 5;
|
||||
const int ACTION_REQUEST_CLEANUP_LOG = 6;
|
||||
const int ACTION_RESUME_ACTIVATE = 7;
|
||||
const int ACTION_SET_NOTIFICATION_TEXT = 8;
|
||||
const int ACTION_SET_NOTIFICATION_FALLBACK = 9;
|
||||
const int ACTION_SHARE_CONFIG = 10;
|
||||
|
||||
// Event Types that will be Dispatched after registration
|
||||
const int EVENT_INIT = 0;
|
||||
|
@ -37,6 +39,7 @@ const int EVENT_DISCONNECTED = 2;
|
|||
const int EVENT_STATISTIC_UPDATE = 3;
|
||||
const int EVENT_BACKEND_LOGS = 4;
|
||||
const int EVENT_ACTIVATION_ERROR = 5;
|
||||
const int EVENT_CONFIG_IMPORT = 6;
|
||||
|
||||
namespace {
|
||||
AndroidController* s_instance = nullptr;
|
||||
|
@ -57,10 +60,12 @@ AndroidController* AndroidController::instance() {
|
|||
return s_instance;
|
||||
}
|
||||
|
||||
bool AndroidController::initialize()
|
||||
bool AndroidController::initialize(StartPageLogic *startPageLogic)
|
||||
{
|
||||
qDebug() << "Initializing";
|
||||
|
||||
m_startPageLogic = startPageLogic;
|
||||
|
||||
// Hook in the native implementation for startActivityForResult into the JNI
|
||||
JNINativeMethod methods[]{{"startActivityForResult",
|
||||
"(Landroid/content/Intent;)V",
|
||||
|
@ -148,6 +153,16 @@ void AndroidController::setNotificationText(const QString& title,
|
|||
m_serviceBinder.transact(ACTION_SET_NOTIFICATION_TEXT, data, nullptr);
|
||||
}
|
||||
|
||||
void AndroidController::shareConfig(const QString& configContent, const QString& suggestedName) {
|
||||
QJsonObject rootObject;
|
||||
rootObject["data"] = configContent;
|
||||
rootObject["suggestedName"] = suggestedName;
|
||||
QJsonDocument doc(rootObject);
|
||||
QAndroidParcel parcel;
|
||||
parcel.writeData(doc.toJson());
|
||||
m_serviceBinder.transact(ACTION_SHARE_CONFIG, parcel, nullptr);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets fallback Notification text that should be shown in case the VPN
|
||||
* switches into the Connected state without the app open
|
||||
|
@ -187,6 +202,10 @@ void AndroidController::cleanupBackendLogs() {
|
|||
m_serviceBinder.transact(ACTION_REQUEST_CLEANUP_LOG, nullParcel, nullptr);
|
||||
}
|
||||
|
||||
void AndroidController::importConfig(const QString& data){
|
||||
m_startPageLogic->importConnectionFromCode(data);
|
||||
}
|
||||
|
||||
void AndroidController::onServiceConnected(
|
||||
const QString& name, const QAndroidBinder& serviceBinder) {
|
||||
qDebug() << "Server " + name + " connected";
|
||||
|
@ -198,7 +217,7 @@ void AndroidController::onServiceConnected(
|
|||
// Send the Service our Binder to recive incoming Events
|
||||
QAndroidParcel binderParcel;
|
||||
binderParcel.writeBinder(m_binder);
|
||||
m_serviceBinder.transact(ACTION_REGISTERLISTENER, binderParcel, nullptr);
|
||||
m_serviceBinder.transact(ACTION_REGISTER_LISTENER, binderParcel, nullptr);
|
||||
}
|
||||
|
||||
void AndroidController::onServiceDisconnected(const QString& name) {
|
||||
|
@ -279,7 +298,14 @@ bool AndroidController::VPNBinder::onTransact(int code,
|
|||
case EVENT_ACTIVATION_ERROR:
|
||||
qDebug() << "Transact: error";
|
||||
emit m_controller->connectionStateChanged(VpnProtocol::Error);
|
||||
|
||||
break;
|
||||
case EVENT_CONFIG_IMPORT:
|
||||
qDebug() << "Transact: config import";
|
||||
doc = QJsonDocument::fromJson(data.readData());
|
||||
buffer = doc.object()["config"].toString();
|
||||
qDebug() << "Transact: config string" << buffer;
|
||||
m_controller->importConfig(buffer);
|
||||
break;
|
||||
default:
|
||||
qWarning() << "Transact: Invalid!";
|
||||
break;
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
#include <QAndroidBinder>
|
||||
#include <QAndroidServiceConnection>
|
||||
|
||||
#include "ui/uilogic.h"
|
||||
#include "ui/pages_logic/StartPageLogic.h"
|
||||
|
||||
#include "protocols/vpnprotocol.h"
|
||||
using namespace amnezia;
|
||||
|
||||
|
||||
|
||||
class AndroidController : public QObject, public QAndroidServiceConnection
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -19,7 +21,7 @@ public:
|
|||
|
||||
virtual ~AndroidController() override = default;
|
||||
|
||||
bool initialize();
|
||||
bool initialize(StartPageLogic *startPageLogic);
|
||||
|
||||
ErrorCode start();
|
||||
void stop();
|
||||
|
@ -27,9 +29,11 @@ public:
|
|||
|
||||
void checkStatus();
|
||||
void setNotificationText(const QString& title, const QString& message, int timerSec);
|
||||
void shareConfig(const QString& data, const QString& suggestedName);
|
||||
void setFallbackConnectedNotification();
|
||||
void getBackendLogs(std::function<void(const QString&)>&& callback);
|
||||
void cleanupBackendLogs();
|
||||
void importConfig(const QString& data);
|
||||
|
||||
// from QAndroidServiceConnection
|
||||
void onServiceConnected(const QString& name, const QAndroidBinder& serviceBinder) override;
|
||||
|
@ -58,6 +62,8 @@ private:
|
|||
//Protocol m_protocol;
|
||||
QJsonObject m_vpnConfig;
|
||||
|
||||
StartPageLogic *m_startPageLogic;
|
||||
|
||||
bool m_serviceConnected = false;
|
||||
std::function<void(const QString&)> m_logCallback;
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ New encryption keys pair will be generated.")
|
|||
Layout.bottomMargin: 10
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
text: qsTr("Save to file")
|
||||
text: Qt.platform.os === "android" ? qsTr("Share") : qsTr("Save to file")
|
||||
enabled: tfShareCode.textArea.length > 0
|
||||
visible: tfShareCode.textArea.length > 0
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ PageShareProtocolBase {
|
|||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
text: qsTr("Save to file")
|
||||
text: Qt.platform.os === "android" ? qsTr("Share") : qsTr("Save to file")
|
||||
enabled: tfShareCode.textArea.length > 0
|
||||
visible: tfShareCode.textArea.length > 0
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ PageShareProtocolBase {
|
|||
Layout.preferredHeight: 40
|
||||
width: parent.width - 60
|
||||
|
||||
text: qsTr("Save to file")
|
||||
text: Qt.platform.os === "android" ? qsTr("Share") : qsTr("Save to file")
|
||||
enabled: tfShareCode.textArea.length > 0
|
||||
visible: tfShareCode.textArea.length > 0
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ PageShareProtocolBase {
|
|||
Layout.preferredHeight: 40
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Save to file")
|
||||
text: Qt.platform.os === "android" ? qsTr("Share") : qsTr("Save to file")
|
||||
enabled: tfShareCode.textArea.length > 0
|
||||
visible: tfShareCode.textArea.length > 0
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ void UiLogic::initalizeUiLogic()
|
|||
pageLogic<VpnLogic>()->onConnectionStateChanged(VpnProtocol::Connected);
|
||||
}
|
||||
});
|
||||
if (!AndroidController::instance()->initialize()) {
|
||||
if (!AndroidController::instance()->initialize(pageLogic<StartPageLogic>())) {
|
||||
qCritical() << QString("Init failed") ;
|
||||
emit VpnProtocol::Error;
|
||||
return;
|
||||
|
@ -593,8 +593,9 @@ void UiLogic::saveTextFile(const QString& desc, const QString& suggestedName, QS
|
|||
if (fileName.isEmpty()) return;
|
||||
if (!fileName.toString().endsWith(ext)) fileName = QUrl(fileName.toString() + ext);
|
||||
#elif defined Q_OS_ANDROID
|
||||
fileName = QFileDialog::getSaveFileUrl(nullptr, suggestedName,
|
||||
QUrl::fromLocalFile(docDir), "*" + ext);
|
||||
qDebug() << "UiLogic::shareConfig" << data;
|
||||
AndroidController::instance()->shareConfig(data, suggestedName);
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (fileName.isEmpty()) return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue