Skip to content

Commit

Permalink
io,app: add custom scheme 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 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 <inkeliz@inkeliz.com>
  • Loading branch information
inkeliz committed Jun 3, 2024
1 parent e6da07a commit 9ef4718
Show file tree
Hide file tree
Showing 13 changed files with 445 additions and 9 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.onOpenURI(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 onOpenURI(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)onOpenURI:(NSString *)url;
@end
25 changes: 25 additions & 0 deletions app/internal/windows/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ type MonitorInfo struct {
Flags uint32
}

type CopyDataStruct struct {
DwData uintptr
CbData uint32
LpData uintptr
}

const (
TRUE = 1

Expand Down Expand Up @@ -247,6 +253,7 @@ const (
WM_CANCELMODE = 0x001F
WM_CHAR = 0x0102
WM_CLOSE = 0x0010
WM_COPYDATA = 0x004A
WM_CREATE = 0x0001
WM_DPICHANGED = 0x02E0
WM_DESTROY = 0x0002
Expand Down Expand Up @@ -344,6 +351,7 @@ var (
_DefWindowProc = user32.NewProc("DefWindowProcW")
_DestroyWindow = user32.NewProc("DestroyWindow")
_DispatchMessage = user32.NewProc("DispatchMessageW")
_FindWindow = user32.NewProc("FindWindowW")
_EmptyClipboard = user32.NewProc("EmptyClipboard")
_GetWindowRect = user32.NewProc("GetWindowRect")
_GetClientRect = user32.NewProc("GetClientRect")
Expand Down Expand Up @@ -374,6 +382,7 @@ var (
_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 @@ -473,6 +482,14 @@ func EmptyClipboard() error {
return nil
}

func FindWindow(lpClassName string) (syscall.Handle, error) {
hwnd, _, err := _FindWindow.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpClassName))), 0)
if hwnd == 0 {
return 0, fmt.Errorf("FindWindow failed: %v", err)
}
return syscall.Handle(hwnd), nil
}

func GetWindowRect(hwnd syscall.Handle) Rect {
var r Rect
_GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
Expand Down Expand Up @@ -780,6 +797,14 @@ 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
13 changes: 12 additions & 1 deletion app/os_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ import "C"
import (
"errors"
"fmt"
"gioui.org/io/transfer"
"image"
"image/color"
"io"
"math"
"net/url"
"os"
"path/filepath"
"runtime"
Expand All @@ -146,7 +148,6 @@ import (
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)

Expand Down Expand Up @@ -667,6 +668,16 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
}
}

//export Java_org_gioui_GioView_onOpenURI
func Java_org_gioui_GioView_onOpenURI(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.ProcessEvent(transfer.URLEvent{URL: u})
}

func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
Expand Down
42 changes: 42 additions & 0 deletions app/os_ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ import (
"image"
"io"
"os"
"net/url"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"sync"
"time"
"unicode/utf16"
"unsafe"
Expand Down Expand Up @@ -121,6 +123,16 @@ type window struct {

var mainWindow = newWindowRendezvous()

// activeViews is the list of active views.
var activeViews = make([]*window, 0, 1)

// mutexActiveViews is used to protect activeViews.
var mutexActiveViews = sync.Mutex{}

// startupURI is the URI to open when the first window is created,
// but no window is active yet.
var startupURI *url.URL

func init() {
// Darwin requires UI operations happen on the main thread only.
runtime.LockOSThread()
Expand All @@ -145,8 +157,15 @@ func onCreate(view, controller C.CFTypeRef) {
}
w.displayLink = dl
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
mutexActiveViews.Lock()
activeViews = append(activeViews, w)
mutexActiveViews.Unlock()
w.Configure(wopts.options)
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
if startupURI != nil {
w.ProcessEvent(transfer.URLEvent{URL: startupURI})
startupURI = nil
}
}

func viewFor(h C.uintptr_t) *window {
Expand Down Expand Up @@ -213,6 +232,14 @@ func onDestroy(h C.uintptr_t) {
w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
mutexActiveViews.Lock()
for i, v := range activeViews {
if v == w {
activeViews = append(activeViews[:i], activeViews[i+1:]...)
break
}
}
mutexActiveViews.Unlock()
w.view = 0
}

Expand Down Expand Up @@ -425,6 +452,21 @@ func osMain() {
}
}

//export gio_onOpenURI
func gio_onOpenURI(uri C.CFTypeRef) {
u, err := url.Parse(nsstringToString(uri))
if err != nil {
return
}
if len(activeViews) == 0 {
startupURI = u
return
}
for _, w := range activeViews {
w.ProcessEvent(transfer.URLEvent{URL: u})
}
}

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

- (BOOL)onOpenURI:(NSString *)url {
gio_onOpenURI((__bridge CFTypeRef)url);
return YES;
}
#endif
@end

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

@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) GioViewController *controller;
@end

@implementation _gioAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = controller;
self.controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = self.controller;
[self.window makeKeyAndVisible];
return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
return [self.controller onOpenURI:url.absoluteString];
}
@end

int gio_applicationMain(int argc, char *argv[]) {
Expand Down
45 changes: 45 additions & 0 deletions app/os_macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"errors"
"image"
"io"
"net/url"
"runtime"
"runtime/cgo"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -334,6 +336,17 @@ type window struct {
// launched is closed when applicationDidFinishLaunching is called.
var launched = make(chan struct{})

// activeViews is the list of active windows.
var activeViews = make([]*window, 0, 1)

// mutexActiveViews protects activeViews.
var mutexActiveViews = sync.Mutex{}

// startupURI is the URL event that was received before the app was launched.
// Since the app is not running yet, the URL event is stored and processed after
// the view is created.
var startupURI *url.URL

// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
Expand Down Expand Up @@ -865,6 +878,10 @@ func gio_onAttached(h C.uintptr_t, attached C.int) {
if attached != 0 {
layer := C.layerForView(w.view)
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
if startupURI != nil {
w.ProcessEvent(transfer.URLEvent{URL: startupURI})
startupURI = nil
}
} else {
w.ProcessEvent(AppKitViewEvent{})
w.visible = false
Expand All @@ -879,6 +896,14 @@ func gio_onDestroy(h C.uintptr_t) {
w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
mutexActiveViews.Lock()
for i, win := range activeViews {
if win == w {
activeViews = append(activeViews[:i], activeViews[i+1:]...)
break
}
}
mutexActiveViews.Unlock()
w.view = 0
}

Expand Down Expand Up @@ -914,6 +939,23 @@ func gio_onFinishLaunching() {
close(launched)
}

//export gio_onOpenURI
func gio_onOpenURI(uri C.CFTypeRef) {
u, err := url.Parse(nsstringToString(uri))
if err != nil {
return
}

if len(activeViews) == 0 {
startupURI = u
return
}

for _, w := range activeViews {
w.ProcessEvent(transfer.URLEvent{URL: u})
}
}

func newWindow(win *callbacks, options []Option) {
<-launched
res := make(chan struct{})
Expand Down Expand Up @@ -971,6 +1013,9 @@ func (w *window) init() error {
return err
}
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
mutexActiveViews.Lock()
activeViews = append(activeViews, w)
mutexActiveViews.Unlock()
w.view = view
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions app/os_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ CFTypeRef gio_createView(void) {
selector:@selector(applicationDidHide:)
name:NSApplicationDidHideNotification
object:nil];

return CFBridgingRetain(view);
}
}
Expand All @@ -412,6 +413,11 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
}
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
}
}
@end

void gio_main() {
Expand Down
Loading

0 comments on commit 9ef4718

Please sign in to comment.