keymanapp / keyman

Keyman cross platform input methods system running on Android, iOS, Linux, macOS, Windows and mobile and desktop web
https://keyman.com/
Other
403 stars 112 forks source link

feat(web): add support for OS-provided user dictionaries #11872

Open jahorton opened 5 months ago

jahorton commented 5 months ago

As originally listed on https://github.com/keymanapp/keyman/issues/5673:

OS integration:

While we aren't doing predictive-text for those platforms quite yet, the sentiment is real and is something we can address for Android and iOS:

This would allow us to include a user's contacts (names) and such as predictive-text suggestions. Names generally aren't in lexical models, yet can often be frequently used - as a result, this would be quite helpful to users.

jahorton commented 4 months ago

According to https://ioactive.com/discovering-and-exploiting-a-vulnerability-in-androids-personal-dictionary/, there used to a permission we'd need to request when running under Android, but now it supposedly checks to see if the request is coming from an IME or spellchecker... somehow. I've followed the guide stuff I could see in the original link above, and I'm able to avoid it throwing errors... but so far, I'm only ever getting an empty dictionary.

jahorton commented 4 months ago

I figured I'd try a similar approach out, but with contacts data. The documentation I've found suggests that contact names are not automatically included in the user dictionary, even if I were able to get data from it, so we'd need to do so separately. It does require requesting additional permission - permission that the user needs to enable, as far as I can see - but I have had success with it.

To be clear, after adding the permission request within the Android manifest, I had to toggle it manually within the settings menu for the contacts-data requests to work. Permissions name: android.permission.READ_CONTACTS.

Documentation re: the data source:

https://developer.android.com/identity/providers/contacts-provider#DataBasics

Requesting ContactsContract.Data.DISPLAY_NAME (querying against ContactsContract.Data.CONTENT_URI) will provide the full name of each of the user's contacts. I haven't (yet) found "given" vs "family" sections, which would be useful for languages like Khmer and Thai, but it's certainly a start.

jahorton commented 4 months ago

Found the incantations to get the name split into components, but something's a bit off at the moment.

ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME (and .FAMILY_NAME) give us the desired split.

That said, for some reason, when I query for these components, there's a spontaneous + malformed extra row that gets returned from the query. When just querying the DISPLAY_NAME, I don't have this issue. I haven't been able to determine a reliable 'filter' I can use to handle this yet.

jahorton commented 4 months ago

As for iOS, we can make a request for the lexicon both in-app and in system-keyboard mode, but we'll only get a UILexicon response when in system-keyboard mode. We're given an empty one when in-app.

That said, what we do get looks very straightforward to work with. Note that there doesn't seem to be any notion of probability or frequency associated with its values.

jahorton commented 4 months ago

OK, I figured out what's needed for filtering with Android.

There is a column within the table queriable by ContactsContract.Data.CONTENT_URI corresponding to the value held by ContactsContract.Data.MIMETYPE. From there, ContactsContract.CommonDataKinds.____.CONTENT_ITEM_TYPE (fill in the blank with any child type of CommonDataKinds) holds a matching value to the row's entry for this column if the row holds data of its type. That's the filter we can use to determine whether or not the row holds "structured name" data, and from there we can use it to build a "contact lexicon" or similar.

We are able to query this data in both in-app and system-keyboard modes.

jahorton commented 3 months ago

Interesting limitation to consider / design question: how do we search-term-key user dictionary entries?

This can cause some trickiness, as the on-device user dictionary... doesn't exactly have a pre-defined search-term keying function. We'd likely want to re-use the one from whatever lexical model is active... but that's buried within the model itself, which is only loaded within the predictive-text worker.

Toward my current design to address this:

@mcdurdin has mentioned an alternate idea, though it comes with its own notable caveats: "why not just search the two separately and combine results after?"


So, there are two main approaches in view... and there's actually a third I thought of originally but had eliminated as a possibility. It's probably worth documenting all of them, as whichever ones we reject now might end up having uses elsewhere down the road. So...

Model blending / traversal compositing

That is, merging the results from two or more models into one at runtime, then searching over that.

Parallel model searching

@mcdurdin's proposition - "Why blend? Just search separately, then merge."

Live model editing:

A third possibility that I'd previously eliminated from consideration - Why stop at blending the results? Go for a full, complete blending!

jahorton commented 3 months ago

I'm starting to think the "Parallel model searching" route might actually be the optimal way forward - being able to drop the "same search-term keying" requirement may actually be a pretty big win.

I only realized some of the notable performance-time caveats to "traversal compositing" once I got far enough into its implementation. Still, I'm glad I worked out the needed logic for it - it was a bit tough, but it was a fun problem with a solution that will likely see use in some form down the road, even if not now.