Skip to content

Commit 9ef4718

Browse files
committed
io,app: add custom scheme 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 transfer.URLEvent, containing the URL launched. If the program is already open, one transfer.URLEvent will be sent to the current app. Limitations: On Windows, if the program listen to schemes (compiled with `-schemes`), 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 `-schemes` flag in `gogio`, using as `-schemes 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]>
1 parent e6da07a commit 9ef4718

13 files changed

+445
-9
lines changed

app/GioActivity.java

+7
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

+11
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.onOpenURI(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 onOpenURI(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

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
#include <UIKit/UIKit.h>
44

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

app/internal/windows/windows.go

+25
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ type MonitorInfo struct {
108108
Flags uint32
109109
}
110110

111+
type CopyDataStruct struct {
112+
DwData uintptr
113+
CbData uint32
114+
LpData uintptr
115+
}
116+
111117
const (
112118
TRUE = 1
113119

@@ -247,6 +253,7 @@ const (
247253
WM_CANCELMODE = 0x001F
248254
WM_CHAR = 0x0102
249255
WM_CLOSE = 0x0010
256+
WM_COPYDATA = 0x004A
250257
WM_CREATE = 0x0001
251258
WM_DPICHANGED = 0x02E0
252259
WM_DESTROY = 0x0002
@@ -344,6 +351,7 @@ var (
344351
_DefWindowProc = user32.NewProc("DefWindowProcW")
345352
_DestroyWindow = user32.NewProc("DestroyWindow")
346353
_DispatchMessage = user32.NewProc("DispatchMessageW")
354+
_FindWindow = user32.NewProc("FindWindowW")
347355
_EmptyClipboard = user32.NewProc("EmptyClipboard")
348356
_GetWindowRect = user32.NewProc("GetWindowRect")
349357
_GetClientRect = user32.NewProc("GetClientRect")
@@ -374,6 +382,7 @@ var (
374382
_ReleaseDC = user32.NewProc("ReleaseDC")
375383
_ScreenToClient = user32.NewProc("ScreenToClient")
376384
_ShowWindow = user32.NewProc("ShowWindow")
385+
_SendMessage = user32.NewProc("SendMessageW")
377386
_SetCapture = user32.NewProc("SetCapture")
378387
_SetCursor = user32.NewProc("SetCursor")
379388
_SetClipboardData = user32.NewProc("SetClipboardData")
@@ -473,6 +482,14 @@ func EmptyClipboard() error {
473482
return nil
474483
}
475484

485+
func FindWindow(lpClassName string) (syscall.Handle, error) {
486+
hwnd, _, err := _FindWindow.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpClassName))), 0)
487+
if hwnd == 0 {
488+
return 0, fmt.Errorf("FindWindow failed: %v", err)
489+
}
490+
return syscall.Handle(hwnd), nil
491+
}
492+
476493
func GetWindowRect(hwnd syscall.Handle) Rect {
477494
var r Rect
478495
_GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
@@ -780,6 +797,14 @@ func ReleaseDC(hdc syscall.Handle) {
780797
_ReleaseDC.Call(uintptr(hdc))
781798
}
782799

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

app/os_android.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ import "C"
121121
import (
122122
"errors"
123123
"fmt"
124+
"gioui.org/io/transfer"
124125
"image"
125126
"image/color"
126127
"io"
127128
"math"
129+
"net/url"
128130
"os"
129131
"path/filepath"
130132
"runtime"
@@ -146,7 +148,6 @@ import (
146148
"gioui.org/io/pointer"
147149
"gioui.org/io/semantic"
148150
"gioui.org/io/system"
149-
"gioui.org/io/transfer"
150151
"gioui.org/unit"
151152
)
152153

@@ -667,6 +668,16 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
667668
}
668669
}
669670

671+
//export Java_org_gioui_GioView_onOpenURI
672+
func Java_org_gioui_GioView_onOpenURI(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
673+
w := cgo.Handle(view).Value().(*window)
674+
u, err := url.Parse(goString(env, uri))
675+
if err != nil {
676+
return
677+
}
678+
w.ProcessEvent(transfer.URLEvent{URL: u})
679+
}
680+
670681
func (w *window) ProcessEvent(e event.Event) {
671682
w.processEvent(e)
672683
}

app/os_ios.go

+42
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ import (
8383
"image"
8484
"io"
8585
"os"
86+
"net/url"
8687
"runtime"
8788
"runtime/cgo"
8889
"runtime/debug"
8990
"strings"
91+
"sync"
9092
"time"
9193
"unicode/utf16"
9294
"unsafe"
@@ -121,6 +123,16 @@ type window struct {
121123

122124
var mainWindow = newWindowRendezvous()
123125

126+
// activeViews is the list of active views.
127+
var activeViews = make([]*window, 0, 1)
128+
129+
// mutexActiveViews is used to protect activeViews.
130+
var mutexActiveViews = sync.Mutex{}
131+
132+
// startupURI is the URI to open when the first window is created,
133+
// but no window is active yet.
134+
var startupURI *url.URL
135+
124136
func init() {
125137
// Darwin requires UI operations happen on the main thread only.
126138
runtime.LockOSThread()
@@ -145,8 +157,15 @@ func onCreate(view, controller C.CFTypeRef) {
145157
}
146158
w.displayLink = dl
147159
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
160+
mutexActiveViews.Lock()
161+
activeViews = append(activeViews, w)
162+
mutexActiveViews.Unlock()
148163
w.Configure(wopts.options)
149164
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
165+
if startupURI != nil {
166+
w.ProcessEvent(transfer.URLEvent{URL: startupURI})
167+
startupURI = nil
168+
}
150169
}
151170

152171
func viewFor(h C.uintptr_t) *window {
@@ -213,6 +232,14 @@ func onDestroy(h C.uintptr_t) {
213232
w.displayLink.Close()
214233
w.displayLink = nil
215234
cgo.Handle(h).Delete()
235+
mutexActiveViews.Lock()
236+
for i, v := range activeViews {
237+
if v == w {
238+
activeViews = append(activeViews[:i], activeViews[i+1:]...)
239+
break
240+
}
241+
}
242+
mutexActiveViews.Unlock()
216243
w.view = 0
217244
}
218245

@@ -425,6 +452,21 @@ func osMain() {
425452
}
426453
}
427454

455+
//export gio_onOpenURI
456+
func gio_onOpenURI(uri C.CFTypeRef) {
457+
u, err := url.Parse(nsstringToString(uri))
458+
if err != nil {
459+
return
460+
}
461+
if len(activeViews) == 0 {
462+
startupURI = u
463+
return
464+
}
465+
for _, w := range activeViews {
466+
w.ProcessEvent(transfer.URLEvent{URL: u})
467+
}
468+
}
469+
428470
//export gio_runMain
429471
func gio_runMain() {
430472
if !isMainThread() {

app/os_ios.m

+11-2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ - (void)keyboardWillHide:(NSNotification *)note {
103103
_keyboardHeight = 0.0;
104104
[self.view setNeedsLayout];
105105
}
106+
107+
- (BOOL)onOpenURI:(NSString *)url {
108+
gio_onOpenURI((__bridge CFTypeRef)url);
109+
return YES;
110+
}
106111
#endif
107112
@end
108113

@@ -283,16 +288,20 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
283288

284289
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
285290
@property (strong, nonatomic) UIWindow *window;
291+
@property (strong, nonatomic) GioViewController *controller;
286292
@end
287293

288294
@implementation _gioAppDelegate
289295
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
290296
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
291-
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
292-
self.window.rootViewController = controller;
297+
self.controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
298+
self.window.rootViewController = self.controller;
293299
[self.window makeKeyAndVisible];
294300
return YES;
295301
}
302+
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
303+
return [self.controller onOpenURI:url.absoluteString];
304+
}
296305
@end
297306

298307
int gio_applicationMain(int argc, char *argv[]) {

app/os_macos.go

+45
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"errors"
1010
"image"
1111
"io"
12+
"net/url"
1213
"runtime"
1314
"runtime/cgo"
1415
"strings"
16+
"sync"
1517
"time"
1618
"unicode"
1719
"unicode/utf8"
@@ -334,6 +336,17 @@ type window struct {
334336
// launched is closed when applicationDidFinishLaunching is called.
335337
var launched = make(chan struct{})
336338

339+
// activeViews is the list of active windows.
340+
var activeViews = make([]*window, 0, 1)
341+
342+
// mutexActiveViews protects activeViews.
343+
var mutexActiveViews = sync.Mutex{}
344+
345+
// startupURI is the URL event that was received before the app was launched.
346+
// Since the app is not running yet, the URL event is stored and processed after
347+
// the view is created.
348+
var startupURI *url.URL
349+
337350
// nextTopLeft is the offset to use for the next window's call to
338351
// cascadeTopLeftFromPoint.
339352
var nextTopLeft C.NSPoint
@@ -865,6 +878,10 @@ func gio_onAttached(h C.uintptr_t, attached C.int) {
865878
if attached != 0 {
866879
layer := C.layerForView(w.view)
867880
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
881+
if startupURI != nil {
882+
w.ProcessEvent(transfer.URLEvent{URL: startupURI})
883+
startupURI = nil
884+
}
868885
} else {
869886
w.ProcessEvent(AppKitViewEvent{})
870887
w.visible = false
@@ -879,6 +896,14 @@ func gio_onDestroy(h C.uintptr_t) {
879896
w.displayLink.Close()
880897
w.displayLink = nil
881898
cgo.Handle(h).Delete()
899+
mutexActiveViews.Lock()
900+
for i, win := range activeViews {
901+
if win == w {
902+
activeViews = append(activeViews[:i], activeViews[i+1:]...)
903+
break
904+
}
905+
}
906+
mutexActiveViews.Unlock()
882907
w.view = 0
883908
}
884909

@@ -914,6 +939,23 @@ func gio_onFinishLaunching() {
914939
close(launched)
915940
}
916941

942+
//export gio_onOpenURI
943+
func gio_onOpenURI(uri C.CFTypeRef) {
944+
u, err := url.Parse(nsstringToString(uri))
945+
if err != nil {
946+
return
947+
}
948+
949+
if len(activeViews) == 0 {
950+
startupURI = u
951+
return
952+
}
953+
954+
for _, w := range activeViews {
955+
w.ProcessEvent(transfer.URLEvent{URL: u})
956+
}
957+
}
958+
917959
func newWindow(win *callbacks, options []Option) {
918960
<-launched
919961
res := make(chan struct{})
@@ -971,6 +1013,9 @@ func (w *window) init() error {
9711013
return err
9721014
}
9731015
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
1016+
mutexActiveViews.Lock()
1017+
activeViews = append(activeViews, w)
1018+
mutexActiveViews.Unlock()
9741019
w.view = view
9751020
return nil
9761021
}

app/os_macos.m

+6
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ CFTypeRef gio_createView(void) {
395395
selector:@selector(applicationDidHide:)
396396
name:NSApplicationDidHideNotification
397397
object:nil];
398+
398399
return CFBridgingRetain(view);
399400
}
400401
}
@@ -412,6 +413,11 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
412413
[NSApp activateIgnoringOtherApps:YES];
413414
gio_onFinishLaunching();
414415
}
416+
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
417+
for (NSURL *url in urls) {
418+
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
419+
}
420+
}
415421
@end
416422

417423
void gio_main() {

0 commit comments

Comments
 (0)