From 378afe9272cad82ca59ec90fc5b4d1af4cb2094e Mon Sep 17 00:00:00 2001 From: Sebastian Streich Date: Mon, 18 Dec 2023 15:58:33 +0100 Subject: [PATCH] Android Deep Linking Bits (#8763) * Add support to read the Opener URL from Intents Wooorking Wooorking * Formatting * Remove unused * run clang format .. * Formatting --- .../firefox/vpn/daemon/NotificationUtil.kt | 35 ++++++++++++---- .../mozilla/firefox/vpn/qt/VPNActivity.java | 41 ++++++++++++++++++- src/commands/commandui.cpp | 16 ++++++++ src/loghandler.cpp | 8 ++++ src/platforms/android/androidvpnactivity.cpp | 38 ++++++++++++++++- src/platforms/android/androidvpnactivity.h | 12 ++++++ 6 files changed, 139 insertions(+), 11 deletions(-) diff --git a/android/daemon/src/main/java/org/mozilla/firefox/vpn/daemon/NotificationUtil.kt b/android/daemon/src/main/java/org/mozilla/firefox/vpn/daemon/NotificationUtil.kt index 41a27706a6..5f4f4dff30 100644 --- a/android/daemon/src/main/java/org/mozilla/firefox/vpn/daemon/NotificationUtil.kt +++ b/android/daemon/src/main/java/org/mozilla/firefox/vpn/daemon/NotificationUtil.kt @@ -11,6 +11,7 @@ import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import androidx.core.app.NotificationCompat import kotlinx.serialization.Serializable @@ -39,8 +40,18 @@ class NotificationUtil(ctx: Service) { // Create the Intent that Should be Fired if the User Clicks the notification val activity = Class.forName(mainActivityName) val intent = Intent(context, activity) + try { + message.requestedScreen.let { + intent.data = Uri.parse(message.requestedScreen) + } + } catch (ex: Exception) { + // Uuuh let's just put a default one? + intent.data = Uri.parse("mozilla-vpn://home") + } + val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + // Build our notification mNotificationBuilder .setSmallIcon(R.drawable.icon_mozillavpn_notifiaction) @@ -132,17 +143,20 @@ class NotificationUtil(ctx: Service) { } } - /* - * ClientNotification - * Message sent from the client manually. - */ + /* + * ClientNotification + * Message sent from the client manually. + */ @Serializable -data class ClientNotification(val header: String, val body: String) +data class ClientNotification( + val header: String, + val body: String, +) - /* - * A "Canned" Notification contains all strings needed for the "(dis-)/connected" flow - * and is provided by the controller when asking for a connection. - */ + /* + * A "Canned" Notification contains all strings needed for the "(dis-)/connected" flow + * and is provided by the controller when asking for a connection. + */ @Serializable data class CannedNotification( // Message to be shown when the Client connects @@ -151,6 +165,8 @@ data class CannedNotification( val disconnectedMessage: ClientNotification, // Product-Name -> Will be used as the Notification Header val productName: String, + // requestedScreen: a url -> mozilla-vpn:// + val requestedScreen: String?, ) { companion object { /** @@ -173,6 +189,7 @@ data class CannedNotification( messages.getString("disconnectedBody"), ), messages.getString("productName"), + messages.getString("requestedScreen"), ) } catch (e: Exception) { Log.e("NotificationUtil", "Failed to Parse Notification Object $value") diff --git a/android/vpnClient/src/main/java/org/mozilla/firefox/vpn/qt/VPNActivity.java b/android/vpnClient/src/main/java/org/mozilla/firefox/vpn/qt/VPNActivity.java index 713facf5f2..23326e4956 100644 --- a/android/vpnClient/src/main/java/org/mozilla/firefox/vpn/qt/VPNActivity.java +++ b/android/vpnClient/src/main/java/org/mozilla/firefox/vpn/qt/VPNActivity.java @@ -8,12 +8,14 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; +import android.util.Log; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; @@ -64,7 +66,8 @@ public Object getSystemService (String name){ public native void onServiceMessage(int actionCode, String body); public native void qtOnServiceConnected(); public native void qtOnServiceDisconnected(); - + public native void onIntentInternal(); + public static void connectService(){ VPNActivity.getInstance().initServiceConnection(); } @@ -214,4 +217,40 @@ protected void onDestroy() { unbindService(mConnection); super.onDestroy(); } + + /** + * Checks the intent that opened the activtiy + * @return a mozilla-vpn:// url + */ + public static String getOpenerURL() { + if (instance == null) { + return ""; + } + Uri maybeURI = instance.getIntent().getData(); + if (maybeURI == null) { + // Just a normal open + return ""; + } + return maybeURI.toString(); + } + @Override + protected void onNewIntent(Intent intent) { + // getIntent() always returns the + // original intent the app was opened with + // we however have no use for that + // so let's always keep the newest one + // and notify the Client of that Change + setIntent(intent); + if (nativeMethodsAvailable) { + onIntentInternal(); + } + } + + // Make sure we do Not Call Native Functions + // Until the Client has told us the + // registration of them is complete + private static boolean nativeMethodsAvailable = false; + private static void nativeMethodsRegistered() { + nativeMethodsAvailable = true; + } } diff --git a/src/commands/commandui.cpp b/src/commands/commandui.cpp index 2036584c38..daee6663a1 100644 --- a/src/commands/commandui.cpp +++ b/src/commands/commandui.cpp @@ -70,6 +70,7 @@ #ifdef MZ_ANDROID # include "platforms/android/androidcommons.h" # include "platforms/android/androidutils.h" +# include "platforms/android/androidvpnactivity.h" #endif #ifndef Q_OS_WIN @@ -454,6 +455,20 @@ int CommandUI::run(QStringList& tokens) { &ServerHandler::close); #endif +#ifdef MZ_ANDROID + // If we are created with an url intent, auto pass that. + QUrl maybeURL = AndroidVPNActivity::getOpenerURL(); + if (!maybeURL.isValid()) { + logger.error() << "Error in deep-link:" << maybeURL.toString(); + } else { + Navigator::instance()->requestDeepLink(url); + } + // Whenever the Client is re-opened with a new url + // pass that to the navigaot + QObject::connect( + AndroidVPNActivity::instance(), &AndroidVPNActivity::onOpenedWithUrl, + [](QUrl url) { Navigator::instance()->requestDeepLink(url); }); +#else // If there happen to be navigation URLs, send them to the navigator class. for (const QString& value : tokens) { QUrl url(value); @@ -463,6 +478,7 @@ int CommandUI::run(QStringList& tokens) { Navigator::instance()->requestDeepLink(url); } } +#endif KeyRegenerator keyRegenerator; // Let's go. diff --git a/src/loghandler.cpp b/src/loghandler.cpp index c4a8191562..b4c3973188 100644 --- a/src/loghandler.cpp +++ b/src/loghandler.cpp @@ -261,12 +261,20 @@ void LogHandler::addLog(const Log& log, emit logEntryAdded(buffer); #if defined(MZ_ANDROID) +# ifdef MZ_DEBUG + const char* str = buffer.constData(); + if (str) { + __android_log_write(ANDROID_LOG_DEBUG, Constants::ANDROID_LOG_NAME, str); + } +# else if (!Constants::inProduction()) { const char* str = buffer.constData(); if (str) { __android_log_write(ANDROID_LOG_DEBUG, Constants::ANDROID_LOG_NAME, str); } } +# endif + #endif } diff --git a/src/platforms/android/androidvpnactivity.cpp b/src/platforms/android/androidvpnactivity.cpp index 55a711c630..d06d0654e5 100644 --- a/src/platforms/android/androidvpnactivity.cpp +++ b/src/platforms/android/androidvpnactivity.cpp @@ -9,7 +9,9 @@ #include #include #include +#include +#include "androidutils.h" #include "constants.h" #include "frontend/navigator.h" #include "jni.h" @@ -36,7 +38,7 @@ AndroidVPNActivity::AndroidVPNActivity() { reinterpret_cast(onServiceConnected)}, {"qtOnServiceDisconnected", "()V", reinterpret_cast(onServiceDisconnected)}, - }; + {"onIntentInternal", "()V", reinterpret_cast(onIntentInternal)}}; QJniObject javaClass(CLASSNAME); QJniEnvironment env; jclass objectClass = env->GetObjectClass(javaClass.object()); @@ -44,6 +46,8 @@ AndroidVPNActivity::AndroidVPNActivity() { sizeof(methods) / sizeof(methods[0])); env->DeleteLocalRef(objectClass); logger.debug() << "Registered native methods"; + QJniObject::callStaticMethod(CLASSNAME, "nativeMethodsRegistered", + "()V"); }); QObject::connect(SettingsHolder::instance(), @@ -189,3 +193,35 @@ void AndroidVPNActivity::onAppStateChange() { "(Z)V", isSensitive); }); } + +QUrl AndroidVPNActivity::getOpenerURL() { + logger.debug() << "Getting deep-link:"; + QJniEnvironment env; + QJniObject string = QJniObject::callStaticObjectMethod( + CLASSNAME, "getOpenerURL", "()Ljava/lang/String;"); + jstring value = (jstring)string.object(); + auto buf = AndroidUtils::getQByteArrayFromJString(env.jniEnv(), value); + + QString maybeURL = QString::fromUtf8(buf); + if (maybeURL.isEmpty()) { + return QUrl(); + } + logger.debug() << "Got with deep-link:" << maybeURL; + QUrl url(maybeURL); + if (!url.isValid()) { + return QUrl(); + } + if (url.scheme() != Constants::DEEP_LINK_SCHEME) { + return QUrl(); + } + return url; +} + +void AndroidVPNActivity::onIntentInternal(JNIEnv* env, jobject thiz) { + logger.debug() << "Activity Resumed with a new Intent"; + auto url = getOpenerURL(); + if (url.isEmpty()) { + return; + } + emit s_instance->onOpenedWithUrl(url); +} diff --git a/src/platforms/android/androidvpnactivity.h b/src/platforms/android/androidvpnactivity.h index 20fb389a47..fa2e225284 100644 --- a/src/platforms/android/androidvpnactivity.h +++ b/src/platforms/android/androidvpnactivity.h @@ -90,6 +90,14 @@ class AndroidVPNActivity : public QObject { static bool handleBackButton(JNIEnv* env, jobject thiz); static void sendToService(ServiceAction type, const QString& data = ""); static void connectService(); + /** + * @brief Checks if the Intent that opened the Activiy + * Contains a `mozilla-vpn://` url + * returns an Empty url if none is found + * + * @return QUrl + */ + static QUrl getOpenerURL(); void onAppStateChange(); signals: @@ -103,6 +111,7 @@ class AndroidVPNActivity : public QObject { void eventOnboardingCompleted(); void eventVpnConfigPermissionResponse(bool granted); void eventRequestGleanUploadEnabledState(); + void onOpenedWithUrl(const QUrl& data); private: AndroidVPNActivity(); @@ -113,6 +122,9 @@ class AndroidVPNActivity : public QObject { jstring body); static void onServiceConnected(JNIEnv* env, jobject thiz); static void onServiceDisconnected(JNIEnv* env, jobject thiz); + + // We got a new Intent + static void onIntentInternal(JNIEnv* env, jobject thiz); void handleServiceMessage(int code, const QString& data); };