diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index cb70759d..55cffe20 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -1,5 +1,10 @@ - + @@ -18,12 +23,72 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -79,10 +144,17 @@ - + + + + @@ -95,10 +167,24 @@ - + + + + + + + + diff --git a/client/android/res/xml/fileprovider.xml b/client/android/res/xml/fileprovider.xml new file mode 100644 index 00000000..426348c0 --- /dev/null +++ b/client/android/res/xml/fileprovider.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/IPCContract.kt b/client/android/src/org/amnezia/vpn/IPCContract.kt new file mode 100644 index 00000000..4ae3596d --- /dev/null +++ b/client/android/src/org/amnezia/vpn/IPCContract.kt @@ -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" diff --git a/client/android/src/org/amnezia/vpn/VPNService.kt b/client/android/src/org/amnezia/vpn/VPNService.kt index a1f8550c..23e9ff0c 100644 --- a/client/android/src/org/amnezia/vpn/VPNService.kt +++ b/client/android/src/org/amnezia/vpn/VPNService.kt @@ -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()) + } + } } diff --git a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt index d81d5077..640a0657 100644 --- a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt +++ b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt @@ -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 } /** @@ -70,101 +72,148 @@ class VPNServiceBinder(service: VPNService) : Binder() { return true } - ACTIONS.resumeActivate -> { - // [data] is empty - // Activate the current tunnel - try { - mResumeConfig?.let { this.mService.turnOn(it) } - } catch (e: Exception) { - Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}") - } - return true - } - - ACTIONS.deactivate -> { - // [data] here is empty - this.mService.turnOff() - return true - } - - ACTIONS.registerEventListener -> { - // [data] contains the Binder that we need to dispatch the Events - val binder = data.readStrongBinder() - mListener = binder - val obj = JSONObject() - obj.put("connected", mService.isUp) - obj.put("time", mService.connectionTime) - dispatchEvent(EVENTS.init, obj.toString()) - return true - } - - ACTIONS.requestStatistic -> { - dispatchEvent(EVENTS.statisticUpdate, mService.status.toString()) - return true - } - - ACTIONS.requestGetLog -> { - // Grabs all the Logs and dispatch new Log Event - 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 - } - IBinder.LAST_CALL_TRANSACTION -> { - Log.e(tag, "The OS Requested to shut down the VPN") - this.mService.turnOff() - return true - } - - else -> { - Log.e(tag, "Received invalid bind request \t Code -> $code") - // If we're hitting this there is probably something wrong in the client. - return false - } - } - return false - } - - /** - * Dispatches an Event to all registered Binders - * [code] the Event that happened - see [EVENTS] - * To register an Eventhandler use [onTransact] with - * [ACTIONS.registerEventListener] - */ - fun dispatchEvent(code: Int, payload: String?) { + ACTIONS.resumeActivate -> { + // [data] is empty + // Activate the current tunnel try { - mListener?.let { - if (it.isBinderAlive) { - val data = Parcel.obtain() - data.writeByteArray(payload?.toByteArray(charset("UTF-8"))) - it.transact(code, data, Parcel.obtain(), 0) - } - } - } catch (e: DeadObjectException) { - // If the QT Process is killed (not just inactive) - // we cant access isBinderAlive, so nothing to do here. + mResumeConfig?.let { this.mService.turnOn(it) } + } catch (e: Exception) { + Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}") } + return true } - /** - * The codes we Are Using in case of [dispatchEvent] - */ - object EVENTS { - const val init = 0 - const val connected = 1 - const val disconnected = 2 - const val statisticUpdate = 3 - const val backendLogs = 4 - const val activationError = 5 + ACTIONS.deactivate -> { + // [data] here is empty + this.mService.turnOff() + return true + } + + ACTIONS.registerEventListener -> { + // [data] contains the Binder that we need to dispatch the Events + val binder = data.readStrongBinder() + mListener = binder + val obj = JSONObject() + 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 + } + + ACTIONS.requestStatistic -> { + dispatchEvent(EVENTS.statisticUpdate, mService.status.toString()) + return true + } + + ACTIONS.requestGetLog -> { + // Grabs all the Logs and dispatch new Log Event + 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() + return true + } + + else -> { + Log.e(tag, "Received invalid bind request \t Code -> $code") + // If we're hitting this there is probably something wrong in the client. + return false } } + + return false + } + + /** + * Dispatches an Event to all registered Binders + * [code] the Event that happened - see [EVENTS] + * To register an Eventhandler use [onTransact] with + * [ACTIONS.registerEventListener] + */ + fun dispatchEvent(code: Int, payload: String?) { + try { + mListener?.let { + if (it.isBinderAlive) { + val data = Parcel.obtain() + data.writeByteArray(payload?.toByteArray(charset("UTF-8"))) + it.transact(code, data, Parcel.obtain(), 0) + } + } + } catch (e: DeadObjectException) { + // If the QT Process is killed (not just inactive) + // we cant access isBinderAlive, so nothing to do here. + } + } + + /** + * The codes we Are Using in case of [dispatchEvent] + */ + object EVENTS { + const val init = 0 + const val connected = 1 + const val disconnected = 2 + 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 + } + } +} diff --git a/client/android/src/org/amnezia/vpn/qt/VPNActivity.java b/client/android/src/org/amnezia/vpn/qt/VPNActivity.java deleted file mode 100644 index 11d99063..00000000 --- a/client/android/src/org/amnezia/vpn/qt/VPNActivity.java +++ /dev/null @@ -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(); -} diff --git a/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt new file mode 100644 index 00000000..c5b5107e --- /dev/null +++ b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt @@ -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, 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) + } +} diff --git a/client/client.pro b/client/client.pro index 7840ae6a..ca309455 100644 --- a/client/client.pro +++ b/client/client.pro @@ -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 diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 6b2c1aa0..af30fe05 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -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; diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index a4734414..c2a65381 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -4,11 +4,13 @@ #include #include +#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&& 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 m_logCallback; diff --git a/client/ui/qml/Pages/Share/PageShareProtoAmnezia.qml b/client/ui/qml/Pages/Share/PageShareProtoAmnezia.qml index e4b0ff83..2afecbad 100644 --- a/client/ui/qml/Pages/Share/PageShareProtoAmnezia.qml +++ b/client/ui/qml/Pages/Share/PageShareProtoAmnezia.qml @@ -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 diff --git a/client/ui/qml/Pages/Share/PageShareProtoCloak.qml b/client/ui/qml/Pages/Share/PageShareProtoCloak.qml index bd415c95..fe92c1e7 100644 --- a/client/ui/qml/Pages/Share/PageShareProtoCloak.qml +++ b/client/ui/qml/Pages/Share/PageShareProtoCloak.qml @@ -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 diff --git a/client/ui/qml/Pages/Share/PageShareProtoOpenVPN.qml b/client/ui/qml/Pages/Share/PageShareProtoOpenVPN.qml index 5ca587b7..ab4a9a3d 100644 --- a/client/ui/qml/Pages/Share/PageShareProtoOpenVPN.qml +++ b/client/ui/qml/Pages/Share/PageShareProtoOpenVPN.qml @@ -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 diff --git a/client/ui/qml/Pages/Share/PageShareProtoWireGuard.qml b/client/ui/qml/Pages/Share/PageShareProtoWireGuard.qml index f5746e20..336964ac 100644 --- a/client/ui/qml/Pages/Share/PageShareProtoWireGuard.qml +++ b/client/ui/qml/Pages/Share/PageShareProtoWireGuard.qml @@ -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 diff --git a/client/ui/uilogic.cpp b/client/ui/uilogic.cpp index e1468da7..1922a1ab 100644 --- a/client/ui/uilogic.cpp +++ b/client/ui/uilogic.cpp @@ -134,7 +134,7 @@ void UiLogic::initalizeUiLogic() pageLogic()->onConnectionStateChanged(VpnProtocol::Connected); } }); - if (!AndroidController::instance()->initialize()) { + if (!AndroidController::instance()->initialize(pageLogic())) { qCritical() << QString("Init failed") ; emit VpnProtocol::Error; return; @@ -595,8 +595,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;