Skip to content

Commit

Permalink
Android Deep Linking Bits (#8763)
Browse files Browse the repository at this point in the history
* Add support to read the Opener URL from Intents

Wooorking

Wooorking

* Formatting

* Remove unused

* run clang format ..

* Formatting
  • Loading branch information
strseb authored Dec 18, 2023
1 parent 08db1e2 commit 378afe9
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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://<my-string>
val requestedScreen: String?,
) {
companion object {
/**
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -214,4 +217,40 @@ protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}

/**
* Checks the intent that opened the activtiy
* @return a mozilla-vpn://<screenID> 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;
}
}
16 changes: 16 additions & 0 deletions src/commands/commandui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -463,6 +478,7 @@ int CommandUI::run(QStringList& tokens) {
Navigator::instance()->requestDeepLink(url);
}
}
#endif

KeyRegenerator keyRegenerator;
// Let's go.
Expand Down
8 changes: 8 additions & 0 deletions src/loghandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
38 changes: 37 additions & 1 deletion src/platforms/android/androidvpnactivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <QJniObject>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>

#include "androidutils.h"
#include "constants.h"
#include "frontend/navigator.h"
#include "jni.h"
Expand All @@ -36,14 +38,16 @@ AndroidVPNActivity::AndroidVPNActivity() {
reinterpret_cast<void*>(onServiceConnected)},
{"qtOnServiceDisconnected", "()V",
reinterpret_cast<void*>(onServiceDisconnected)},
};
{"onIntentInternal", "()V", reinterpret_cast<void*>(onIntentInternal)}};
QJniObject javaClass(CLASSNAME);
QJniEnvironment env;
jclass objectClass = env->GetObjectClass(javaClass.object<jobject>());
env->RegisterNatives(objectClass, methods,
sizeof(methods) / sizeof(methods[0]));
env->DeleteLocalRef(objectClass);
logger.debug() << "Registered native methods";
QJniObject::callStaticMethod<void>(CLASSNAME, "nativeMethodsRegistered",
"()V");
});

QObject::connect(SettingsHolder::instance(),
Expand Down Expand Up @@ -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);
}
12 changes: 12 additions & 0 deletions src/platforms/android/androidvpnactivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -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://<something>` url
* returns an Empty url if none is found
*
* @return QUrl
*/
static QUrl getOpenerURL();
void onAppStateChange();

signals:
Expand All @@ -103,6 +111,7 @@ class AndroidVPNActivity : public QObject {
void eventOnboardingCompleted();
void eventVpnConfigPermissionResponse(bool granted);
void eventRequestGleanUploadEnabledState();
void onOpenedWithUrl(const QUrl& data);

private:
AndroidVPNActivity();
Expand All @@ -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);
};

Expand Down

0 comments on commit 378afe9

Please sign in to comment.