-
Notifications
You must be signed in to change notification settings - Fork 134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
io,app: add deeplinking support #117
base: main
Are you sure you want to change the base?
Conversation
I use this code for testing: https://gist.github.com/inkeliz/11c071ec4bf8f922a3cb48fd0cbb3b95. That is based on gio-plugins/deeplink/demo, but modified, since the events comes from |
@gedw99 That panic was from gio-plugins, not from gio (this current PR). You should not use gio-plugins/deeplink anymore, since everything was ported to Gio (this patch). I didn't use I park the code here, https://github.com/inkeliz/deeplink-demo. It uses the
That will listen to |
Just to remind me in the future:
|
Hi @inkeliz, thanks a lot for implementing this feature. |
@mearaj, I don't think it's difficult to implement. However, personally, I don't use Linux, either my target-audience (it's < 1% that uses Linux). But, seems that you need to create a file, say
Then run However, its works similar to Windows: that will open the app passing the URL as the first parameter. So, it will require to create some IPC to send the URL to the active window, and make it a single-instance (either by pipe or d-bus). Currently, |
No worries, I understand. Thank you so much for the reply and providing the helpful info. :) |
HI @gedw99 |
Hi @ALL, |
I saw the gogio code, it doesn't supports linux as pointed out by @inkeliz. I have decided to create a separate repo for this feature on linux. It will support both gio and non gio apps. Will post the link here. Thanks :) |
Another alternative is to create some app.Scheme("yourscheme") option. Problems: that only works with Linux/Windows, because macOS/iOS/Android requires some manifest, and can't register it on runtime. So, what happens on iOS/Android?! Notice: My suggestion is NOT an My idea with Another approach, is to read Golang's AST. You use The easiest alternative: create a private-variable and then use Line 27 in d62057a
In this patch, a new one: Lines 1016 to 1018 in fe42c61
It not requires |
Hi @inkeliz , I mostly understood what you meant. I will try to implement it in gio itself by looking at the way you did for Windows. In case if I can't do it, I will share my implementation repo link here and will discuss with all of you guys :) |
Hi @inkeliz,@gedw99 and everyone. Desktop Entry Specs Before Window Creation:
The issue currently I am facing is that I don't know how to proceed further. In the current state whenever app receives any uri, it fires window.Event(transfer.URLEvent{URL: urlPtr}). I have created a PR at your branch.(It's not a final PR, but just for your review) |
discussion at gioui#117 (comment) Signed-off-by: Mearaj Bhagad <[email protected]>
On macOS (or iOS) it also receives multiple URLs, that is translated to multiple |
Thanks for asking.
These are the reasons I can currently think of :) |
Can you provide an example of how to open multiple URLs? Honestly, I've tried testing this using On Android, Currently, only macOS (and Linux) provide a list of URLs, and I'm not sure how to test that. Changing to |
Ahh yes, you are right. On linux I can convert multiple url into a single url . This way the app will have a common interface, so yeah your reason sounds very reasonable. So let's keep the way it is currently. I will make some changes in the current linux implementation and get back to you and will ask for your help again. Thanks a lot @inkeliz. |
app/window.go
Outdated
case transfer.URLEvent: | ||
w.out <- e2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is simple, but global. I believe transfer.URLEvent
s should be routed like transfer.DataEvent
. No?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. In the case of DataEvent
it can target a specific area of the UI, right? Like, drag/drop at specific zone. In the case of URLEvent
that is somewhat external. At least in my case, it will require to have SomeOp{Tag: tag}
in the main
. However, that already happens with the "Back Button" (which is now on InputOp
).
I don't know if that should be routed, and if it should be a new Op
or use TargetOp
? The MIME
field is strange in that case. Is MIME
the scheme? What happens if want the listen "all schemes"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think (most/all) events should be routed, for composability of components. Without routing, you can't just use some widget and forget about its internals. With routing, you don't need to know that the widget listens for a particular dnd or file open.
I don't know if that should be routed, and if it should be a new Op or use TargetOp? The MIME field is strange in that case. Is MIME the scheme? What happens if want the listen "all schemes"?
This is a problem for dnd as well. I imagine the MIME type be deferred from the file extension, falling back to "application/octet-stream". I also imagine you being able to register a range of types with say "image/*" or similar.
See also https://lists.sr.ht/~eliasnaur/gio-patches/patches/42549.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But in that case what "mime" is the "scheme"? I mean, how I listen to all schemes?MIME: "schemes/*
? How I listen to a specific scheme? MIME: "schemes/myCustomScheme
?
Also, what happens when the app is launched using one URL/Scheme? Because, in that specific case, I think you don't have TargetOp
setup yet. For each new TargetOp
it sends the "startup-url"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But in that case what "mime" is the "scheme"? I mean, how I listen to all schemes?
MIME: "schemes/*
? How I listen to a specific scheme?MIME: "schemes/myCustomScheme
?
Good points. A SchemeOp
seems appropriate.
Also, what happens when the app is launched using one URL/Scheme? Because, in that specific case, I think you don't have
TargetOp
setup yet. For each newTargetOp
it sends the "startup-url"?
The incoming URL must be retined by the app
package until one frame has been processed, to give the program a chance to receive the URL. After that, it can be forgotten.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add a new commit with SchemeOp. I create a another router/transfer.go
, since the current TargetOp
seems to be integrated with clip-area
.
app/os_windows.go
Outdated
mmap, err := windows.OpenFileMapping(windows.FILE_MAP_WRITE, schemesURI) | ||
if err != nil { | ||
return | ||
} | ||
defer windows.CloseFileMapping(mmap) | ||
|
||
dst, err := windows.MapViewOfFile(mmap, windows.FILE_MAP_WRITE, 1024*8) | ||
if err != nil { | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think WM_COPYDATA
is simpler and safer than communicating through the filesystem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to use WM_COPYDATA
but it requires to know the specific HWND of the receiver. Also, in both cases it will need to have mutex, which named-mmap
is also used for.
The receiver creates the mmap using the scheme as the name of the mmap. The sender checks if the mmap already exists, if exists writes the URL into the mmap and then sends a event, that event don't contain any pointer or mmap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to use WM_COPYDATA but it requires to know the specific HWND of the receiver.
If Windows opens a particular Gio binary, you should be able to locate any other running instances of that binary and only target them with WM_COPYDATA. For example, by the window class name or window name/id.
Also, in both cases it will need to have mutex, which named-mmap is also used for.
See above, I don't think you need a mutex if you just target windows belonging to the binary being opened.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found one issue related to UNC. I tried to search by path, then send COPYDATA to that executable.
For instance, it's possible to have one X:/demo.exe
and //192.168.0.1/demo.exe
, the first one is one "map" to the second one. Here is the issue, because I can't use os.Executable() == anotherExePath
.
Because of that edge-case, I'm comparing the filepath.Base
. However, Base
may cause collision (for instance your/app.exe is the same of randompath/app.exe).
I think another alternative is to use -appid
(?) from gogio
and also checks the WindowClassName
(which is currently hardcoded as "Gio"), not just the filepath.Base
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think another alternative is to use -appid (?) from gogio and also checks the WindowClassName (which is currently hardcoded as "Gio"), not just the filepath.Base.
Yes, please. That's what I meant by my comment mentioning window class names. I really don't think we should use executable paths to identify scheme event receivers. Coupled with my review comment about RegisterScheme
belonging in gogio
or an installer, I think gogio
to make URL scheme opening work on Windows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice that appID
is set in internal/log
, and used only for Android. I create a new variable, in os_windows.go
, but seems that both have the same purpose.
Maybe we should create some app/meta/meta.go
which contains AppID
, Version
, URLSchemes
(...)? That makes easier to set variables (without gogio) and also re-use it anywhere.
I will leave as duplicated, and in some next patch create such package.
69b0574
to
b38ddc0
Compare
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]>
@eliasnaur I need to squash multiple commits, due to rebase. I test have tested it on macOS, Android, iOS and Windows. I tried to avoid re-introducing the old |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great overall. A few nits and questions in the comments.
io/input/pointer.go
Outdated
@@ -320,6 +329,12 @@ func (p *pointerFilter) Matches(e event.Event) bool { | |||
return true | |||
} | |||
} | |||
case transfer.URLEvent: | |||
for _, t := range p.scheme { | |||
if e.URL != nil && (t == "" || t == e.URL.Scheme) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should assume e.URL != nil
.
case transfer.URLFilter: | ||
t = q // See comment in processEvent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not treat transfer.URLFilter
the same as the tag-less key.Filter
above, instead of inventing a fake tag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because q.key.scratchFilter = append(q.key.scratchFilter, f)
is a specific slice that holds key.Filter. Where the "generic one" (used by anything else) uses "taggedFilter". I don't think is right to add yet another specialised slice.
@@ -464,6 +468,10 @@ func (q *Router) processEvent(e event.Event, system bool) { | |||
cstate, evts := q.cqueue.Push(state.clipboardState, e) | |||
state.clipboardState = cstate | |||
q.changeState(e, state, evts) | |||
case transfer.URLEvent: | |||
var evts []taggedEvent | |||
evts = append(evts, taggedEvent{tag: q, event: e}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like key.Event
above, I would not expect a transfer.URLEvent
to have an associated tag.
// On Windows, launching the app using a URI will start a new instance of the app, | ||
// a new window. That behavior, by default, doesn't align with iOS/Android/macOS, where | ||
// the deeplink sends the event to the running app (if any). We are emulating it. | ||
if hwnd, _ := windows.FindWindow(ID); hwnd != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if multiple program instances have windows matching the ID?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that is unlikely, and only one instance of the program must run anyway. Previously the executable path was used as identifier, then moved to appid
(#117 (comment)).
Maybe in gogio
it must throw one error if appid
is not provided and schemes
are.
Assuming that appid
is unique only one instance of this app will run.
Signed-off-by: inkeliz <[email protected]>
Signed-off-by: inkeliz <[email protected]>
Signed-off-by: inkeliz <[email protected]>
I wanted to merge this soon, and thought more about the corner-cases. I'm having cold feet with respect to exposing deeplinking support as
I don't have better ideas than to back out the package app
// Events is an iterator that yields events that are not specific to any window,
// such as [URLEvent]. It never returns.
//
// Events must be called by the main goroutine, and replaces the
// call to [Main].
func Events(yield func(event.Event) bool) and package app
func Main() {
Events(func(event.Event){})
} ( |
One consequence of a global |
Originally, the event was sent directly to window.Event (instead of gtx.Event). I find it odd to create another event handler (window.Event, layout.Context.Event, and now app.Events). While I understand the reasoning, since this Event is not associated with a specific Window, I think it's just too much. Also, some OSes (Android and Switch) it's somewhat bounded to the View/Window. |
f8029f2
to
026d3f9
Compare
How do you propose delivering the deeplink events to a running programs without open windows? |
3d36537
to
74ccc9c
Compare
does gio community consider to merge this to main? I am testing out golang to write mobile app and working on an Oauth ( keycloak ) authentication which will do a "callback", normally we will define custom scheme ( eg myapp://callback ) to receive the token back. it will be helpful to have the deeplink support. @inkeliz is it easy to make the deeplink as a separate package ? |
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 singleinstance of the app can be open. In other words, just a single
myprogram.exe
canbe 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 ingogio
, usingas
-deeplink gio
will listen togio://
.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:
Windows - You need to compiling using -X argument:
Android - You need to add IntentFilter in GioActivity:
That assumes that you still using GioActivity and GioAppDelegate, otherwise more
changes are required.