Skip to content

Commit 69b0574

Browse files
committed
io,app: add deeplinking support
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 <inkeliz@inkeliz.com>
1 parent 1934b4f commit 69b0574

File tree

14 files changed

+364
-7
lines changed

14 files changed

+364
-7
lines changed

app/GioActivity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import android.app.Activity;
66
import android.os.Bundle;
7+
import android.content.Intent;
78
import android.content.res.Configuration;
89
import android.view.ViewGroup;
910
import android.view.View;
@@ -29,6 +30,7 @@ public final class GioActivity extends Activity {
2930

3031
layer.addView(view);
3132
setContentView(layer);
33+
onNewIntent(this.getIntent());
3234
}
3335

3436
@Override public void onDestroy() {
@@ -60,4 +62,9 @@ public final class GioActivity extends Activity {
6062
if (!view.backPressed())
6163
super.onBackPressed();
6264
}
65+
66+
@Override protected void onNewIntent(Intent intent) {
67+
super.onNewIntent(intent);
68+
view.onIntentEvent(intent);
69+
}
6370
}

app/GioView.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import android.app.FragmentManager;
1313
import android.app.FragmentTransaction;
1414
import android.content.Context;
15+
import android.content.Intent;
1516
import android.graphics.Canvas;
1617
import android.graphics.Color;
1718
import android.graphics.Matrix;
@@ -311,6 +312,15 @@ private void setHighRefreshRate() {
311312
window.setAttributes(layoutParams);
312313
}
313314

315+
protected void onIntentEvent(Intent intent) {
316+
if (intent == null) {
317+
return;
318+
}
319+
if (intent.getData() != null) {
320+
this.onDeeplink(nhandle, intent.getData().toString());
321+
}
322+
}
323+
314324
@Override protected boolean dispatchHoverEvent(MotionEvent event) {
315325
if (!accessManager.isTouchExplorationEnabled()) {
316326
return super.dispatchHoverEvent(event);
@@ -549,6 +559,7 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1
549559
static private native void onExitTouchExploration(long handle);
550560
static private native void onA11yFocus(long handle, int viewId);
551561
static private native void onClearA11yFocus(long handle, int viewId);
562+
static private native void onDeeplink(long handle, String uri);
552563
static private native void imeSetSnippet(long handle, int start, int end);
553564
static private native String imeSnippet(long handle);
554565
static private native int imeSnippetStart(long handle);

app/framework_ios.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
#include <UIKit/UIKit.h>
44

55
@interface GioViewController : UIViewController
6+
- (BOOL)onDeeplink:(NSString *)url;
67
@end

app/internal/windows/windows.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ const (
9999

100100
CW_USEDEFAULT = -2147483648
101101

102+
ERROR_ALREADY_EXISTS = 183
103+
102104
GWL_STYLE = ^(uintptr(16) - 1) // -16
103105

104106
GCS_COMPSTR = 0x0008
@@ -111,7 +113,8 @@ const (
111113
CFS_POINT = 0x0002
112114
CFS_CANDIDATEPOS = 0x0040
113115

114-
HWND_TOPMOST = ^(uint32(1) - 1) // -1
116+
HWND_TOPMOST = ^(uint32(1) - 1) // -1
117+
HWND_BROADCAST = 0xFFFF
115118

116119
HTCAPTION = 2
117120
HTCLIENT = 1
@@ -303,15 +306,25 @@ const (
303306
LR_MONOCHROME = 0x00000001
304307
LR_SHARED = 0x00008000
305308
LR_VGACOLOR = 0x00000080
309+
310+
FILE_MAP_READ = 0x0004
311+
)
312+
313+
var (
314+
GIO_DEEPLINKING uint32 // Custom message for deep linking, lazy init
306315
)
307316

308317
var (
309318
kernel32 = syscall.NewLazySystemDLL("kernel32.dll")
319+
_CreateMutex = kernel32.NewProc("CreateMutexW")
320+
_GetMutexInfo = kernel32.NewProc("GetMutexInfo")
310321
_GetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
311322
_GlobalAlloc = kernel32.NewProc("GlobalAlloc")
312323
_GlobalFree = kernel32.NewProc("GlobalFree")
313324
_GlobalLock = kernel32.NewProc("GlobalLock")
314325
_GlobalUnlock = kernel32.NewProc("GlobalUnlock")
326+
_OpenFileMapping = kernel32.NewProc("OpenFileMappingA")
327+
_ReleaseMutex = kernel32.NewProc("ReleaseMutex")
315328

316329
user32 = syscall.NewLazySystemDLL("user32.dll")
317330
_AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx")
@@ -347,9 +360,11 @@ var (
347360
_PostQuitMessage = user32.NewProc("PostQuitMessage")
348361
_ReleaseCapture = user32.NewProc("ReleaseCapture")
349362
_RegisterClassExW = user32.NewProc("RegisterClassExW")
363+
_RegisterWindowMessage = user32.NewProc("RegisterWindowMessageW")
350364
_ReleaseDC = user32.NewProc("ReleaseDC")
351365
_ScreenToClient = user32.NewProc("ScreenToClient")
352366
_ShowWindow = user32.NewProc("ShowWindow")
367+
_SendMessage = user32.NewProc("SendMessageW")
353368
_SetCapture = user32.NewProc("SetCapture")
354369
_SetCursor = user32.NewProc("SetCursor")
355370
_SetClipboardData = user32.NewProc("SetClipboardData")
@@ -651,6 +666,42 @@ func GlobalUnlock(h syscall.Handle) {
651666
_GlobalUnlock.Call(uintptr(h))
652667
}
653668

669+
func CreateMutex(name string) (ptr syscall.Handle, alreadyExists bool, err error) {
670+
r, _, err := _CreateMutex.Call(0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
671+
switch err.(syscall.Errno) {
672+
case 0:
673+
return syscall.Handle(r), false, nil
674+
case ERROR_ALREADY_EXISTS:
675+
return syscall.Handle(r), true, nil
676+
default:
677+
return 0, false, fmt.Errorf("CreateMutex: %v", err)
678+
}
679+
}
680+
681+
func GetMutexInfo(h syscall.Handle) (pid, count uint32, err error) {
682+
r, _, err := _GetMutexInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)), uintptr(unsafe.Pointer(&count)))
683+
if r == 0 {
684+
return 0, 0, fmt.Errorf("GetMutexInfo: %v", err)
685+
}
686+
return pid, count, nil
687+
}
688+
689+
func OpenFileMapping(name string) (ptr syscall.Handle, err error) {
690+
r, _, err := _OpenFileMapping.Call(FILE_MAP_READ, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
691+
if r == 0 {
692+
return 0, fmt.Errorf("OpenFileMapping: %v", err)
693+
}
694+
return syscall.Handle(r), nil
695+
}
696+
697+
func ReleaseMutex(h uintptr) error {
698+
r, _, err := _ReleaseMutex.Call(h)
699+
if r == 0 {
700+
return fmt.Errorf("ReleaseMutex: %v", err)
701+
}
702+
return nil
703+
}
704+
654705
func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error {
655706
r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0)
656707
if r == 0 {
@@ -735,10 +786,26 @@ func RegisterClassEx(cls *WndClassEx) (uint16, error) {
735786
return uint16(a), nil
736787
}
737788

789+
func RegisterWindowMessage(name string) (uint32, error) {
790+
r, _, err := _RegisterWindowMessage.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))))
791+
if r == 0 {
792+
return 0, fmt.Errorf("RegisterWindowMessage: %v", err)
793+
}
794+
return uint32(r), nil
795+
}
796+
738797
func ReleaseDC(hdc syscall.Handle) {
739798
_ReleaseDC.Call(uintptr(hdc))
740799
}
741800

801+
func SendMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error {
802+
r, _, err := _SendMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
803+
if r == 0 {
804+
return fmt.Errorf("SendMessage failed: %v", err)
805+
}
806+
return nil
807+
}
808+
742809
func SetForegroundWindow(hwnd syscall.Handle) {
743810
_SetForegroundWindow.Call(uintptr(hwnd))
744811
}

app/os_android.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ import "C"
121121
import (
122122
"errors"
123123
"fmt"
124+
"gioui.org/io/deeplink"
124125
"image"
125126
"image/color"
126127
"math"
128+
"net/url"
127129
"os"
128130
"path/filepath"
129131
"runtime"
@@ -659,6 +661,16 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
659661
}
660662
}
661663

664+
//export Java_org_gioui_GioView_onDeeplink
665+
func Java_org_gioui_GioView_onDeeplink(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
666+
w := cgo.Handle(view).Value().(*window)
667+
u, err := url.Parse(goString(env, uri))
668+
if err != nil {
669+
return
670+
}
671+
w.callbacks.Event(deeplink.Event{URL: u})
672+
}
673+
662674
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
663675
for _, ch := range sem.Children {
664676
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))

app/os_ios.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ static struct drawParams viewDrawParams(CFTypeRef viewRef) {
7171
import "C"
7272

7373
import (
74+
"gioui.org/io/deeplink"
7475
"image"
76+
"net/url"
7577
"runtime"
7678
"runtime/debug"
7779
"time"
@@ -131,6 +133,9 @@ func onCreate(view, controller C.CFTypeRef) {
131133
w.Configure(wopts.options)
132134
w.w.Event(system.StageEvent{Stage: system.StagePaused})
133135
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
136+
if startupDeeplink != nil {
137+
w.w.Event(deeplink.Event{URL: startupDeeplink})
138+
}
134139
}
135140

136141
//export gio_onDraw
@@ -351,6 +356,23 @@ func newWindow(win *callbacks, options []Option) error {
351356
func osMain() {
352357
}
353358

359+
var startupDeeplink *url.URL
360+
361+
//export gio_onDeeplink
362+
func gio_onDeeplink(uri C.CFTypeRef) {
363+
u, err := url.Parse(nsstringToString(uri))
364+
if err != nil {
365+
return
366+
}
367+
if len(views) == 0 {
368+
startupDeeplink = u
369+
return
370+
}
371+
for _, w := range views {
372+
w.w.Event(deeplink.Event{URL: u})
373+
}
374+
}
375+
354376
//export gio_runMain
355377
func gio_runMain() {
356378
runMain()

app/os_ios.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ - (void)keyboardWillHide:(NSNotification *)note {
9999
_keyboardHeight = 0.0;
100100
[self.view setNeedsLayout];
101101
}
102+
103+
- (BOOL)onDeeplink:(NSString *)url {
104+
gio_onDeeplink((__bridge CFTypeRef)url);
105+
return YES;
106+
}
102107
@end
103108

104109
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {

app/os_macos.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package app
77

88
import (
99
"errors"
10+
"gioui.org/io/deeplink"
1011
"image"
12+
"net/url"
1113
"runtime"
1214
"time"
1315
"unicode"
@@ -842,6 +844,23 @@ func gio_onFinishLaunching() {
842844
close(launched)
843845
}
844846

847+
var startupDeeplink *url.URL
848+
849+
//export gio_onDeeplink
850+
func gio_onDeeplink(uri C.CFTypeRef) {
851+
u, err := url.Parse(nsstringToString(uri))
852+
if err != nil {
853+
return
854+
}
855+
if len(viewMap) == 0 {
856+
startupDeeplink = u
857+
return
858+
}
859+
for _, w := range viewMap {
860+
w.w.Event(deeplink.Event{URL: u})
861+
}
862+
}
863+
845864
func newWindow(win *callbacks, options []Option) error {
846865
<-launched
847866
errch := make(chan error)
@@ -867,6 +886,9 @@ func newWindow(win *callbacks, options []Option) error {
867886
C.makeKeyAndOrderFront(window)
868887
layer := C.layerForView(w.view)
869888
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
889+
if startupDeeplink != nil {
890+
w.w.Event(deeplink.Event{URL: startupDeeplink})
891+
}
870892
})
871893
return <-errch
872894
}

app/os_macos.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ @implementation GioAppDelegate
376376
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
377377
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
378378
[NSApp activateIgnoringOtherApps:YES];
379+
379380
gio_onFinishLaunching();
380381
}
381382
- (void)applicationDidHide:(NSNotification *)aNotification {
@@ -384,6 +385,11 @@ - (void)applicationDidHide:(NSNotification *)aNotification {
384385
- (void)applicationWillUnhide:(NSNotification *)notification {
385386
gio_onAppShow();
386387
}
388+
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
389+
for (NSURL *url in urls) {
390+
gio_onDeeplink((__bridge CFTypeRef)url.absoluteString);
391+
}
392+
}
387393
@end
388394

389395
void gio_main() {

0 commit comments

Comments
 (0)