Skip to content

Commit

Permalink
io,app: add deeplinking support
Browse files Browse the repository at this point in the history
Now, it's possible to launch one Gio app using a custom URI scheme, such as `gio://some/data`.

This feature is supported on Android, iOS, macOS and Windows, issuing a new deeplink.Event,
containing the URL launched. If the program is already opened, one deeplink.Event will be
sent to the current opened app.

Limitations:
On Windows, if the program uses deeplink (compiled with `-deeplink`), then just a single
instance of the app can be open. In other words, just a single `myprogram.exe` can
be active.

Security:
Deeplinking have the same level of security of clipboard. Any other software can send such
information and read the content, without any restriction. That should not be used to transfer
sensible data, and can't be fully trusted.

Setup/Compiling:
In order to set the custom scheme, you need to use the new `-deeplink` flag in `gogio`, using
as `-deeplink gio` will listen to `gio://`.

If you are not using gogio you need to defined some values, which varies for each OS:

macOS/iOS - You need to define the following Properly List:
```
<key>CFBundleURLTypes</key>
<array>
  <dict>
	<key>CFBundleURLSchemes</key>
	<array>
	  <string>yourCustomScheme</string>
	</array>
  </dict>
</array>
```

Windows - You need to compiling using -X argument:
```
-ldflags="-X "gioui.org/app.schemesDeeplink=yourCustomScheme" -H=windowsgui"
```

Android - You need to add IntentFilter in GioActivity:
```
<intent-filter>
	<action android:name="android.intent.action.VIEW"></action>
	<category android:name="android.intent.category.DEFAULT"></category>
	<category android:name="android.intent.category.BROWSABLE"></category>
	<data android:scheme="yourCustomScheme"></data>
</intent-filter>
```

That assumes that you still using GioActivity and GioAppDelegate, otherwise more
changes are required.

Signed-off-by: inkeliz <[email protected]>
  • Loading branch information
inkeliz committed Jul 8, 2023
1 parent 1934b4f commit 69b0574
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 7 deletions.
7 changes: 7 additions & 0 deletions app/GioActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import android.app.Activity;
import android.os.Bundle;
import android.content.Intent;
import android.content.res.Configuration;
import android.view.ViewGroup;
import android.view.View;
Expand All @@ -29,6 +30,7 @@ public final class GioActivity extends Activity {

layer.addView(view);
setContentView(layer);
onNewIntent(this.getIntent());
}

@Override public void onDestroy() {
Expand Down Expand Up @@ -60,4 +62,9 @@ public final class GioActivity extends Activity {
if (!view.backPressed())
super.onBackPressed();
}

@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
view.onIntentEvent(intent);
}
}
11 changes: 11 additions & 0 deletions app/GioView.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
Expand Down Expand Up @@ -311,6 +312,15 @@ private void setHighRefreshRate() {
window.setAttributes(layoutParams);
}

protected void onIntentEvent(Intent intent) {
if (intent == null) {
return;
}
if (intent.getData() != null) {
this.onDeeplink(nhandle, intent.getData().toString());
}
}

@Override protected boolean dispatchHoverEvent(MotionEvent event) {
if (!accessManager.isTouchExplorationEnabled()) {
return super.dispatchHoverEvent(event);
Expand Down Expand Up @@ -549,6 +559,7 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1
static private native void onExitTouchExploration(long handle);
static private native void onA11yFocus(long handle, int viewId);
static private native void onClearA11yFocus(long handle, int viewId);
static private native void onDeeplink(long handle, String uri);
static private native void imeSetSnippet(long handle, int start, int end);
static private native String imeSnippet(long handle);
static private native int imeSnippetStart(long handle);
Expand Down
1 change: 1 addition & 0 deletions app/framework_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
#include <UIKit/UIKit.h>

@interface GioViewController : UIViewController
- (BOOL)onDeeplink:(NSString *)url;
@end
69 changes: 68 additions & 1 deletion app/internal/windows/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const (

CW_USEDEFAULT = -2147483648

ERROR_ALREADY_EXISTS = 183

GWL_STYLE = ^(uintptr(16) - 1) // -16

GCS_COMPSTR = 0x0008
Expand All @@ -111,7 +113,8 @@ const (
CFS_POINT = 0x0002
CFS_CANDIDATEPOS = 0x0040

HWND_TOPMOST = ^(uint32(1) - 1) // -1
HWND_TOPMOST = ^(uint32(1) - 1) // -1
HWND_BROADCAST = 0xFFFF

HTCAPTION = 2
HTCLIENT = 1
Expand Down Expand Up @@ -303,15 +306,25 @@ const (
LR_MONOCHROME = 0x00000001
LR_SHARED = 0x00008000
LR_VGACOLOR = 0x00000080

FILE_MAP_READ = 0x0004
)

var (
GIO_DEEPLINKING uint32 // Custom message for deep linking, lazy init
)

var (
kernel32 = syscall.NewLazySystemDLL("kernel32.dll")
_CreateMutex = kernel32.NewProc("CreateMutexW")
_GetMutexInfo = kernel32.NewProc("GetMutexInfo")
_GetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
_GlobalAlloc = kernel32.NewProc("GlobalAlloc")
_GlobalFree = kernel32.NewProc("GlobalFree")
_GlobalLock = kernel32.NewProc("GlobalLock")
_GlobalUnlock = kernel32.NewProc("GlobalUnlock")
_OpenFileMapping = kernel32.NewProc("OpenFileMappingA")
_ReleaseMutex = kernel32.NewProc("ReleaseMutex")

user32 = syscall.NewLazySystemDLL("user32.dll")
_AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx")
Expand Down Expand Up @@ -347,9 +360,11 @@ var (
_PostQuitMessage = user32.NewProc("PostQuitMessage")
_ReleaseCapture = user32.NewProc("ReleaseCapture")
_RegisterClassExW = user32.NewProc("RegisterClassExW")
_RegisterWindowMessage = user32.NewProc("RegisterWindowMessageW")
_ReleaseDC = user32.NewProc("ReleaseDC")
_ScreenToClient = user32.NewProc("ScreenToClient")
_ShowWindow = user32.NewProc("ShowWindow")
_SendMessage = user32.NewProc("SendMessageW")
_SetCapture = user32.NewProc("SetCapture")
_SetCursor = user32.NewProc("SetCursor")
_SetClipboardData = user32.NewProc("SetClipboardData")
Expand Down Expand Up @@ -651,6 +666,42 @@ func GlobalUnlock(h syscall.Handle) {
_GlobalUnlock.Call(uintptr(h))
}

func CreateMutex(name string) (ptr syscall.Handle, alreadyExists bool, err error) {
r, _, err := _CreateMutex.Call(0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
switch err.(syscall.Errno) {
case 0:
return syscall.Handle(r), false, nil
case ERROR_ALREADY_EXISTS:
return syscall.Handle(r), true, nil
default:
return 0, false, fmt.Errorf("CreateMutex: %v", err)
}
}

func GetMutexInfo(h syscall.Handle) (pid, count uint32, err error) {
r, _, err := _GetMutexInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)), uintptr(unsafe.Pointer(&count)))
if r == 0 {
return 0, 0, fmt.Errorf("GetMutexInfo: %v", err)
}
return pid, count, nil
}

func OpenFileMapping(name string) (ptr syscall.Handle, err error) {
r, _, err := _OpenFileMapping.Call(FILE_MAP_READ, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
if r == 0 {
return 0, fmt.Errorf("OpenFileMapping: %v", err)
}
return syscall.Handle(r), nil
}

func ReleaseMutex(h uintptr) error {
r, _, err := _ReleaseMutex.Call(h)
if r == 0 {
return fmt.Errorf("ReleaseMutex: %v", err)
}
return nil
}

func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error {
r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0)
if r == 0 {
Expand Down Expand Up @@ -735,10 +786,26 @@ func RegisterClassEx(cls *WndClassEx) (uint16, error) {
return uint16(a), nil
}

func RegisterWindowMessage(name string) (uint32, error) {
r, _, err := _RegisterWindowMessage.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
if r == 0 {
return 0, fmt.Errorf("RegisterWindowMessage: %v", err)
}
return uint32(r), nil
}

func ReleaseDC(hdc syscall.Handle) {
_ReleaseDC.Call(uintptr(hdc))
}

func SendMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error {
r, _, err := _SendMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
if r == 0 {
return fmt.Errorf("SendMessage failed: %v", err)
}
return nil
}

func SetForegroundWindow(hwnd syscall.Handle) {
_SetForegroundWindow.Call(uintptr(hwnd))
}
Expand Down
12 changes: 12 additions & 0 deletions app/os_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,11 @@ import "C"
import (
"errors"
"fmt"
"gioui.org/io/deeplink"
"image"
"image/color"
"math"
"net/url"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -659,6 +661,16 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
}
}

//export Java_org_gioui_GioView_onDeeplink
func Java_org_gioui_GioView_onDeeplink(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
w := cgo.Handle(view).Value().(*window)
u, err := url.Parse(goString(env, uri))
if err != nil {
return
}
w.callbacks.Event(deeplink.Event{URL: u})
}

func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
for _, ch := range sem.Children {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
Expand Down
22 changes: 22 additions & 0 deletions app/os_ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ static struct drawParams viewDrawParams(CFTypeRef viewRef) {
import "C"

import (
"gioui.org/io/deeplink"
"image"
"net/url"
"runtime"
"runtime/debug"
"time"
Expand Down Expand Up @@ -131,6 +133,9 @@ func onCreate(view, controller C.CFTypeRef) {
w.Configure(wopts.options)
w.w.Event(system.StageEvent{Stage: system.StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
if startupDeeplink != nil {
w.w.Event(deeplink.Event{URL: startupDeeplink})
}
}

//export gio_onDraw
Expand Down Expand Up @@ -351,6 +356,23 @@ func newWindow(win *callbacks, options []Option) error {
func osMain() {
}

var startupDeeplink *url.URL

//export gio_onDeeplink
func gio_onDeeplink(uri C.CFTypeRef) {
u, err := url.Parse(nsstringToString(uri))
if err != nil {
return
}
if len(views) == 0 {
startupDeeplink = u
return
}
for _, w := range views {
w.w.Event(deeplink.Event{URL: u})
}
}

//export gio_runMain
func gio_runMain() {
runMain()
Expand Down
5 changes: 5 additions & 0 deletions app/os_ios.m
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ - (void)keyboardWillHide:(NSNotification *)note {
_keyboardHeight = 0.0;
[self.view setNeedsLayout];
}

- (BOOL)onDeeplink:(NSString *)url {
gio_onDeeplink((__bridge CFTypeRef)url);
return YES;
}
@end

static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
Expand Down
22 changes: 22 additions & 0 deletions app/os_macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ package app

import (
"errors"
"gioui.org/io/deeplink"
"image"
"net/url"
"runtime"
"time"
"unicode"
Expand Down Expand Up @@ -842,6 +844,23 @@ func gio_onFinishLaunching() {
close(launched)
}

var startupDeeplink *url.URL

//export gio_onDeeplink
func gio_onDeeplink(uri C.CFTypeRef) {
u, err := url.Parse(nsstringToString(uri))
if err != nil {
return
}
if len(viewMap) == 0 {
startupDeeplink = u
return
}
for _, w := range viewMap {
w.w.Event(deeplink.Event{URL: u})
}
}

func newWindow(win *callbacks, options []Option) error {
<-launched
errch := make(chan error)
Expand All @@ -867,6 +886,9 @@ func newWindow(win *callbacks, options []Option) error {
C.makeKeyAndOrderFront(window)
layer := C.layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
if startupDeeplink != nil {
w.w.Event(deeplink.Event{URL: startupDeeplink})
}
})
return <-errch
}
Expand Down
6 changes: 6 additions & 0 deletions app/os_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ @implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];

gio_onFinishLaunching();
}
- (void)applicationDidHide:(NSNotification *)aNotification {
Expand All @@ -384,6 +385,11 @@ - (void)applicationDidHide:(NSNotification *)aNotification {
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
}
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
gio_onDeeplink((__bridge CFTypeRef)url.absoluteString);
}
}
@end

void gio_main() {
Expand Down
Loading

0 comments on commit 69b0574

Please sign in to comment.