mirror of
https://github.com/amnesica/KryptEY.git
synced 2025-11-11 11:55:02 -05:00
Initial commit
This commit is contained in:
commit
07abe383cd
1056 changed files with 69364 additions and 0 deletions
75
.gitignore
vendored
Normal file
75
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
45
HELP.md
Normal file
45
HELP.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Help
|
||||
|
||||
## How to start a chat?
|
||||
|
||||
- Invite a chat partner by clicking on the contact list and sending them your invite message. The
|
||||
invite message will be directly placed in the messenger's textfield.
|
||||
- Your chat partner has to add you to their contact list and then send you an encrypted message.
|
||||
Copy this message to your clipboard and click on the "decrypt" button.
|
||||
- A new context menu will open where you have to save the name of the chat partner. Then click on
|
||||
the "done" button. The contact is now automatically selected.
|
||||
|
||||
## Someone sent me an invite message. What do I have to do?
|
||||
|
||||
- Copy the invite message to your clipboard and click on the "decrypt" button.
|
||||
- A new context menu will open where you have to save the name of the chat partner. Then click on
|
||||
the "done" button.
|
||||
- Select your chat partner via the contact list and send them an encrypted message.
|
||||
|
||||
## How do I send/encrypt a message?
|
||||
|
||||
- If you have already added your chat partner as a contact, select them from the contact list and
|
||||
then write your message in the KryptEY text field.
|
||||
- Then click on the "encrypt" button. The message will be placed encrypted in the text field of your
|
||||
messenger and you can send it.
|
||||
- If you haven't added your chat partner yet, see "How to start a chat?"
|
||||
|
||||
## How do I receive/decrypt a message?
|
||||
|
||||
- Select your chat partner from the contact list and copy the message to your clipboard. Then click
|
||||
on the "decrypt" button.
|
||||
- The message will be displayed in the KryptEY text field.
|
||||
|
||||
## How can I see past messages with my chat partner?
|
||||
|
||||
- Select your chat partner from the contact list and then click on the "messege log" button in the
|
||||
main view.
|
||||
- Note that if you delete the contact, the message history will be deleted too.
|
||||
|
||||
## How can I verify that my chat partner is who he claims to be?
|
||||
|
||||
- Next to the name of your chat partner in the contact list is a verified/unverified symbol.
|
||||
- Click on the symbol and your shared security number will appear. Compare the number with your chat
|
||||
partner's number.
|
||||
- If they match, click the "done" button and your contact will be marked as verified.
|
||||
|
||||
112
KRYPTEY.md
Normal file
112
KRYPTEY.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# KryptEY
|
||||
|
||||
KryptEY was created by [mellitopia](https://github.com/mellitopia)
|
||||
and [amnesica](https://github.com/amnesica).
|
||||
|
||||
We implemented a stand alone android keyboard, KryptEY, which enables E2EE encryption of single
|
||||
messages on any Android messenger. It is based on
|
||||
the [Simple Keyboard](https://github.com/rkkr/simple-keyboard). The Android version of
|
||||
the [Signal Protocol](https://mvnrepository.com/artifact/org.signal/libsignal-android) is used for
|
||||
the E2EE functionality.
|
||||
|
||||
The keyboard provides the functionality to encrypt and decrypt messages, is independent of a
|
||||
messenger app and does not require a server for the key exchange. The keyboard includes a separate
|
||||
text field for entering the message that is to be encrypted. After selecting the receiver, the
|
||||
message is sent to the text field of the used messenger and then sent to the chat partner as usual.
|
||||
The chat partner copies the message to the clipboard, the application recognises the KryptEY message
|
||||
as well as the sender and offers the option to decrypt the ciphertext message. The decrypted message
|
||||
is displayed in the KryptEY text field and saved in the message history. Further, chat partners can
|
||||
be created and deleted through the application and their security number can be verified by
|
||||
comparing it on both end devices. Encrypted and decrypted messages are stored in a message history
|
||||
for later viewing and there is a Q&A section that helps with questions about the keyboard and its
|
||||
functionalities.
|
||||
|
||||
The elliptic curve X25519 with SHA-512 is used in the X3DH Key Agreement Protocol from the applied
|
||||
Signal library. The hash function SHA-256 is used for the various chains and AES-256 with CBC (
|
||||
Pkcs#7) is used for the encryption of the messages. SHA-512 is also used to generate the
|
||||
fingerprint, the representation of the public key used for encryption.
|
||||
|
||||
## Initialization
|
||||
|
||||
After installing the app, the Signal Protocol is initialised on the device. For this purpose,
|
||||
a `SignalProtocolAddress` consisting of a randomised UUID and an arbitrary device id is created.
|
||||
Further, an identity key, two one-time prekeys and a signed prekey are created. From this
|
||||
information, various stores are created to man- age the protocol: the `IdentityKeyStore`
|
||||
, `PreKeyMetadataStore`, `PreKeyStore`, `SenderKeyStore`, `SessionStore`, `SignalProtcolStore`, and
|
||||
the `SignedPreKeyStore`. All protocol information is stored serialised in the application’s
|
||||
SharedPreferences. The Jackson library is used for this purpose. All the information together forms
|
||||
an account on the device, with the `SignalProtocolAddress` identifying the user.
|
||||
|
||||
In addition, there are four different message types in KryptEY for exchanging keys and ciphertext
|
||||
messages between chat partners.
|
||||
|
||||
1. `PreKeyResponse`: to send the `PreKeyBundle` (Invite message)
|
||||
2. `PreKeySignalMessage`: to send a ciphertext and `PreKeyBundle` after establishing the session on
|
||||
one’s side.
|
||||
3. `SignalMessage`: to send a ciphertext
|
||||
4. `PreKeyResponse`+`SignalMessage`: to send a ciphertext with new `PreKeyBundle` information to
|
||||
update the session
|
||||
|
||||
## Session Establishment and Key Management
|
||||
|
||||
To exchange messages, a session must first be initialised on both end devices. If Bob wants to
|
||||
communicate with Alice, Bob needs Alice’s `PreKeyBundle` to establish the session on his side. Alice
|
||||
sends this via an invite message, which contains her `PreKeyBundle` data (`PreKeyResponse`). This
|
||||
allows Bob to add Alice as a contact within the keyboard and establish a session on his side. Bob
|
||||
can now send an initial encrypted message to Alice. Since Alice has not yet initialised a session
|
||||
with Bob, this message is a `PreKeySignalMessage` and contains additional information besides the
|
||||
actual ciphertext message so that Alice can establish a session with Bob. After Alice has also added
|
||||
Bob as a contact and created a session, she can now decrypt the message Bob sent. At this point,
|
||||
both parties have established a session and can send messages to each other. These messages
|
||||
are `SignalMessages`.
|
||||
|
||||
The one-time prekeys used to establish the sessions are renewed after each use, while the identity
|
||||
key is never renewed. After 30 days the signed prekeys are rotated. The old signed prekey is deleted
|
||||
after 2 days. After such rotation, an updated `PreKeyBundle` is sent together with an encrypted
|
||||
message with which the receiver, e.g. Bob, can update his session with Alice (`PreKeyResponse`
|
||||
+`SignalMessage`). This combination of messages eliminates the need for a separate reading of the
|
||||
renewed KeyBundle information. For the user, the message appears like a normal `SignalMessage`. The
|
||||
session management with the manually created `PreKeyResponse` is necessary, because no server is
|
||||
used for the exchange of the `PreKeyBundles`.
|
||||
|
||||
In addition to the `PreKeySignalMessage` and `SignalMessage`, which are also used by the Signal app,
|
||||
KryptEY uses the `PreKeyResponse` and `PreKeyResponse`+`SignalMessage`. As previously mentioned,
|
||||
these are necessary to guarantee the Signal Protocol without the use of a server. Further, in the
|
||||
Signal app 100 one-time prekeys are created, while in KryptEY only 2 one-time prekeys are created.
|
||||
These one-time prekeys are replaced after each use, which eliminates the need for time-consuming key
|
||||
management of 100 one-time prekeys. Also, unlike Signal, KryptEY does not use telephone numbers to
|
||||
identify users. Instead, randomised UUIDs are used. This could contribute to an increase of the
|
||||
privacy of the users, since no telephone numbers are used to identify the users.tween chat partners.
|
||||
|
||||
## MessageEnvelope and Message Encoding
|
||||
|
||||
To send a message, all information is collected in a `MessageEnvelope` and then sent as plain JSON
|
||||
or hidden in a decoy message. Depending on the message type, this envelope contains
|
||||
the `PreKeyResponse`, the `CipherTextMessage` (`PreKeySignalMessage` or `SignalMessage`) as a byte
|
||||
array, the type of the `CipherTextMessage` (`PreKeySignalMessage` or `SignalMessage`), a timestamp
|
||||
and the `SignalProtocolAddress`.
|
||||
|
||||
There are two different encoding modes in KryptEY, raw mode and fairytale mode. Messages can be sent
|
||||
as a JSON array (raw mode) or hidden in a decoy message (fairytale mode) to make the conversation
|
||||
look inconspicuous. In the latter, the encrypted message is hidden in invisible, non-printable
|
||||
Unicode characters. To keep the message size as small as possible, the JSON is minified, i.e. all
|
||||
spaces and paragraphs are removed and the key values of the JSON are replaced by abbreviated key
|
||||
values, e.g. ”preKeyResponse” becomes ”pR”. After that, the string is compressed with GZIP and
|
||||
converted into a binary string. When converting to invisible Unicode characters, 4 bits are always
|
||||
mapped to an invisible Unicode character like U+200C (ZERO WIDTH NON-JOINER). There are 16 invisible
|
||||
Unicode characters to choose from, covering all combinations from 0000-1111. The invisible Unicode
|
||||
string is then placed after an arbitrary sentence from one of the two available
|
||||
fairytales [Cinderella](https://www.cs.cmu.edu/∼spok/grimmtmp/016.txt)
|
||||
and [Rapunzel](https://www.cs.cmu.edu/∼spok/grimmtmp/009.txt) and can be sent to the application.
|
||||
After receiving, the invisible Unicode characters are extracted from the message, converted to a
|
||||
binary string, decompressed, and deminified. Then the message can be read by the app. The invisible
|
||||
characters are included in the transmitted messages, which can cause problems in some messengers,
|
||||
unfortunately (see Limitations in the README).
|
||||
|
||||
## Additional Information
|
||||
|
||||
The keyboard only needs the ”VIBRATE” permission to enable vibration after key press. Unlike
|
||||
the [Android Open Source Project keyboard](https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/refs/heads/master/java/AndroidManifest.xml)
|
||||
, the application does not require any sensitive permissions such as access to external storage or
|
||||
contacts. Internet access is also not needed. At least Android 8.0 (API 26) is required and the
|
||||
application has been licensed with GPL-3.0.
|
||||
674
LICENSE.md
Normal file
674
LICENSE.md
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
3
PRIVACY.md
Normal file
3
PRIVACY.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<h1 align="center">KryptEY - Privacy</h1>
|
||||
|
||||
All data remains on the device and is not send anywhere! Enjoy private communication!
|
||||
106
README.md
Normal file
106
README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<p align="center">
|
||||
<img src="static/logo/logo.png" height="150" title="KryptEY Logo">
|
||||
</p>
|
||||
|
||||
<h1 align="center">KryptEY - Secure E2EE communication</h1>
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Keyboard for secure end-to-end-encrypted messages through the signal protocol in any messenger.
|
||||
Communicate securely and independent, regardless of the legal situation or whether messengers use
|
||||
E2EE. No server needed.
|
||||
|
||||
KryptEY was created by [mellitopia](https://github.com/mellitopia)
|
||||
and [amnesica](https://github.com/amnesica).
|
||||
|
||||
## Motivation
|
||||
|
||||
Breaking of end-to-end encryption (E2EE) by laws such as the planned EU chat control is an ongoing
|
||||
issue. Content in messengers that use E2EE, such as Whatsapp or Signal, could thus be monitored by
|
||||
third parties. E2EE is often, but not always, standard in messengers. There are proven methods for
|
||||
E2EE such as PGP. However, these methods are sometimes cumbersomely integrated and require a lot of
|
||||
effort to use.
|
||||
|
||||
KryptEY is an Android keyboard that implements the Signal protocol. The keyboard works
|
||||
messenger-independently and both the X3DH Key Agreement Protocol and the Double Ratchet Algorithm
|
||||
work without a server, thus it enables a highly independent use of the protocol.
|
||||
|
||||
## Features
|
||||
|
||||
Based upon the [Simple Keyboard](https://github.com/rkkr/simple-keyboard) KryptEY adds a view above
|
||||
the Keyboard for the E2EE functionality.
|
||||
|
||||
- use E2EE through Signal Protocol in any messenger
|
||||
- encryption/decryption of messages
|
||||
- enter message through separate text field in keyboard
|
||||
- use clipboard to read messages
|
||||
- manage contacts in own contact list in keyboard
|
||||
- message log to view sent/received messages
|
||||
- send messages as plain JSON (raw mode) or hidden in a decoy text (fairytale mode)
|
||||
- verification of E2EE functionality via fingerprint
|
||||
- Q&A View helps with questions
|
||||
|
||||
See [this](/KRYPTEY.md) document for further information on how KryptEY is working.
|
||||
|
||||
## Demo
|
||||
|
||||
Conversation between Alice (right) and Bob (left) in the Signal Messenger using KryptEY.
|
||||
|
||||
<div style="display:flex;" align="center">
|
||||
<img alt="App image" src="static/screenshots/demo.gif" width="80%">
|
||||
</div>
|
||||
|
||||
## Download
|
||||
|
||||
<a href='https://github.com/amnesica/KryptEY/releases'><img alt='Get it on Github' src='static/github/get-it-on-github.png' height='60'/></a>
|
||||
|
||||
If you need instructions on how to use the app, see our help [here](/HELP.md)
|
||||
|
||||
## Privacy
|
||||
|
||||
Read our privacy statement [here](/PRIVACY.md)
|
||||
|
||||
## Permissions
|
||||
|
||||
- VIBRATE: Required for vibrations on key press
|
||||
|
||||
## Security
|
||||
|
||||
The existing security properties for the Signal Protocol are also valid for the keyboard.
|
||||
|
||||
The elliptic curve X25519 with SHA-512 is used in the X3DH Key Agreement Protocol from the applied
|
||||
Signal library. The hash function SHA-256 is used for the various chains and AES-256 with CBC (
|
||||
Pkcs#7) is used for the encryption of the messages. SHA-512 is also used to generate the
|
||||
fingerprint, the representation of the public key used for encryption.
|
||||
|
||||
## Limitations
|
||||
|
||||
The keyboard was designed as a POC and only allows 1-to-1 conversations. However, the application
|
||||
can also be used in a group chat to a limited extent. Here, a message can be directed to a
|
||||
specific chat partner and not to all people. Other participants of the group chat cannot decrypt
|
||||
the message.
|
||||
|
||||
Text messages in Telegram are getting copied as HTML and not as plain text. When decoding the
|
||||
message with the fairytale mode the copied message is compromised and can't be read properly.
|
||||
Therefore, it can't be decoded at all. However, the raw mode works properly. When using KryptEY
|
||||
with Telegram we recommend the raw mode.
|
||||
|
||||
Some messengers like Threema only allows up to 3500 bytes per message. Therefore, different
|
||||
character input limitations apply. To stay under the 3500 bytes limit, only 500 characters are
|
||||
allowed for raw and fairytale mode. For convenience these limitation applies for all messengers.
|
||||
|
||||
## Used libraries
|
||||
|
||||
- [Signal Protocol (android)](https://github.com/signalapp/libsignal)
|
||||
- [Jackson](https://github.com/FasterXML/jackson)
|
||||
- [Protobuf (lite)](https://github.com/protocolbuffers/protobuf/tree/main/java)
|
||||
- [JUnit4](https://github.com/junit-team/junit4)
|
||||
|
||||
## Credits
|
||||
|
||||
- [AOSP Keyboard](https://android.googlesource.com/platform/packages/inputmethods/LatinIME/)
|
||||
- [Simple Keyboard](https://github.com/rkkr/simple-keyboard)
|
||||
- [OpenBoard](https://github.com/openboard-team/openboard)
|
||||
- [FlorisBoard](https://github.com/florisboard/florisboard)
|
||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
29
app/build.gradle
Normal file
29
app/build.gradle
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion '33.0.0'
|
||||
defaultConfig {
|
||||
applicationId "com.amnesica.kryptey"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
versionCode 24
|
||||
versionName "0.1.5"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
debuggable false
|
||||
renderscriptDebuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.signal:libsignal-android:0.21.1'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.21.12'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
19
app/proguard-rules.pro
vendored
Normal file
19
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/com.amnesica/Android/Sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keep class com.amnesica.kryptey.inputmethod.R
|
||||
54
app/src/main/AndroidManifest.xml
Normal file
54
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.amnesica.kryptey.inputmethod">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/english_ime_name"
|
||||
android:supportsRtl="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<service
|
||||
android:name="com.amnesica.kryptey.inputmethod.latin.LatinIME"
|
||||
android:directBootAware="false"
|
||||
android:exported="false"
|
||||
android:label="@string/english_ime_name"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.view.im"
|
||||
android:resource="@xml/method" />
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name="com.amnesica.kryptey.inputmethod.latin.settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/english_ime_name"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/platformSettingsTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="com.amnesica.kryptey.inputmethod.latin.SystemBroadcastReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.compat;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class EditorInfoCompatUtils {
|
||||
private EditorInfoCompatUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
public static String imeActionName(final int imeOptions) {
|
||||
final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION;
|
||||
switch (actionId) {
|
||||
case EditorInfo.IME_ACTION_UNSPECIFIED:
|
||||
return "actionUnspecified";
|
||||
case EditorInfo.IME_ACTION_NONE:
|
||||
return "actionNone";
|
||||
case EditorInfo.IME_ACTION_GO:
|
||||
return "actionGo";
|
||||
case EditorInfo.IME_ACTION_SEARCH:
|
||||
return "actionSearch";
|
||||
case EditorInfo.IME_ACTION_SEND:
|
||||
return "actionSend";
|
||||
case EditorInfo.IME_ACTION_NEXT:
|
||||
return "actionNext";
|
||||
case EditorInfo.IME_ACTION_DONE:
|
||||
return "actionDone";
|
||||
case EditorInfo.IME_ACTION_PREVIOUS:
|
||||
return "actionPrevious";
|
||||
default:
|
||||
return "actionUnknown(" + actionId + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static Locale getPrimaryHintLocale(final EditorInfo editorInfo) {
|
||||
if (editorInfo == null) {
|
||||
return null;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleList localeList = editorInfo.hintLocales;
|
||||
if (localeList != null && !localeList.isEmpty())
|
||||
return localeList.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2021 Raimondas Rimkus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.compat;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MenuItemIconColorCompat {
|
||||
/**
|
||||
* Set a menu item's icon to matching text color.
|
||||
*
|
||||
* @param menuItem the menu item that should change colors.
|
||||
* @param actionBar target ActionBar.
|
||||
*/
|
||||
public static void matchMenuIconColor(final View view, final MenuItem menuItem, final ActionBar actionBar) {
|
||||
ArrayList<View> views = new ArrayList<>();
|
||||
|
||||
view.getRootView().findViewsWithText(views, actionBar.getTitle(), View.FIND_VIEWS_WITH_TEXT);
|
||||
if (views.size() == 1 && views.get(0) instanceof TextView) {
|
||||
int color = ((TextView) views.get(0)).getCurrentTextColor();
|
||||
setIconColor(menuItem, color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a menu item's icon to specific color.
|
||||
*
|
||||
* @param menuItem the menu item that should change colors.
|
||||
* @param color the color that the icon should be changed to.
|
||||
*/
|
||||
private static void setIconColor(final MenuItem menuItem, final int color) {
|
||||
if (menuItem != null) {
|
||||
Drawable drawable = menuItem.getIcon();
|
||||
if (drawable != null) {
|
||||
drawable.mutate();
|
||||
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2020 Raimondas Rimkus
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.compat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceManagerCompat {
|
||||
public static Context getDeviceContext(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return context.createDeviceProtectedStorageContext();
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
public static SharedPreferences getDeviceSharedPreferences(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(getDeviceContext(context));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.compat;
|
||||
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
public class ViewOutlineProviderCompatUtils {
|
||||
private ViewOutlineProviderCompatUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
public interface InsetsUpdater {
|
||||
void setInsets(final InputMethodService.Insets insets);
|
||||
}
|
||||
|
||||
private static final InsetsUpdater EMPTY_INSETS_UPDATER = new InsetsUpdater() {
|
||||
@Override
|
||||
public void setInsets(final InputMethodService.Insets insets) {
|
||||
}
|
||||
};
|
||||
|
||||
public static InsetsUpdater setInsetsOutlineProvider(final View view) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return EMPTY_INSETS_UPDATER;
|
||||
}
|
||||
return ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.compat;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Outline;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class ViewOutlineProviderCompatUtilsLXX {
|
||||
private ViewOutlineProviderCompatUtilsLXX() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
static InsetsUpdater setInsetsOutlineProvider(final View view) {
|
||||
final InsetsOutlineProvider provider = new InsetsOutlineProvider(view);
|
||||
view.setOutlineProvider(provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static class InsetsOutlineProvider extends ViewOutlineProvider
|
||||
implements InsetsUpdater {
|
||||
private final View mView;
|
||||
private static final int NO_DATA = -1;
|
||||
private int mLastVisibleTopInsets = NO_DATA;
|
||||
|
||||
public InsetsOutlineProvider(final View view) {
|
||||
mView = view;
|
||||
view.setOutlineProvider(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInsets(final InputMethodService.Insets insets) {
|
||||
final int visibleTopInsets = insets.visibleTopInsets;
|
||||
if (mLastVisibleTopInsets != visibleTopInsets) {
|
||||
mLastVisibleTopInsets = visibleTopInsets;
|
||||
mView.invalidateOutline();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(final View view, final Outline outline) {
|
||||
if (mLastVisibleTopInsets == NO_DATA) {
|
||||
// Call default implementation.
|
||||
ViewOutlineProvider.BACKGROUND.getOutline(view, outline);
|
||||
return;
|
||||
}
|
||||
// TODO: Revisit this when floating/resize keyboard is supported.
|
||||
outline.setRect(
|
||||
view.getLeft(), mLastVisibleTopInsets, view.getRight(), view.getBottom());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.amnesica.kryptey.inputmethod.crypto;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
@Deprecated
|
||||
// Basic AES encryption class for testing
|
||||
public class AESCrypt {
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
public static String encrypt(CharSequence value, CharSequence password) throws Exception {
|
||||
Key key = generateKey(password);
|
||||
Cipher cipher = Cipher.getInstance(AESCrypt.ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] encryptedByteValue = cipher.doFinal(value.toString().getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.encodeToString(encryptedByteValue, Base64.DEFAULT);
|
||||
}
|
||||
|
||||
public static String decrypt(CharSequence value, CharSequence password) throws Exception {
|
||||
Key key = generateKey(password);
|
||||
Cipher cipher = Cipher.getInstance(AESCrypt.ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
byte[] decryptedValue64 = Base64.decode(value.toString(), Base64.DEFAULT);
|
||||
byte[] decryptedByteValue = cipher.doFinal(decryptedValue64);
|
||||
return new String(decryptedByteValue, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Key generateKey(final CharSequence password) {
|
||||
Key key = new SecretKeySpec(password.toString().getBytes(), AESCrypt.ALGORITHM);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.event;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
|
||||
/**
|
||||
* Class representing a generic input event as handled by Latin IME.
|
||||
* <p>
|
||||
* This contains information about the origin of the event, but it is generalized and should
|
||||
* represent a software keypress, hardware keypress, or d-pad move alike.
|
||||
* Very importantly, this does not necessarily result in inputting one character, or even anything
|
||||
* at all - it may be a dead key, it may be a partial input, it may be a special key on the
|
||||
* keyboard, it may be a cancellation of a keypress (e.g. in a soft keyboard the finger of the
|
||||
* user has slid out of the key), etc. It may also be a batch input from a gesture or handwriting
|
||||
* for example.
|
||||
* The combiner should figure out what to do with this.
|
||||
*/
|
||||
public class Event {
|
||||
// Should the types below be represented by separate classes instead? It would be cleaner
|
||||
// but probably a bit too much
|
||||
// An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard.
|
||||
final public static int EVENT_TYPE_NOT_HANDLED = 0;
|
||||
// A key press that is part of input, for example pressing an alphabetic character on a
|
||||
// hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later
|
||||
// through combination.
|
||||
final public static int EVENT_TYPE_INPUT_KEYPRESS = 1;
|
||||
// A toggle event is triggered by a key that affects the previous character. An example would
|
||||
// be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with
|
||||
// repeated presses.
|
||||
final public static int EVENT_TYPE_TOGGLE = 2;
|
||||
// A mode event instructs the combiner to change modes. The canonical example would be the
|
||||
// hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard
|
||||
// if handled at the combiner level.
|
||||
final public static int EVENT_TYPE_MODE_KEY = 3;
|
||||
// An event corresponding to a string generated by some software process.
|
||||
final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6;
|
||||
// An event corresponding to a cursor move
|
||||
final public static int EVENT_TYPE_CURSOR_MOVE = 7;
|
||||
|
||||
// 0 is a valid code point, so we use -1 here.
|
||||
final public static int NOT_A_CODE_POINT = -1;
|
||||
// -1 is a valid key code, so we use 0 here.
|
||||
final public static int NOT_A_KEY_CODE = 0;
|
||||
|
||||
final private static int FLAG_NONE = 0;
|
||||
// This event is coming from a key repeat, software or hardware.
|
||||
final private static int FLAG_REPEAT = 0x2;
|
||||
// This event has already been consumed.
|
||||
final private static int FLAG_CONSUMED = 0x4;
|
||||
|
||||
final private int mEventType; // The type of event - one of the constants above
|
||||
// The code point associated with the event, if relevant. This is a unicode code point, and
|
||||
// has nothing to do with other representations of the key. It is only relevant if this event
|
||||
// is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point
|
||||
// associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when
|
||||
// it's not relevant.
|
||||
final public int mCodePoint;
|
||||
|
||||
final public CharSequence mText;
|
||||
|
||||
// The key code associated with the event, if relevant. This is relevant whenever this event
|
||||
// has been triggered by a key press, but not for a gesture for example. This has conceptually
|
||||
// no link to the code point, although keys that enter a straight code point may often set
|
||||
// this to be equal to mCodePoint for convenience. If this is not a key, this must contain
|
||||
// NOT_A_KEY_CODE.
|
||||
final public int mKeyCode;
|
||||
|
||||
// Coordinates of the touch event, if relevant. If useful, we may want to replace this with
|
||||
// a MotionEvent or something in the future. This is only relevant when the keypress is from
|
||||
// a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the
|
||||
// future or some other awesome sauce.
|
||||
final public int mX;
|
||||
final public int mY;
|
||||
|
||||
// Some flags that can't go into the key code. It's a bit field of FLAG_*
|
||||
final private int mFlags;
|
||||
|
||||
// The next event, if any. Null if there is no next event yet.
|
||||
final public Event mNextEvent;
|
||||
|
||||
// This method is private - to create a new event, use one of the create* utility methods.
|
||||
private Event(final int type, final CharSequence text, final int codePoint, final int keyCode,
|
||||
final int x, final int y, final int flags,
|
||||
final Event next) {
|
||||
mEventType = type;
|
||||
mText = text;
|
||||
mCodePoint = codePoint;
|
||||
mKeyCode = keyCode;
|
||||
mX = x;
|
||||
mY = y;
|
||||
mFlags = flags;
|
||||
mNextEvent = next;
|
||||
}
|
||||
|
||||
public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode,
|
||||
final int x, final int y, final boolean isKeyRepeat) {
|
||||
return new Event(EVENT_TYPE_INPUT_KEYPRESS, null, codePoint, keyCode, x, y,
|
||||
isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an input event with a CharSequence. This is used by some software processes whose
|
||||
* output is a string, possibly with styling. Examples include press on a multi-character key,
|
||||
* or combination that outputs a string.
|
||||
*
|
||||
* @param text the CharSequence associated with this event.
|
||||
* @param keyCode the key code, or NOT_A_KEYCODE if not applicable.
|
||||
* @return an event for this text.
|
||||
*/
|
||||
public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) {
|
||||
return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode,
|
||||
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE,
|
||||
FLAG_NONE, null /* next */);
|
||||
}
|
||||
|
||||
// Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys
|
||||
// that result in input like letters or space.
|
||||
public boolean isFunctionalKeyEvent() {
|
||||
// This logic may need to be refined in the future
|
||||
return NOT_A_CODE_POINT == mCodePoint;
|
||||
}
|
||||
|
||||
public boolean isKeyRepeat() {
|
||||
return 0 != (FLAG_REPEAT & mFlags);
|
||||
}
|
||||
|
||||
public boolean isConsumed() {
|
||||
return 0 != (FLAG_CONSUMED & mFlags);
|
||||
}
|
||||
|
||||
public CharSequence getTextToCommit() {
|
||||
if (isConsumed()) {
|
||||
return ""; // A consumed event should input no text.
|
||||
}
|
||||
switch (mEventType) {
|
||||
case EVENT_TYPE_MODE_KEY:
|
||||
case EVENT_TYPE_NOT_HANDLED:
|
||||
case EVENT_TYPE_TOGGLE:
|
||||
case EVENT_TYPE_CURSOR_MOVE:
|
||||
return "";
|
||||
case EVENT_TYPE_INPUT_KEYPRESS:
|
||||
return StringUtils.newSingleCodePointString(mCodePoint);
|
||||
case EVENT_TYPE_SOFTWARE_GENERATED_STRING:
|
||||
return mText;
|
||||
}
|
||||
throw new RuntimeException("Unknown event type: " + mEventType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.event;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
|
||||
|
||||
/**
|
||||
* An object encapsulating a single transaction for input.
|
||||
*/
|
||||
public class InputTransaction {
|
||||
// UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later,
|
||||
// it's because something will change that we can't evaluate now, which means that even if we
|
||||
// re-evaluate now we'll have to do it again later. The only case where that wouldn't apply
|
||||
// would be if we needed to update now to find out the new state right away, but then we
|
||||
// can't do it with this deferred mechanism anyway.
|
||||
public static final int SHIFT_NO_UPDATE = 0;
|
||||
public static final int SHIFT_UPDATE_NOW = 1;
|
||||
public static final int SHIFT_UPDATE_LATER = 2;
|
||||
|
||||
// Initial conditions
|
||||
public final SettingsValues mSettingsValues;
|
||||
|
||||
// Outputs
|
||||
private int mRequiredShiftUpdate = SHIFT_NO_UPDATE;
|
||||
|
||||
public InputTransaction(final SettingsValues settingsValues) {
|
||||
mSettingsValues = settingsValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that this transaction requires some type of shift update.
|
||||
*
|
||||
* @param updateType What type of shift update this requires.
|
||||
*/
|
||||
public void requireShiftUpdate(final int updateType) {
|
||||
mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets what type of shift update this transaction requires.
|
||||
*
|
||||
* @return The shift update type.
|
||||
*/
|
||||
public int getRequiredShiftUpdate() {
|
||||
return mRequiredShiftUpdate;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,957 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_SHIFT;
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL;
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyDrawParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeySpecParser;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyStyle;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyVisualAttributes;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardIconsSet;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardRow;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.MoreKeySpec;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Class for describing the position and characteristics of a single key in the keyboard.
|
||||
*/
|
||||
public class Key implements Comparable<Key> {
|
||||
/**
|
||||
* The key code (unicode or custom code) that this key generates.
|
||||
*/
|
||||
private final int mCode;
|
||||
|
||||
/**
|
||||
* Label to display
|
||||
*/
|
||||
private final String mLabel;
|
||||
/**
|
||||
* Hint label to display on the key in conjunction with the label
|
||||
*/
|
||||
private final String mHintLabel;
|
||||
/**
|
||||
* Flags of the label
|
||||
*/
|
||||
private final int mLabelFlags;
|
||||
private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
|
||||
private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
|
||||
private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
|
||||
// Font typeface specification.
|
||||
private static final int LABEL_FLAGS_FONT_MASK = 0x30;
|
||||
private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
|
||||
private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
|
||||
private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
|
||||
// Start of key text ratio enum values
|
||||
private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
|
||||
private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
|
||||
private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
|
||||
private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
|
||||
private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
|
||||
// End of key text ratio mask enum values
|
||||
private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
|
||||
private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
|
||||
// The bit to calculate the ratio of key label width against key width. If autoXScale bit is on
|
||||
// and autoYScale bit is off, the key label may be shrunk only for X-direction.
|
||||
// If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled.
|
||||
private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
|
||||
private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
|
||||
private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
|
||||
| LABEL_FLAGS_AUTO_Y_SCALE;
|
||||
private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
|
||||
private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
|
||||
private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
|
||||
private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
|
||||
private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
|
||||
private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
|
||||
|
||||
/**
|
||||
* Icon to display instead of a label. Icon takes precedence over a label
|
||||
*/
|
||||
private final int mIconId;
|
||||
|
||||
/**
|
||||
* Width of the key, excluding the padding
|
||||
*/
|
||||
private final int mWidth;
|
||||
/**
|
||||
* Height of the key, excluding the padding
|
||||
*/
|
||||
private final int mHeight;
|
||||
/**
|
||||
* Exact theoretical width of the key, excluding the padding
|
||||
*/
|
||||
private final float mDefinedWidth;
|
||||
/**
|
||||
* Exact theoretical height of the key, excluding the padding
|
||||
*/
|
||||
private final float mDefinedHeight;
|
||||
/**
|
||||
* X coordinate of the top-left corner of the key in the keyboard layout, excluding the
|
||||
* padding.
|
||||
*/
|
||||
private final int mX;
|
||||
/**
|
||||
* Y coordinate of the top-left corner of the key in the keyboard layout, excluding the
|
||||
* padding.
|
||||
*/
|
||||
private final int mY;
|
||||
/**
|
||||
* Hit bounding box of the key
|
||||
*/
|
||||
private final Rect mHitbox = new Rect();
|
||||
|
||||
/**
|
||||
* More keys. It is guaranteed that this is null or an array of one or more elements
|
||||
*/
|
||||
private final MoreKeySpec[] mMoreKeys;
|
||||
/**
|
||||
* More keys column number and flags
|
||||
*/
|
||||
private final int mMoreKeysColumnAndFlags;
|
||||
private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff;
|
||||
// If this flag is specified, more keys keyboard should have the specified number of columns.
|
||||
// Otherwise more keys keyboard should have less than or equal to the specified maximum number
|
||||
// of columns.
|
||||
private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100;
|
||||
// If this flag is specified, the order of more keys is determined by the order in the more
|
||||
// keys' specification. Otherwise the order of more keys is automatically determined.
|
||||
private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200;
|
||||
private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0;
|
||||
private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER =
|
||||
MORE_KEYS_FLAGS_FIXED_COLUMN;
|
||||
private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER =
|
||||
(MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER);
|
||||
private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
|
||||
private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000;
|
||||
// TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively.
|
||||
private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
|
||||
private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
|
||||
private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
|
||||
private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
|
||||
|
||||
/**
|
||||
* Background type that represents different key background visual than normal one.
|
||||
*/
|
||||
private final int mBackgroundType;
|
||||
public static final int BACKGROUND_TYPE_EMPTY = 0;
|
||||
public static final int BACKGROUND_TYPE_NORMAL = 1;
|
||||
public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
|
||||
public static final int BACKGROUND_TYPE_ACTION = 5;
|
||||
public static final int BACKGROUND_TYPE_SPACEBAR = 6;
|
||||
|
||||
private final int mActionFlags;
|
||||
private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
|
||||
private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
|
||||
private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
|
||||
private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
|
||||
|
||||
private final KeyVisualAttributes mKeyVisualAttributes;
|
||||
private final OptionalAttributes mOptionalAttributes;
|
||||
|
||||
private static final class OptionalAttributes {
|
||||
/**
|
||||
* Text to output when pressed. This can be multiple characters, like ".com"
|
||||
*/
|
||||
public final String mOutputText;
|
||||
public final int mAltCode;
|
||||
|
||||
private OptionalAttributes(final String outputText, final int altCode) {
|
||||
mOutputText = outputText;
|
||||
mAltCode = altCode;
|
||||
}
|
||||
|
||||
public static OptionalAttributes newInstance(final String outputText, final int altCode) {
|
||||
if (outputText == null && altCode == CODE_UNSPECIFIED) {
|
||||
return null;
|
||||
}
|
||||
return new OptionalAttributes(outputText, altCode);
|
||||
}
|
||||
}
|
||||
|
||||
private final int mHashCode;
|
||||
|
||||
/**
|
||||
* The current pressed state of this key
|
||||
*/
|
||||
private boolean mPressed;
|
||||
|
||||
/**
|
||||
* Constructor for a key on <code>MoreKeyKeyboard</code>.
|
||||
*/
|
||||
public Key(final String label, final int iconId, final int code, final String outputText,
|
||||
final String hintLabel, final int labelFlags, final int backgroundType,
|
||||
final float x, final float y, final float width, final float height,
|
||||
final float leftPadding, final float rightPadding, final float topPadding,
|
||||
final float bottomPadding) {
|
||||
mHitbox.set(Math.round(x - leftPadding), Math.round(y - topPadding),
|
||||
Math.round(x + width + rightPadding), Math.round(y + height + bottomPadding));
|
||||
mX = Math.round(x);
|
||||
mY = Math.round(y);
|
||||
mWidth = Math.round(x + width) - mX;
|
||||
mHeight = Math.round(y + height) - mY;
|
||||
mDefinedWidth = width;
|
||||
mDefinedHeight = height;
|
||||
mHintLabel = hintLabel;
|
||||
mLabelFlags = labelFlags;
|
||||
mBackgroundType = backgroundType;
|
||||
// TODO: Pass keyActionFlags as an argument.
|
||||
mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
|
||||
mMoreKeys = null;
|
||||
mMoreKeysColumnAndFlags = 0;
|
||||
mLabel = label;
|
||||
mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED);
|
||||
mCode = code;
|
||||
mIconId = iconId;
|
||||
mKeyVisualAttributes = null;
|
||||
|
||||
mHashCode = computeHashCode(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a key with the given top-left coordinate and extract its attributes from a key
|
||||
* specification string, Key attribute array, key style, and etc.
|
||||
*
|
||||
* @param keySpec the key specification.
|
||||
* @param keyAttr the Key XML attributes array.
|
||||
* @param style the {@link KeyStyle} of this key.
|
||||
* @param params the keyboard building parameters.
|
||||
* @param row the row that this key belongs to. row's x-coordinate will be the right edge of
|
||||
* this key.
|
||||
*/
|
||||
public Key(final String keySpec, final TypedArray keyAttr,
|
||||
final KeyStyle style, final KeyboardParams params,
|
||||
final KeyboardRow row) {
|
||||
// Update the row to work with the new key
|
||||
row.setCurrentKey(keyAttr, isSpacer());
|
||||
|
||||
mDefinedWidth = row.getKeyWidth();
|
||||
mDefinedHeight = row.getKeyHeight();
|
||||
|
||||
final float keyLeft = row.getKeyX();
|
||||
final float keyTop = row.getKeyY();
|
||||
final float keyRight = keyLeft + mDefinedWidth;
|
||||
final float keyBottom = keyTop + mDefinedHeight;
|
||||
|
||||
final float leftPadding = row.getKeyLeftPadding();
|
||||
final float topPadding = row.getKeyTopPadding();
|
||||
final float rightPadding = row.getKeyRightPadding();
|
||||
final float bottomPadding = row.getKeyBottomPadding();
|
||||
|
||||
mHitbox.set(Math.round(keyLeft - leftPadding), Math.round(keyTop - topPadding),
|
||||
Math.round(keyRight + rightPadding), Math.round(keyBottom + bottomPadding));
|
||||
mX = Math.round(keyLeft);
|
||||
mY = Math.round(keyTop);
|
||||
mWidth = Math.round(keyRight) - mX;
|
||||
mHeight = Math.round(keyBottom) - mY;
|
||||
|
||||
mBackgroundType = style.getInt(keyAttr,
|
||||
R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
|
||||
|
||||
mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
|
||||
| row.getDefaultKeyLabelFlags();
|
||||
final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
|
||||
final Locale localeForUpcasing = params.mId.getLocale();
|
||||
int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
|
||||
String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
|
||||
|
||||
// Get maximum column order number and set a relevant mode value.
|
||||
int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER
|
||||
| style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn,
|
||||
params.mMaxMoreKeysKeyboardColumn);
|
||||
int value;
|
||||
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
|
||||
// Override with fixed column order number and set a relevant mode value.
|
||||
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER
|
||||
| (value & MORE_KEYS_COLUMN_NUMBER_MASK);
|
||||
}
|
||||
if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
|
||||
// Override with fixed column order number and set a relevant mode value.
|
||||
moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER
|
||||
| (value & MORE_KEYS_COLUMN_NUMBER_MASK);
|
||||
}
|
||||
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
|
||||
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
|
||||
}
|
||||
if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
|
||||
moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
|
||||
}
|
||||
mMoreKeysColumnAndFlags = moreKeysColumnAndFlags;
|
||||
|
||||
final String[] additionalMoreKeys;
|
||||
if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
|
||||
additionalMoreKeys = null;
|
||||
} else {
|
||||
additionalMoreKeys = style.getStringArray(keyAttr,
|
||||
R.styleable.Keyboard_Key_additionalMoreKeys);
|
||||
}
|
||||
moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
|
||||
if (moreKeys != null) {
|
||||
actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
|
||||
mMoreKeys = new MoreKeySpec[moreKeys.length];
|
||||
for (int i = 0; i < moreKeys.length; i++) {
|
||||
mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing);
|
||||
}
|
||||
} else {
|
||||
mMoreKeys = null;
|
||||
}
|
||||
mActionFlags = actionFlags;
|
||||
|
||||
mIconId = KeySpecParser.getIconId(keySpec);
|
||||
|
||||
final int code = KeySpecParser.getCode(keySpec);
|
||||
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
|
||||
mLabel = params.mId.mCustomActionLabel;
|
||||
} else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
// This is a workaround to have a key that has a supplementary code point in its label.
|
||||
// Because we can put a string in resource neither as a XML entity of a supplementary
|
||||
// code point nor as a surrogate pair.
|
||||
mLabel = new StringBuilder().appendCodePoint(code).toString();
|
||||
} else {
|
||||
final String label = KeySpecParser.getLabel(keySpec);
|
||||
mLabel = needsToUpcase
|
||||
? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing)
|
||||
: label;
|
||||
}
|
||||
if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
|
||||
mHintLabel = null;
|
||||
} else {
|
||||
final String hintLabel = style.getString(
|
||||
keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
|
||||
mHintLabel = needsToUpcase
|
||||
? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing)
|
||||
: hintLabel;
|
||||
}
|
||||
String outputText = KeySpecParser.getOutputText(keySpec);
|
||||
if (needsToUpcase) {
|
||||
outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing);
|
||||
}
|
||||
// Choose the first letter of the label as primary code if not specified.
|
||||
if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
|
||||
&& !TextUtils.isEmpty(mLabel)) {
|
||||
if (StringUtils.codePointCount(mLabel) == 1) {
|
||||
// Use the first letter of the hint label if shiftedLetterActivated flag is
|
||||
// specified.
|
||||
if (hasShiftedLetterHint() && isShiftedLetterActivated()) {
|
||||
mCode = mHintLabel.codePointAt(0);
|
||||
} else {
|
||||
mCode = mLabel.codePointAt(0);
|
||||
}
|
||||
} else {
|
||||
// In some locale and case, the character might be represented by multiple code
|
||||
// points, such as upper case Eszett of German alphabet.
|
||||
outputText = mLabel;
|
||||
mCode = CODE_OUTPUT_TEXT;
|
||||
}
|
||||
} else if (code == CODE_UNSPECIFIED && outputText != null) {
|
||||
if (StringUtils.codePointCount(outputText) == 1) {
|
||||
mCode = outputText.codePointAt(0);
|
||||
outputText = null;
|
||||
} else {
|
||||
mCode = CODE_OUTPUT_TEXT;
|
||||
}
|
||||
} else {
|
||||
mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing)
|
||||
: code;
|
||||
}
|
||||
final int altCodeInAttr = KeySpecParser.parseCode(
|
||||
style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED);
|
||||
final int altCode = needsToUpcase
|
||||
? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing)
|
||||
: altCodeInAttr;
|
||||
mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode);
|
||||
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
|
||||
mHashCode = computeHashCode(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for DynamicGridKeyboard.GridKey.
|
||||
*
|
||||
* @param key the original key.
|
||||
*/
|
||||
protected Key(final Key key) {
|
||||
this(key, key.mMoreKeys);
|
||||
}
|
||||
|
||||
private Key(final Key key, final MoreKeySpec[] moreKeys) {
|
||||
// Final attributes.
|
||||
mCode = key.mCode;
|
||||
mLabel = key.mLabel;
|
||||
mHintLabel = key.mHintLabel;
|
||||
mLabelFlags = key.mLabelFlags;
|
||||
mIconId = key.mIconId;
|
||||
mWidth = key.mWidth;
|
||||
mHeight = key.mHeight;
|
||||
mDefinedWidth = key.mDefinedWidth;
|
||||
mDefinedHeight = key.mDefinedHeight;
|
||||
mX = key.mX;
|
||||
mY = key.mY;
|
||||
mHitbox.set(key.mHitbox);
|
||||
mMoreKeys = moreKeys;
|
||||
mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
|
||||
mBackgroundType = key.mBackgroundType;
|
||||
mActionFlags = key.mActionFlags;
|
||||
mKeyVisualAttributes = key.mKeyVisualAttributes;
|
||||
mOptionalAttributes = key.mOptionalAttributes;
|
||||
mHashCode = key.mHashCode;
|
||||
// Key state.
|
||||
mPressed = key.mPressed;
|
||||
}
|
||||
|
||||
public static Key removeRedundantMoreKeys(final Key key,
|
||||
final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) {
|
||||
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
||||
final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys(
|
||||
moreKeys, lettersOnBaseLayout);
|
||||
return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys);
|
||||
}
|
||||
|
||||
private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) {
|
||||
if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
|
||||
switch (keyboardElementId) {
|
||||
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int computeHashCode(final Key key) {
|
||||
return Arrays.hashCode(new Object[]{
|
||||
key.mX,
|
||||
key.mY,
|
||||
key.mWidth,
|
||||
key.mHeight,
|
||||
key.mCode,
|
||||
key.mLabel,
|
||||
key.mHintLabel,
|
||||
key.mIconId,
|
||||
key.mBackgroundType,
|
||||
Arrays.hashCode(key.mMoreKeys),
|
||||
key.getOutputText(),
|
||||
key.mActionFlags,
|
||||
key.mLabelFlags,
|
||||
// Key can be distinguishable without the following members.
|
||||
// key.mOptionalAttributes.mAltCode,
|
||||
// key.mOptionalAttributes.mDisabledIconId,
|
||||
// key.mOptionalAttributes.mPreviewIconId,
|
||||
// key.mMaxMoreKeysColumn,
|
||||
// key.mDefinedHeight,
|
||||
// key.mDefinedWidth,
|
||||
});
|
||||
}
|
||||
|
||||
private boolean equalsInternal(final Key o) {
|
||||
if (this == o) return true;
|
||||
return o.mX == mX
|
||||
&& o.mY == mY
|
||||
&& o.mWidth == mWidth
|
||||
&& o.mHeight == mHeight
|
||||
&& o.mCode == mCode
|
||||
&& TextUtils.equals(o.mLabel, mLabel)
|
||||
&& TextUtils.equals(o.mHintLabel, mHintLabel)
|
||||
&& o.mIconId == mIconId
|
||||
&& o.mBackgroundType == mBackgroundType
|
||||
&& Arrays.equals(o.mMoreKeys, mMoreKeys)
|
||||
&& TextUtils.equals(o.getOutputText(), getOutputText())
|
||||
&& o.mActionFlags == mActionFlags
|
||||
&& o.mLabelFlags == mLabelFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Key o) {
|
||||
if (equalsInternal(o)) return 0;
|
||||
if (mHashCode > o.mHashCode) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
return o instanceof Key && equalsInternal((Key) o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight();
|
||||
}
|
||||
|
||||
public String toShortString() {
|
||||
final int code = getCode();
|
||||
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
return getOutputText();
|
||||
}
|
||||
return Constants.printableCode(code);
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return mCode;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String getHintLabel() {
|
||||
return mHintLabel;
|
||||
}
|
||||
|
||||
public MoreKeySpec[] getMoreKeys() {
|
||||
return mMoreKeys;
|
||||
}
|
||||
|
||||
public void setHitboxRightEdge(final int right) {
|
||||
mHitbox.right = right;
|
||||
}
|
||||
|
||||
public final boolean isSpacer() {
|
||||
return this instanceof Spacer;
|
||||
}
|
||||
|
||||
public final boolean isActionKey() {
|
||||
return mBackgroundType == BACKGROUND_TYPE_ACTION;
|
||||
}
|
||||
|
||||
public final boolean isShift() {
|
||||
return mCode == CODE_SHIFT;
|
||||
}
|
||||
|
||||
public final boolean isModifier() {
|
||||
return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
|
||||
}
|
||||
|
||||
public final boolean isRepeatable() {
|
||||
return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
|
||||
}
|
||||
|
||||
public final boolean noKeyPreview() {
|
||||
return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
|
||||
}
|
||||
|
||||
public final boolean altCodeWhileTyping() {
|
||||
return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
|
||||
}
|
||||
|
||||
public final boolean isLongPressEnabled() {
|
||||
// We need not start long press timer on the key which has activated shifted letter.
|
||||
return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
|
||||
&& (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
|
||||
}
|
||||
|
||||
public KeyVisualAttributes getVisualAttributes() {
|
||||
return mKeyVisualAttributes;
|
||||
}
|
||||
|
||||
public final Typeface selectTypeface(final KeyDrawParams params) {
|
||||
switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) {
|
||||
case LABEL_FLAGS_FONT_NORMAL:
|
||||
return Typeface.DEFAULT;
|
||||
case LABEL_FLAGS_FONT_MONO_SPACE:
|
||||
return Typeface.MONOSPACE;
|
||||
case LABEL_FLAGS_FONT_DEFAULT:
|
||||
default:
|
||||
// The type-face is specified by keyTypeface attribute.
|
||||
return params.mTypeface;
|
||||
}
|
||||
}
|
||||
|
||||
public final int selectTextSize(final KeyDrawParams params) {
|
||||
switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
|
||||
case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
|
||||
return params.mLetterSize;
|
||||
case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
|
||||
return params.mLargeLetterSize;
|
||||
case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
|
||||
return params.mLabelSize;
|
||||
case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
|
||||
return params.mHintLabelSize;
|
||||
default: // No follow key ratio flag specified.
|
||||
return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
|
||||
}
|
||||
}
|
||||
|
||||
public final int selectTextColor(final KeyDrawParams params) {
|
||||
if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) {
|
||||
return params.mFunctionalTextColor;
|
||||
}
|
||||
return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
|
||||
}
|
||||
|
||||
public final int selectHintTextSize(final KeyDrawParams params) {
|
||||
if (hasHintLabel()) {
|
||||
return params.mHintLabelSize;
|
||||
}
|
||||
if (hasShiftedLetterHint()) {
|
||||
return params.mShiftedLetterHintSize;
|
||||
}
|
||||
return params.mHintLetterSize;
|
||||
}
|
||||
|
||||
public final int selectHintTextColor(final KeyDrawParams params) {
|
||||
if (hasHintLabel()) {
|
||||
return params.mHintLabelColor;
|
||||
}
|
||||
if (hasShiftedLetterHint()) {
|
||||
return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
|
||||
: params.mShiftedLetterHintInactivatedColor;
|
||||
}
|
||||
return params.mHintLetterColor;
|
||||
}
|
||||
|
||||
public final String getPreviewLabel() {
|
||||
return isShiftedLetterActivated() ? mHintLabel : mLabel;
|
||||
}
|
||||
|
||||
private boolean previewHasLetterSize() {
|
||||
return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
|
||||
|| StringUtils.codePointCount(getPreviewLabel()) == 1;
|
||||
}
|
||||
|
||||
public final int selectPreviewTextSize(final KeyDrawParams params) {
|
||||
if (previewHasLetterSize()) {
|
||||
return params.mPreviewTextSize;
|
||||
}
|
||||
return params.mLetterSize;
|
||||
}
|
||||
|
||||
public Typeface selectPreviewTypeface(final KeyDrawParams params) {
|
||||
if (previewHasLetterSize()) {
|
||||
return selectTypeface(params);
|
||||
}
|
||||
return Typeface.DEFAULT_BOLD;
|
||||
}
|
||||
|
||||
public final boolean isAlignHintLabelToBottom(final int defaultFlags) {
|
||||
return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0;
|
||||
}
|
||||
|
||||
public final boolean isAlignIconToBottom() {
|
||||
return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0;
|
||||
}
|
||||
|
||||
public final boolean isAlignLabelOffCenter() {
|
||||
return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0;
|
||||
}
|
||||
|
||||
public final boolean hasShiftedLetterHint() {
|
||||
return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0
|
||||
&& !TextUtils.isEmpty(mHintLabel);
|
||||
}
|
||||
|
||||
public final boolean hasHintLabel() {
|
||||
return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
|
||||
}
|
||||
|
||||
public final boolean needsAutoXScale() {
|
||||
return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
|
||||
}
|
||||
|
||||
public final boolean needsAutoScale() {
|
||||
return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
|
||||
}
|
||||
|
||||
private final boolean isShiftedLetterActivated() {
|
||||
return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
|
||||
&& !TextUtils.isEmpty(mHintLabel);
|
||||
}
|
||||
|
||||
public final int getMoreKeysColumnNumber() {
|
||||
return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK;
|
||||
}
|
||||
|
||||
public final boolean isMoreKeysFixedColumn() {
|
||||
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0;
|
||||
}
|
||||
|
||||
public final boolean isMoreKeysFixedOrder() {
|
||||
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0;
|
||||
}
|
||||
|
||||
public final boolean hasLabelsInMoreKeys() {
|
||||
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
|
||||
}
|
||||
|
||||
public final int getMoreKeyLabelFlags() {
|
||||
final int labelSizeFlag = hasLabelsInMoreKeys()
|
||||
? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
|
||||
: LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
|
||||
return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE;
|
||||
}
|
||||
|
||||
public final boolean hasNoPanelAutoMoreKey() {
|
||||
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
|
||||
}
|
||||
|
||||
public final String getOutputText() {
|
||||
final OptionalAttributes attrs = mOptionalAttributes;
|
||||
return (attrs != null) ? attrs.mOutputText : null;
|
||||
}
|
||||
|
||||
public final int getAltCode() {
|
||||
final OptionalAttributes attrs = mOptionalAttributes;
|
||||
return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
|
||||
}
|
||||
|
||||
public int getIconId() {
|
||||
return mIconId;
|
||||
}
|
||||
|
||||
public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
|
||||
final Drawable icon = iconSet.getIconDrawable(getIconId());
|
||||
if (icon != null) {
|
||||
icon.setAlpha(alpha);
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
|
||||
return iconSet.getIconDrawable(getIconId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the key in pixels, excluding the padding.
|
||||
*
|
||||
* @return The width of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the key in pixels, excluding the padding.
|
||||
*
|
||||
* @return The height of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the theoretical width of the key in pixels, excluding the padding. This is the exact
|
||||
* width that the key was defined to be, but this will likely differ from the actual drawn width
|
||||
* because the normal (drawn/functional) width was determined by rounding the left and right
|
||||
* edge to fit evenly in a pixel.
|
||||
*
|
||||
* @return The defined width of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public float getDefinedWidth() {
|
||||
return mDefinedWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the theoretical height of the key in pixels, excluding the padding. This is the exact
|
||||
* height that the key was defined to be, but this will likely differ from the actual drawn
|
||||
* height because the normal (drawn/functional) width was determined by rounding the top and
|
||||
* bottom edge to fit evenly in a pixel.
|
||||
*
|
||||
* @return The defined width of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public float getDefinedHeight() {
|
||||
return mDefinedHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the x-coordinate of the top-left corner of the key in pixels, excluding the padding.
|
||||
*
|
||||
* @return The x-coordinate of the top-left corner of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public int getX() {
|
||||
return mX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the y-coordinate of the top-left corner of the key in pixels, excluding the padding.
|
||||
*
|
||||
* @return The y-coordinate of the top-left corner of the key in pixels, excluding the padding.
|
||||
*/
|
||||
public int getY() {
|
||||
return mY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of padding for the hitbox above the key's visible position.
|
||||
*
|
||||
* @return The hitbox padding above the key.
|
||||
*/
|
||||
public int getTopPadding() {
|
||||
return mY - mHitbox.top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of padding for the hitbox below the key's visible position.
|
||||
*
|
||||
* @return The hitbox padding below the key.
|
||||
*/
|
||||
public int getBottomPadding() {
|
||||
return mHitbox.bottom - mY - mHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of padding for the hitbox to the left of the key's visible position.
|
||||
*
|
||||
* @return The hitbox padding to the left of the key.
|
||||
*/
|
||||
public int getLeftPadding() {
|
||||
return mX - mHitbox.left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount of padding for the hitbox to the right of the key's visible position.
|
||||
*
|
||||
* @return The hitbox padding to the right of the key.
|
||||
*/
|
||||
public int getRightPadding() {
|
||||
return mHitbox.right - mX - mWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the key that it has been pressed, in case it needs to change its appearance or
|
||||
* state.
|
||||
*
|
||||
* @see #onReleased()
|
||||
*/
|
||||
public void onPressed() {
|
||||
mPressed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the key that it has been released, in case it needs to change its appearance or
|
||||
* state.
|
||||
*
|
||||
* @see #onPressed()
|
||||
*/
|
||||
public void onReleased() {
|
||||
mPressed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a point falls on this key.
|
||||
*
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return whether or not the point falls on the key. This generally includes all points
|
||||
* between the key and the keyboard edge for keys attached to an edge and all points between
|
||||
* the key and halfway to adjacent keys.
|
||||
*/
|
||||
public boolean isOnKey(final int x, final int y) {
|
||||
return mHitbox.contains(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square of the distance to the nearest clickable edge of the key and the given
|
||||
* point.
|
||||
*
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the square of the distance of the point from the nearest edge of the key
|
||||
*/
|
||||
public int squaredDistanceToHitboxEdge(final int x, final int y) {
|
||||
final int left = mHitbox.left;
|
||||
// The hit box right is exclusive
|
||||
final int right = mHitbox.right - 1;
|
||||
final int top = mHitbox.top;
|
||||
// The hit box bottom is exclusive
|
||||
final int bottom = mHitbox.bottom - 1;
|
||||
final int edgeX = x < left ? left : Math.min(x, right);
|
||||
final int edgeY = y < top ? top : Math.min(y, bottom);
|
||||
final int dx = x - edgeX;
|
||||
final int dy = y - edgeY;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
static class KeyBackgroundState {
|
||||
private final int[] mReleasedState;
|
||||
private final int[] mPressedState;
|
||||
|
||||
private KeyBackgroundState(final int... attrs) {
|
||||
mReleasedState = attrs;
|
||||
mPressedState = Arrays.copyOf(attrs, attrs.length + 1);
|
||||
mPressedState[attrs.length] = android.R.attr.state_pressed;
|
||||
}
|
||||
|
||||
public int[] getState(final boolean pressed) {
|
||||
return pressed ? mPressedState : mReleasedState;
|
||||
}
|
||||
|
||||
public static final KeyBackgroundState[] STATES = {
|
||||
// 0: BACKGROUND_TYPE_EMPTY
|
||||
new KeyBackgroundState(android.R.attr.state_empty),
|
||||
// 1: BACKGROUND_TYPE_NORMAL
|
||||
new KeyBackgroundState(),
|
||||
// 2: BACKGROUND_TYPE_FUNCTIONAL
|
||||
new KeyBackgroundState(),
|
||||
// 3: BACKGROUND_TYPE_STICKY_OFF
|
||||
new KeyBackgroundState(android.R.attr.state_checkable),
|
||||
// 4: BACKGROUND_TYPE_STICKY_ON
|
||||
new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked),
|
||||
// 5: BACKGROUND_TYPE_ACTION
|
||||
new KeyBackgroundState(android.R.attr.state_active),
|
||||
// 6: BACKGROUND_TYPE_SPACEBAR
|
||||
new KeyBackgroundState(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the background drawable for the key, based on the current state and type of the key.
|
||||
*
|
||||
* @return the background drawable of the key.
|
||||
* @see android.graphics.drawable.StateListDrawable#setState(int[])
|
||||
*/
|
||||
public final Drawable selectBackgroundDrawable(final Drawable keyBackground,
|
||||
final Drawable functionalKeyBackground,
|
||||
final Drawable spacebarBackground) {
|
||||
final Drawable background;
|
||||
if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) {
|
||||
background = functionalKeyBackground;
|
||||
} else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
|
||||
background = spacebarBackground;
|
||||
} else {
|
||||
background = keyBackground;
|
||||
}
|
||||
final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed);
|
||||
background.setState(state);
|
||||
return background;
|
||||
}
|
||||
|
||||
public static class Spacer extends Key {
|
||||
public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle,
|
||||
final KeyboardParams params, final KeyboardRow row) {
|
||||
super(null /* keySpec */, keyAttr, keyStyle, params, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
/**
|
||||
* This class handles key detection.
|
||||
*/
|
||||
public class KeyDetector {
|
||||
private final int mKeyHysteresisDistanceSquared;
|
||||
private final int mKeyHysteresisDistanceForSlidingModifierSquared;
|
||||
|
||||
private Keyboard mKeyboard;
|
||||
private int mCorrectionX;
|
||||
private int mCorrectionY;
|
||||
|
||||
public KeyDetector() {
|
||||
this(0.0f /* keyHysteresisDistance */, 0.0f /* keyHysteresisDistanceForSlidingModifier */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key detection object constructor with key hysteresis distances.
|
||||
*
|
||||
* @param keyHysteresisDistance if the pointer movement distance is smaller than this, the
|
||||
* movement will not be handled as meaningful movement. The unit is pixel.
|
||||
* @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that
|
||||
* starts from a modifier key such as shift and symbols key.
|
||||
*/
|
||||
public KeyDetector(final float keyHysteresisDistance,
|
||||
final float keyHysteresisDistanceForSlidingModifier) {
|
||||
mKeyHysteresisDistanceSquared = (int) (keyHysteresisDistance * keyHysteresisDistance);
|
||||
mKeyHysteresisDistanceForSlidingModifierSquared = (int) (
|
||||
keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier);
|
||||
}
|
||||
|
||||
public void setKeyboard(final Keyboard keyboard, final float correctionX,
|
||||
final float correctionY) {
|
||||
if (keyboard == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
mCorrectionX = (int) correctionX;
|
||||
mCorrectionY = (int) correctionY;
|
||||
mKeyboard = keyboard;
|
||||
}
|
||||
|
||||
public int getKeyHysteresisDistanceSquared(final boolean isSlidingFromModifier) {
|
||||
return isSlidingFromModifier
|
||||
? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared;
|
||||
}
|
||||
|
||||
public int getTouchX(final int x) {
|
||||
return x + mCorrectionX;
|
||||
}
|
||||
|
||||
// TODO: Remove vertical correction.
|
||||
public int getTouchY(final int y) {
|
||||
return y + mCorrectionY;
|
||||
}
|
||||
|
||||
public Keyboard getKeyboard() {
|
||||
return mKeyboard;
|
||||
}
|
||||
|
||||
public boolean alwaysAllowsKeySelectionByDraggingFinger() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the key whose hitbox the touch point is in.
|
||||
*
|
||||
* @param x The x-coordinate of a touch point
|
||||
* @param y The y-coordinate of a touch point
|
||||
* @return the key that the touch point hits.
|
||||
*/
|
||||
public Key detectHitKey(final int x, final int y) {
|
||||
if (mKeyboard == null) {
|
||||
return null;
|
||||
}
|
||||
final int touchX = getTouchX(x);
|
||||
final int touchY = getTouchY(y);
|
||||
|
||||
for (final Key key : mKeyboard.getNearestKeys(touchX, touchY)) {
|
||||
if (key.isOnKey(touchX, touchY)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyVisualAttributes;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardIconsSet;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
|
||||
* consists of rows of keys.
|
||||
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
|
||||
* <pre>
|
||||
* <Keyboard
|
||||
* latin:keyWidth="10%p"
|
||||
* latin:rowHeight="50px"
|
||||
* latin:horizontalGap="2%p"
|
||||
* latin:verticalGap="2%p" >
|
||||
* <Row latin:keyWidth="10%p" >
|
||||
* <Key latin:keyLabel="A" />
|
||||
* ...
|
||||
* </Row>
|
||||
* ...
|
||||
* </Keyboard>
|
||||
* </pre>
|
||||
*/
|
||||
public class Keyboard {
|
||||
public final KeyboardId mId;
|
||||
|
||||
/**
|
||||
* Total height of the keyboard, including the padding and keys
|
||||
*/
|
||||
public final int mOccupiedHeight;
|
||||
/**
|
||||
* Total width of the keyboard, including the padding and keys
|
||||
*/
|
||||
public final int mOccupiedWidth;
|
||||
|
||||
/**
|
||||
* The padding below the keyboard
|
||||
*/
|
||||
public final float mBottomPadding;
|
||||
/**
|
||||
* The padding above the keyboard
|
||||
*/
|
||||
public final float mTopPadding;
|
||||
/**
|
||||
* Default gap between rows
|
||||
*/
|
||||
public final float mVerticalGap;
|
||||
/**
|
||||
* Default gap between columns
|
||||
*/
|
||||
public final float mHorizontalGap;
|
||||
|
||||
/**
|
||||
* Per keyboard key visual parameters
|
||||
*/
|
||||
public final KeyVisualAttributes mKeyVisualAttributes;
|
||||
|
||||
public final int mMostCommonKeyHeight;
|
||||
public final int mMostCommonKeyWidth;
|
||||
|
||||
/**
|
||||
* More keys keyboard template
|
||||
*/
|
||||
public final int mMoreKeysTemplate;
|
||||
|
||||
/**
|
||||
* List of keys in this keyboard
|
||||
*/
|
||||
private final List<Key> mSortedKeys;
|
||||
public final List<Key> mShiftKeys;
|
||||
public final List<Key> mAltCodeKeysWhileTyping;
|
||||
public final KeyboardIconsSet mIconsSet;
|
||||
|
||||
private final SparseArray<Key> mKeyCache = new SparseArray<>();
|
||||
|
||||
private final ProximityInfo mProximityInfo;
|
||||
|
||||
public Keyboard(final KeyboardParams params) {
|
||||
mId = params.mId;
|
||||
mOccupiedHeight = params.mOccupiedHeight;
|
||||
mOccupiedWidth = params.mOccupiedWidth;
|
||||
mMostCommonKeyHeight = params.mMostCommonKeyHeight;
|
||||
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
|
||||
mMoreKeysTemplate = params.mMoreKeysTemplate;
|
||||
mKeyVisualAttributes = params.mKeyVisualAttributes;
|
||||
mBottomPadding = params.mBottomPadding;
|
||||
mTopPadding = params.mTopPadding;
|
||||
mVerticalGap = params.mVerticalGap;
|
||||
mHorizontalGap = params.mHorizontalGap;
|
||||
|
||||
mSortedKeys = Collections.unmodifiableList(new ArrayList<>(params.mSortedKeys));
|
||||
mShiftKeys = Collections.unmodifiableList(params.mShiftKeys);
|
||||
mAltCodeKeysWhileTyping = Collections.unmodifiableList(params.mAltCodeKeysWhileTyping);
|
||||
mIconsSet = params.mIconsSet;
|
||||
|
||||
mProximityInfo = new ProximityInfo(params.mGridWidth, params.mGridHeight,
|
||||
mOccupiedWidth, mOccupiedHeight, mSortedKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sorted list of keys of this keyboard.
|
||||
* The keys are sorted from top-left to bottom-right order.
|
||||
* The list may contain {@link Key.Spacer} object as well.
|
||||
*
|
||||
* @return the sorted unmodifiable list of {@link Key}s of this keyboard.
|
||||
*/
|
||||
public List<Key> getSortedKeys() {
|
||||
return mSortedKeys;
|
||||
}
|
||||
|
||||
public Key getKey(final int code) {
|
||||
if (code == Constants.CODE_UNSPECIFIED) {
|
||||
return null;
|
||||
}
|
||||
synchronized (mKeyCache) {
|
||||
final int index = mKeyCache.indexOfKey(code);
|
||||
if (index >= 0) {
|
||||
return mKeyCache.valueAt(index);
|
||||
}
|
||||
|
||||
for (final Key key : getSortedKeys()) {
|
||||
if (key.getCode() == code) {
|
||||
mKeyCache.put(code, key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
mKeyCache.put(code, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasKey(final Key aKey) {
|
||||
if (mKeyCache.indexOfValue(aKey) >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final Key key : getSortedKeys()) {
|
||||
if (key == aKey) {
|
||||
mKeyCache.put(key.getCode(), key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mId.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of the keys that are closest to the given point.
|
||||
*
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the list of the nearest keys to the given point. If the given
|
||||
* point is out of range, then an array of size zero is returned.
|
||||
*/
|
||||
public List<Key> getNearestKeys(final int x, final int y) {
|
||||
// Avoid dead pixels at edges of the keyboard
|
||||
final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
|
||||
final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
|
||||
return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
|
||||
public interface KeyboardActionListener {
|
||||
/**
|
||||
* Called when the user presses a key. This is sent before the {@link #onCodeInput} is called.
|
||||
* For keys that repeat, this is only called once.
|
||||
*
|
||||
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
|
||||
* the value will be zero.
|
||||
* @param repeatCount how many times the key was repeated. Zero if it is the first press.
|
||||
* @param isSinglePointer true if pressing has occurred while no other key is being pressed.
|
||||
*/
|
||||
void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer);
|
||||
|
||||
/**
|
||||
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
|
||||
* For keys that repeat, this is only called once.
|
||||
*
|
||||
* @param primaryCode the code of the key that was released
|
||||
* @param withSliding true if releasing has occurred because the user slid finger from the key
|
||||
* to other key without releasing the finger.
|
||||
*/
|
||||
void onReleaseKey(int primaryCode, boolean withSliding);
|
||||
|
||||
/**
|
||||
* Send a key code to the listener.
|
||||
*
|
||||
* @param primaryCode this is the code of the key that was pressed
|
||||
* @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
|
||||
* {@link PointerTracker} or so, the value should be
|
||||
* {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the
|
||||
* suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
|
||||
* @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
|
||||
* {@link PointerTracker} or so, the value should be
|
||||
* {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the
|
||||
* suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
|
||||
* @param isKeyRepeat true if this is a key repeat, false otherwise
|
||||
*/
|
||||
// TODO: change this to send an Event object instead
|
||||
void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
|
||||
|
||||
/**
|
||||
* Sends a string of characters to the listener.
|
||||
*
|
||||
* @param text the string of characters to be registered.
|
||||
*/
|
||||
void onTextInput(final String rawText);
|
||||
|
||||
/**
|
||||
* Called when user finished sliding key input.
|
||||
*/
|
||||
void onFinishSlidingInput();
|
||||
|
||||
/**
|
||||
* Send a non-"code input" custom request to the listener.
|
||||
*
|
||||
* @return true if the request has been consumed, false otherwise.
|
||||
*/
|
||||
boolean onCustomRequest(int requestCode);
|
||||
|
||||
void onMovePointer(int steps);
|
||||
|
||||
void onMoveDeletePointer(int steps);
|
||||
|
||||
void onUpWithDeletePointerActive();
|
||||
|
||||
KeyboardActionListener EMPTY_LISTENER = new Adapter();
|
||||
|
||||
class Adapter implements KeyboardActionListener {
|
||||
@Override
|
||||
public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReleaseKey(int primaryCode, boolean withSliding) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextInput(String text) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishSlidingInput() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCustomRequest(int requestCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovePointer(int steps) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoveDeletePointer(int steps) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpWithDeletePointerActive() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.compat.EditorInfoCompatUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Unique identifier for each keyboard type.
|
||||
*/
|
||||
public final class KeyboardId {
|
||||
public static final int MODE_TEXT = 0;
|
||||
public static final int MODE_URL = 1;
|
||||
public static final int MODE_EMAIL = 2;
|
||||
public static final int MODE_IM = 3;
|
||||
public static final int MODE_PHONE = 4;
|
||||
public static final int MODE_NUMBER = 5;
|
||||
public static final int MODE_DATE = 6;
|
||||
public static final int MODE_TIME = 7;
|
||||
public static final int MODE_DATETIME = 8;
|
||||
|
||||
public static final int ELEMENT_ALPHABET = 0;
|
||||
public static final int ELEMENT_ALPHABET_MANUAL_SHIFTED = 1;
|
||||
public static final int ELEMENT_ALPHABET_AUTOMATIC_SHIFTED = 2;
|
||||
public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3;
|
||||
public static final int ELEMENT_SYMBOLS = 5;
|
||||
public static final int ELEMENT_SYMBOLS_SHIFTED = 6;
|
||||
public static final int ELEMENT_PHONE = 7;
|
||||
public static final int ELEMENT_PHONE_SYMBOLS = 8;
|
||||
public static final int ELEMENT_NUMBER = 9;
|
||||
|
||||
public final Subtype mSubtype;
|
||||
public final int mThemeId;
|
||||
public final int mWidth;
|
||||
public final int mHeight;
|
||||
public final int mMode;
|
||||
public final int mElementId;
|
||||
public final EditorInfo mEditorInfo;
|
||||
public final boolean mLanguageSwitchKeyEnabled;
|
||||
public final String mCustomActionLabel;
|
||||
public final boolean mShowMoreKeys;
|
||||
public final boolean mShowNumberRow;
|
||||
|
||||
private final int mHashCode;
|
||||
|
||||
public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
|
||||
mSubtype = params.mSubtype;
|
||||
mThemeId = params.mKeyboardThemeId;
|
||||
mWidth = params.mKeyboardWidth;
|
||||
mHeight = params.mKeyboardHeight;
|
||||
mMode = params.mMode;
|
||||
mElementId = elementId;
|
||||
mEditorInfo = params.mEditorInfo;
|
||||
mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled;
|
||||
mCustomActionLabel = (mEditorInfo.actionLabel != null)
|
||||
? mEditorInfo.actionLabel.toString() : null;
|
||||
mShowMoreKeys = params.mShowMoreKeys;
|
||||
mShowNumberRow = params.mShowNumberRow;
|
||||
|
||||
mHashCode = computeHashCode(this);
|
||||
}
|
||||
|
||||
private static int computeHashCode(final KeyboardId id) {
|
||||
return Arrays.hashCode(new Object[]{
|
||||
id.mElementId,
|
||||
id.mMode,
|
||||
id.mWidth,
|
||||
id.mHeight,
|
||||
id.passwordInput(),
|
||||
id.mLanguageSwitchKeyEnabled,
|
||||
id.isMultiLine(),
|
||||
id.imeAction(),
|
||||
id.mCustomActionLabel,
|
||||
id.navigateNext(),
|
||||
id.navigatePrevious(),
|
||||
id.mSubtype,
|
||||
id.mThemeId
|
||||
});
|
||||
}
|
||||
|
||||
private boolean equals(final KeyboardId other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
return other.mElementId == mElementId
|
||||
&& other.mMode == mMode
|
||||
&& other.mWidth == mWidth
|
||||
&& other.mHeight == mHeight
|
||||
&& other.passwordInput() == passwordInput()
|
||||
&& other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled
|
||||
&& other.isMultiLine() == isMultiLine()
|
||||
&& other.imeAction() == imeAction()
|
||||
&& TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel)
|
||||
&& other.navigateNext() == navigateNext()
|
||||
&& other.navigatePrevious() == navigatePrevious()
|
||||
&& other.mSubtype.equals(mSubtype)
|
||||
&& other.mThemeId == mThemeId;
|
||||
}
|
||||
|
||||
private static boolean isAlphabetKeyboard(final int elementId) {
|
||||
return elementId < ELEMENT_SYMBOLS;
|
||||
}
|
||||
|
||||
public boolean isAlphabetKeyboard() {
|
||||
return isAlphabetKeyboard(mElementId);
|
||||
}
|
||||
|
||||
public boolean navigateNext() {
|
||||
return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
|
||||
|| imeAction() == EditorInfo.IME_ACTION_NEXT;
|
||||
}
|
||||
|
||||
public boolean navigatePrevious() {
|
||||
return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
|
||||
|| imeAction() == EditorInfo.IME_ACTION_PREVIOUS;
|
||||
}
|
||||
|
||||
public boolean passwordInput() {
|
||||
final int inputType = mEditorInfo.inputType;
|
||||
return InputTypeUtils.isPasswordInputType(inputType)
|
||||
|| InputTypeUtils.isVisiblePasswordInputType(inputType);
|
||||
}
|
||||
|
||||
public boolean isMultiLine() {
|
||||
return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
|
||||
}
|
||||
|
||||
public int imeAction() {
|
||||
return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo);
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return mSubtype.getLocaleObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object other) {
|
||||
return other instanceof KeyboardId && equals((KeyboardId) other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s %s]",
|
||||
elementIdToName(mElementId),
|
||||
mSubtype.getLocale(),
|
||||
mSubtype.getKeyboardLayoutSet(),
|
||||
mWidth, mHeight,
|
||||
modeName(mMode),
|
||||
actionName(imeAction()),
|
||||
(navigateNext() ? " navigateNext" : ""),
|
||||
(navigatePrevious() ? " navigatePrevious" : ""),
|
||||
(passwordInput() ? " passwordInput" : ""),
|
||||
(mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""),
|
||||
(isMultiLine() ? " isMultiLine" : ""),
|
||||
KeyboardTheme.getKeyboardThemeName(mThemeId)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean equivalentEditorInfoForKeyboard(final EditorInfo a, final EditorInfo b) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
return a.inputType == b.inputType
|
||||
&& a.imeOptions == b.imeOptions
|
||||
&& TextUtils.equals(a.privateImeOptions, b.privateImeOptions);
|
||||
}
|
||||
|
||||
public static String elementIdToName(final int elementId) {
|
||||
switch (elementId) {
|
||||
case ELEMENT_ALPHABET:
|
||||
return "alphabet";
|
||||
case ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||
return "alphabetManualShifted";
|
||||
case ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||
return "alphabetAutomaticShifted";
|
||||
case ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||
return "alphabetShiftLocked";
|
||||
case ELEMENT_SYMBOLS:
|
||||
return "symbols";
|
||||
case ELEMENT_SYMBOLS_SHIFTED:
|
||||
return "symbolsShifted";
|
||||
case ELEMENT_PHONE:
|
||||
return "phone";
|
||||
case ELEMENT_PHONE_SYMBOLS:
|
||||
return "phoneSymbols";
|
||||
case ELEMENT_NUMBER:
|
||||
return "number";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String modeName(final int mode) {
|
||||
switch (mode) {
|
||||
case MODE_TEXT:
|
||||
return "text";
|
||||
case MODE_URL:
|
||||
return "url";
|
||||
case MODE_EMAIL:
|
||||
return "email";
|
||||
case MODE_IM:
|
||||
return "im";
|
||||
case MODE_PHONE:
|
||||
return "phone";
|
||||
case MODE_NUMBER:
|
||||
return "number";
|
||||
case MODE_DATE:
|
||||
return "date";
|
||||
case MODE_TIME:
|
||||
return "time";
|
||||
case MODE_DATETIME:
|
||||
return "datetime";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String actionName(final int actionId) {
|
||||
return (actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL) ? "actionCustomLabel"
|
||||
: EditorInfoCompatUtils.imeActionName(actionId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.Xml;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardBuilder;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.UniqueKeysCache;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class represents a set of keyboard layouts. Each of them represents a different keyboard
|
||||
* specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same
|
||||
* {@link KeyboardLayoutSet} are related to each other.
|
||||
* A {@link KeyboardLayoutSet} needs to be created for each
|
||||
* {@link android.view.inputmethod.EditorInfo}.
|
||||
*/
|
||||
public final class KeyboardLayoutSet {
|
||||
private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
|
||||
private static final boolean DEBUG_CACHE = false;
|
||||
|
||||
private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
|
||||
private static final String TAG_ELEMENT = "Element";
|
||||
|
||||
private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
|
||||
|
||||
private final Context mContext;
|
||||
private final Params mParams;
|
||||
|
||||
// How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
|
||||
// ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
|
||||
// soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
|
||||
private static final int FORCIBLE_CACHE_SIZE = 4;
|
||||
// By construction of soft references, anything that is also referenced somewhere else
|
||||
// will stay in the cache. So we forcibly keep some references in an array to prevent
|
||||
// them from disappearing from sKeyboardCache.
|
||||
private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
|
||||
private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
|
||||
new HashMap<>();
|
||||
private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static final class KeyboardLayoutSetException extends RuntimeException {
|
||||
public final KeyboardId mKeyboardId;
|
||||
|
||||
public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
|
||||
super(cause);
|
||||
mKeyboardId = keyboardId;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ElementParams {
|
||||
int mKeyboardXmlId;
|
||||
boolean mAllowRedundantMoreKeys;
|
||||
|
||||
public ElementParams() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Params {
|
||||
String mKeyboardLayoutSetName;
|
||||
int mMode;
|
||||
// TODO: Use {@link InputAttributes} instead of these variables.
|
||||
EditorInfo mEditorInfo;
|
||||
boolean mLanguageSwitchKeyEnabled;
|
||||
Subtype mSubtype;
|
||||
int mKeyboardThemeId;
|
||||
int mKeyboardWidth;
|
||||
int mKeyboardHeight;
|
||||
boolean mShowMoreKeys;
|
||||
boolean mShowNumberRow;
|
||||
// Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
|
||||
final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
|
||||
new SparseArray<>();
|
||||
}
|
||||
|
||||
public static void onSystemLocaleChanged() {
|
||||
clearKeyboardCache();
|
||||
}
|
||||
|
||||
public static void onKeyboardThemeChanged() {
|
||||
clearKeyboardCache();
|
||||
}
|
||||
|
||||
private static void clearKeyboardCache() {
|
||||
sKeyboardCache.clear();
|
||||
sUniqueKeysCache.clear();
|
||||
}
|
||||
|
||||
KeyboardLayoutSet(final Context context, final Params params) {
|
||||
mContext = context;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
|
||||
final int keyboardLayoutSetElementId;
|
||||
switch (mParams.mMode) {
|
||||
case KeyboardId.MODE_PHONE:
|
||||
if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
|
||||
keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
|
||||
} else {
|
||||
keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
|
||||
}
|
||||
break;
|
||||
case KeyboardId.MODE_NUMBER:
|
||||
case KeyboardId.MODE_DATE:
|
||||
case KeyboardId.MODE_TIME:
|
||||
case KeyboardId.MODE_DATETIME:
|
||||
keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
|
||||
break;
|
||||
default:
|
||||
keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
|
||||
break;
|
||||
}
|
||||
|
||||
ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
|
||||
keyboardLayoutSetElementId);
|
||||
if (elementParams == null) {
|
||||
elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
|
||||
KeyboardId.ELEMENT_ALPHABET);
|
||||
}
|
||||
// Note: The keyboard for each shift state, and mode are represented as an elementName
|
||||
// attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is
|
||||
// specified as an elementKeyboard attribute in the file.
|
||||
// The KeyboardId is an internal key for a Keyboard object.
|
||||
|
||||
final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
|
||||
return getKeyboard(elementParams, id);
|
||||
}
|
||||
|
||||
private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
|
||||
final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
|
||||
final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
|
||||
if (cachedKeyboard != null) {
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
|
||||
}
|
||||
return cachedKeyboard;
|
||||
}
|
||||
|
||||
final KeyboardBuilder<KeyboardParams> builder =
|
||||
new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
|
||||
sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
|
||||
builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
|
||||
final int keyboardXmlId = elementParams.mKeyboardXmlId;
|
||||
builder.load(keyboardXmlId, id);
|
||||
final Keyboard keyboard = builder.build();
|
||||
sKeyboardCache.put(id, new SoftReference<>(keyboard));
|
||||
if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
|
||||
|| id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)) {
|
||||
// We only forcibly cache the primary, "ALPHABET", layouts.
|
||||
for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
|
||||
sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
|
||||
}
|
||||
sForcibleKeyboardCache[0] = keyboard;
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(TAG, "forcing caching of keyboard with id=" + id);
|
||||
}
|
||||
}
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
|
||||
+ ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
|
||||
}
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final Context mContext;
|
||||
private final Resources mResources;
|
||||
|
||||
private final Params mParams = new Params();
|
||||
|
||||
private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
|
||||
|
||||
public Builder(final Context context, final EditorInfo ei) {
|
||||
mContext = context;
|
||||
mResources = context.getResources();
|
||||
final Params params = mParams;
|
||||
|
||||
final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
|
||||
params.mMode = getKeyboardMode(editorInfo);
|
||||
// TODO: Consolidate those with {@link InputAttributes}.
|
||||
params.mEditorInfo = editorInfo;
|
||||
}
|
||||
|
||||
public Builder setKeyboardTheme(final int themeId) {
|
||||
mParams.mKeyboardThemeId = themeId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
|
||||
mParams.mKeyboardWidth = keyboardWidth;
|
||||
mParams.mKeyboardHeight = keyboardHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSubtype(final Subtype subtype) {
|
||||
// TODO: Consolidate with {@link InputAttributes}.
|
||||
mParams.mSubtype = subtype;
|
||||
mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
|
||||
+ subtype.getKeyboardLayoutSet();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
|
||||
mParams.mLanguageSwitchKeyEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setShowSpecialChars(final boolean enabled) {
|
||||
mParams.mShowMoreKeys = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setShowNumberRow(final boolean enabled) {
|
||||
mParams.mShowNumberRow = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeyboardLayoutSet build() {
|
||||
if (mParams.mSubtype == null)
|
||||
throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
|
||||
final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
|
||||
try {
|
||||
parseKeyboardLayoutSet(mResources, xmlId);
|
||||
} catch (final IOException | XmlPullParserException e) {
|
||||
throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
|
||||
e);
|
||||
}
|
||||
return new KeyboardLayoutSet(mContext, mParams);
|
||||
}
|
||||
|
||||
private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
|
||||
final String packageName = resources.getResourcePackageName(
|
||||
R.xml.keyboard_layout_set_qwerty);
|
||||
return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
|
||||
}
|
||||
|
||||
private void parseKeyboardLayoutSet(final Resources res, final int resId)
|
||||
throws XmlPullParserException, IOException {
|
||||
final XmlResourceParser parser = res.getXml(resId);
|
||||
try {
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
final int event = parser.next();
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_KEYBOARD_SET.equals(tag)) {
|
||||
parseKeyboardLayoutSetContent(parser);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
|
||||
throws XmlPullParserException, IOException {
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
final int event = parser.next();
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_ELEMENT.equals(tag)) {
|
||||
parseKeyboardLayoutSetElement(parser);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
|
||||
}
|
||||
} else if (event == XmlPullParser.END_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_KEYBOARD_SET.equals(tag)) {
|
||||
break;
|
||||
}
|
||||
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
|
||||
throws XmlPullParserException, IOException {
|
||||
final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
|
||||
R.styleable.KeyboardLayoutSet_Element);
|
||||
try {
|
||||
XmlParseUtils.checkAttributeExists(a,
|
||||
R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
|
||||
TAG_ELEMENT, parser);
|
||||
XmlParseUtils.checkAttributeExists(a,
|
||||
R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
|
||||
TAG_ELEMENT, parser);
|
||||
XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
|
||||
|
||||
final ElementParams elementParams = new ElementParams();
|
||||
final int elementName = a.getInt(
|
||||
R.styleable.KeyboardLayoutSet_Element_elementName, 0);
|
||||
elementParams.mKeyboardXmlId = a.getResourceId(
|
||||
R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
|
||||
elementParams.mAllowRedundantMoreKeys = a.getBoolean(
|
||||
R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
|
||||
mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private static int getKeyboardMode(final EditorInfo editorInfo) {
|
||||
final int inputType = editorInfo.inputType;
|
||||
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
|
||||
|
||||
switch (inputType & InputType.TYPE_MASK_CLASS) {
|
||||
case InputType.TYPE_CLASS_NUMBER:
|
||||
return KeyboardId.MODE_NUMBER;
|
||||
case InputType.TYPE_CLASS_DATETIME:
|
||||
switch (variation) {
|
||||
case InputType.TYPE_DATETIME_VARIATION_DATE:
|
||||
return KeyboardId.MODE_DATE;
|
||||
case InputType.TYPE_DATETIME_VARIATION_TIME:
|
||||
return KeyboardId.MODE_TIME;
|
||||
default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
|
||||
return KeyboardId.MODE_DATETIME;
|
||||
}
|
||||
case InputType.TYPE_CLASS_PHONE:
|
||||
return KeyboardId.MODE_PHONE;
|
||||
case InputType.TYPE_CLASS_TEXT:
|
||||
if (InputTypeUtils.isEmailVariation(variation)) {
|
||||
return KeyboardId.MODE_EMAIL;
|
||||
} else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
|
||||
return KeyboardId.MODE_URL;
|
||||
} else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
|
||||
return KeyboardId.MODE_IM;
|
||||
} else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
|
||||
return KeyboardId.MODE_TEXT;
|
||||
} else {
|
||||
return KeyboardId.MODE_TEXT;
|
||||
}
|
||||
default:
|
||||
return KeyboardId.MODE_TEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.event.Event;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardState;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardTextsSet;
|
||||
import com.amnesica.kryptey.inputmethod.latin.InputView;
|
||||
import com.amnesica.kryptey.inputmethod.latin.LatinIME;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputMethodManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.CapsModeUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LanguageOnSpacebarUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.RecapitalizeStatus;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
|
||||
public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
|
||||
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
|
||||
|
||||
private InputView mCurrentInputView;
|
||||
private int mCurrentUiMode;
|
||||
private int mCurrentTextColor = 0x0;
|
||||
private View mMainKeyboardFrame;
|
||||
private MainKeyboardView mKeyboardView;
|
||||
private LatinIME mLatinIME;
|
||||
private RichInputMethodManager mRichImm;
|
||||
|
||||
private KeyboardState mState;
|
||||
|
||||
private KeyboardLayoutSet mKeyboardLayoutSet;
|
||||
// TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}.
|
||||
private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet();
|
||||
|
||||
private KeyboardTheme mKeyboardTheme;
|
||||
private Context mThemeContext;
|
||||
|
||||
private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
|
||||
|
||||
public static KeyboardSwitcher getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private KeyboardSwitcher() {
|
||||
// Intentional empty constructor for singleton.
|
||||
}
|
||||
|
||||
public static void init(final LatinIME latinIme) {
|
||||
sInstance.initInternal(latinIme);
|
||||
}
|
||||
|
||||
private void initInternal(final LatinIME latinIme) {
|
||||
mLatinIME = latinIme;
|
||||
mRichImm = RichInputMethodManager.getInstance();
|
||||
mState = new KeyboardState(this);
|
||||
}
|
||||
|
||||
public void updateKeyboardTheme(final int uiMode) {
|
||||
final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper(
|
||||
mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME), uiMode);
|
||||
if (themeUpdated && mKeyboardView != null) {
|
||||
mLatinIME.setInputView(onCreateInputView(uiMode));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context,
|
||||
final KeyboardTheme keyboardTheme, final int uiMode) {
|
||||
int newTextColor = 0x0;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
newTextColor = context.getResources().getColor(R.color.key_text_color_lxx_system);
|
||||
}
|
||||
|
||||
if (mThemeContext == null
|
||||
|| !keyboardTheme.equals(mKeyboardTheme)
|
||||
|| mCurrentUiMode != uiMode
|
||||
|| newTextColor != mCurrentTextColor) {
|
||||
mKeyboardTheme = keyboardTheme;
|
||||
mCurrentUiMode = uiMode;
|
||||
mCurrentTextColor = newTextColor;
|
||||
mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId);
|
||||
KeyboardLayoutSet.onKeyboardThemeChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
|
||||
final int currentAutoCapsState, final int currentRecapitalizeState) {
|
||||
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
|
||||
mThemeContext, editorInfo);
|
||||
final Resources res = mThemeContext.getResources();
|
||||
final int keyboardWidth = mLatinIME.getMaxWidth();
|
||||
final int keyboardHeight = ResourceUtils.getKeyboardHeight(res, settingsValues);
|
||||
builder.setKeyboardTheme(mKeyboardTheme.mThemeId);
|
||||
builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
|
||||
builder.setSubtype(mRichImm.getCurrentSubtype());
|
||||
builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey());
|
||||
builder.setShowSpecialChars(!settingsValues.mHideSpecialChars);
|
||||
builder.setShowNumberRow(settingsValues.mShowNumberRow);
|
||||
mKeyboardLayoutSet = builder.build();
|
||||
try {
|
||||
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState);
|
||||
mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtype().getLocaleObject(),
|
||||
mThemeContext);
|
||||
} catch (KeyboardLayoutSetException e) {
|
||||
Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
public void saveKeyboardState() {
|
||||
if (getKeyboard() != null) {
|
||||
mState.onSaveKeyboardState();
|
||||
}
|
||||
}
|
||||
|
||||
public void onHideWindow() {
|
||||
if (mKeyboardView != null) {
|
||||
mKeyboardView.onHideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void setKeyboard(
|
||||
final int keyboardId,
|
||||
final KeyboardSwitchState toggleState) {
|
||||
final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent();
|
||||
setMainKeyboardFrame(currentSettingsValues, toggleState);
|
||||
// TODO: pass this object to setKeyboard instead of getting the current values.
|
||||
final MainKeyboardView keyboardView = mKeyboardView;
|
||||
final Keyboard oldKeyboard = keyboardView.getKeyboard();
|
||||
final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId);
|
||||
keyboardView.setKeyboard(newKeyboard);
|
||||
mCurrentInputView.setKeyboardTopPadding((int) newKeyboard.mTopPadding);
|
||||
keyboardView.setKeyPreviewPopupEnabled(
|
||||
currentSettingsValues.mKeyPreviewPopupOn,
|
||||
currentSettingsValues.mKeyPreviewPopupDismissDelay);
|
||||
final boolean subtypeChanged = (oldKeyboard == null)
|
||||
|| !newKeyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype);
|
||||
final int languageOnSpacebarFormatType = LanguageOnSpacebarUtils
|
||||
.getLanguageOnSpacebarFormatType(newKeyboard.mId.mSubtype);
|
||||
keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType);
|
||||
}
|
||||
|
||||
public Keyboard getKeyboard() {
|
||||
if (mKeyboardView != null) {
|
||||
return mKeyboardView.getKeyboard();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
|
||||
// when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
|
||||
public void resetKeyboardStateToAlphabet(final int currentAutoCapsState,
|
||||
final int currentRecapitalizeState) {
|
||||
mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState);
|
||||
}
|
||||
|
||||
public void onPressKey(final int code, final boolean isSinglePointer,
|
||||
final int currentAutoCapsState, final int currentRecapitalizeState) {
|
||||
mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState);
|
||||
}
|
||||
|
||||
public void onReleaseKey(final int code, final boolean withSliding,
|
||||
final int currentAutoCapsState, final int currentRecapitalizeState) {
|
||||
mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState);
|
||||
}
|
||||
|
||||
public void onFinishSlidingInput(final int currentAutoCapsState,
|
||||
final int currentRecapitalizeState) {
|
||||
mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setAlphabetKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setAlphabetKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_ALPHABET, KeyboardSwitchState.OTHER);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setAlphabetManualShiftedKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setAlphabetManualShiftedKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardSwitchState.OTHER);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setAlphabetAutomaticShiftedKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setAlphabetAutomaticShiftedKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardSwitchState.OTHER);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setAlphabetShiftLockedKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setAlphabetShiftLockedKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardSwitchState.OTHER);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setSymbolsKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setSymbolsKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_SYMBOLS, KeyboardSwitchState.OTHER);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void setSymbolsShiftedKeyboard() {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "setSymbolsShiftedKeyboard");
|
||||
}
|
||||
setKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED, KeyboardSwitchState.SYMBOLS_SHIFTED);
|
||||
}
|
||||
|
||||
public boolean isImeSuppressedByHardwareKeyboard(
|
||||
final SettingsValues settingsValues,
|
||||
final KeyboardSwitchState toggleState) {
|
||||
return settingsValues.mHasHardwareKeyboard && toggleState == KeyboardSwitchState.HIDDEN;
|
||||
}
|
||||
|
||||
private void setMainKeyboardFrame(
|
||||
final SettingsValues settingsValues,
|
||||
final KeyboardSwitchState toggleState) {
|
||||
final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState)
|
||||
? View.GONE : View.VISIBLE;
|
||||
mKeyboardView.setVisibility(visibility);
|
||||
// The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
|
||||
// @see #getVisibleKeyboardView() and
|
||||
// @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
|
||||
mMainKeyboardFrame.setVisibility(visibility);
|
||||
}
|
||||
|
||||
public enum KeyboardSwitchState {
|
||||
HIDDEN(-1),
|
||||
SYMBOLS_SHIFTED(KeyboardId.ELEMENT_SYMBOLS_SHIFTED),
|
||||
OTHER(-1);
|
||||
|
||||
final int mKeyboardId;
|
||||
|
||||
KeyboardSwitchState(int keyboardId) {
|
||||
mKeyboardId = keyboardId;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyboardSwitchState getKeyboardSwitchState() {
|
||||
boolean hidden = mKeyboardLayoutSet == null
|
||||
|| mKeyboardView == null
|
||||
|| !mKeyboardView.isShown();
|
||||
if (hidden) {
|
||||
return KeyboardSwitchState.HIDDEN;
|
||||
} else if (isShowingKeyboardId(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)) {
|
||||
return KeyboardSwitchState.SYMBOLS_SHIFTED;
|
||||
}
|
||||
return KeyboardSwitchState.OTHER;
|
||||
}
|
||||
|
||||
// Future method for requesting an updating to the shift state.
|
||||
@Override
|
||||
public void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_ACTION) {
|
||||
Log.d(TAG, "requestUpdatingShiftState: "
|
||||
+ " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
|
||||
+ " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode));
|
||||
}
|
||||
mState.onUpdateShiftState(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void startDoubleTapShiftKeyTimer() {
|
||||
if (DEBUG_TIMER_ACTION) {
|
||||
Log.d(TAG, "startDoubleTapShiftKeyTimer");
|
||||
}
|
||||
final MainKeyboardView keyboardView = getMainKeyboardView();
|
||||
if (keyboardView != null) {
|
||||
keyboardView.startDoubleTapShiftKeyTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public void cancelDoubleTapShiftKeyTimer() {
|
||||
if (DEBUG_TIMER_ACTION) {
|
||||
Log.d(TAG, "setAlphabetKeyboard");
|
||||
}
|
||||
final MainKeyboardView keyboardView = getMainKeyboardView();
|
||||
if (keyboardView != null) {
|
||||
keyboardView.cancelDoubleTapShiftKeyTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// Implements {@link KeyboardState.SwitchActions}.
|
||||
@Override
|
||||
public boolean isInDoubleTapShiftKeyTimeout() {
|
||||
if (DEBUG_TIMER_ACTION) {
|
||||
Log.d(TAG, "isInDoubleTapShiftKeyTimeout");
|
||||
}
|
||||
final MainKeyboardView keyboardView = getMainKeyboardView();
|
||||
return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates state machine to figure out when to automatically switch back to the previous mode.
|
||||
*/
|
||||
public void onEvent(final Event event, final int currentAutoCapsState,
|
||||
final int currentRecapitalizeState) {
|
||||
mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState);
|
||||
}
|
||||
|
||||
public boolean isShowingKeyboardId(int... keyboardIds) {
|
||||
if (mKeyboardView == null || !mKeyboardView.isShown()) {
|
||||
return false;
|
||||
}
|
||||
int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId;
|
||||
for (int keyboardId : keyboardIds) {
|
||||
if (activeKeyboardId == keyboardId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isShowingMoreKeysPanel() {
|
||||
return mKeyboardView.isShowingMoreKeysPanel();
|
||||
}
|
||||
|
||||
public View getVisibleKeyboardView() {
|
||||
return mKeyboardView;
|
||||
}
|
||||
|
||||
public MainKeyboardView getMainKeyboardView() {
|
||||
return mKeyboardView;
|
||||
}
|
||||
|
||||
public void deallocateMemory() {
|
||||
if (mKeyboardView != null) {
|
||||
mKeyboardView.cancelAllOngoingEvents();
|
||||
mKeyboardView.deallocateMemory();
|
||||
}
|
||||
}
|
||||
|
||||
public View onCreateInputView(final int uiMode) {
|
||||
if (mKeyboardView != null) {
|
||||
mKeyboardView.closing();
|
||||
}
|
||||
|
||||
updateKeyboardThemeAndContextThemeWrapper(
|
||||
mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */), uiMode);
|
||||
mCurrentInputView = (InputView) LayoutInflater.from(mThemeContext).inflate(
|
||||
R.layout.input_view, null);
|
||||
mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
|
||||
|
||||
mKeyboardView = mCurrentInputView.findViewById(R.id.keyboard_view);
|
||||
mKeyboardView.setKeyboardActionListener(mLatinIME);
|
||||
return mCurrentInputView;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
|
||||
|
||||
public final class KeyboardTheme {
|
||||
private static final String TAG = KeyboardTheme.class.getSimpleName();
|
||||
|
||||
static final String KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509";
|
||||
|
||||
// These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme
|
||||
//
|
||||
public static final int THEME_ID_PURE_DAY = 6;
|
||||
public static final int THEME_ID_PURE_NIGHT = 7;
|
||||
public static final int DEFAULT_THEME_ID = THEME_ID_PURE_NIGHT;
|
||||
|
||||
/* package private for testing */
|
||||
static final KeyboardTheme[] KEYBOARD_THEMES = {
|
||||
new KeyboardTheme(THEME_ID_PURE_DAY, "LXXPureDay", R.style.KeyboardTheme_LXX_Pure_Day),
|
||||
new KeyboardTheme(THEME_ID_PURE_NIGHT, "LXXPureNight", R.style.KeyboardTheme_LXX_Pure_Night),
|
||||
};
|
||||
|
||||
public final int mThemeId;
|
||||
public final int mStyleId;
|
||||
public final String mThemeName;
|
||||
|
||||
// Note: The themeId should be aligned with "themeId" attribute of Keyboard style
|
||||
// in values/themes-<style>.xml.
|
||||
private KeyboardTheme(final int themeId, final String themeName, final int styleId) {
|
||||
mThemeId = themeId;
|
||||
mThemeName = themeName;
|
||||
mStyleId = styleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o == this) return true;
|
||||
return (o instanceof KeyboardTheme) && ((KeyboardTheme) o).mThemeId == mThemeId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mThemeId;
|
||||
}
|
||||
|
||||
/* package private for testing */
|
||||
static KeyboardTheme searchKeyboardThemeById(final int themeId) {
|
||||
// TODO: This search algorithm isn't optimal if there are many themes.
|
||||
for (final KeyboardTheme theme : KEYBOARD_THEMES) {
|
||||
if (theme.mThemeId == themeId) {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* package private for testing */
|
||||
static KeyboardTheme getDefaultKeyboardTheme() {
|
||||
return searchKeyboardThemeById(DEFAULT_THEME_ID);
|
||||
}
|
||||
|
||||
public static String getKeyboardThemeName(final int themeId) {
|
||||
final KeyboardTheme theme = searchKeyboardThemeById(themeId);
|
||||
Log.i("Getting theme ID", Integer.toString(themeId));
|
||||
return theme.mThemeName;
|
||||
}
|
||||
|
||||
public static void saveKeyboardThemeId(final int themeId, final SharedPreferences prefs) {
|
||||
prefs.edit().putString(KEYBOARD_THEME_KEY, Integer.toString(themeId)).apply();
|
||||
}
|
||||
|
||||
public static KeyboardTheme getKeyboardTheme(final Context context) {
|
||||
final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(context);
|
||||
return getKeyboardTheme(prefs);
|
||||
}
|
||||
|
||||
public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) {
|
||||
final String themeIdString = prefs.getString(KEYBOARD_THEME_KEY, null);
|
||||
if (themeIdString == null) {
|
||||
return searchKeyboardThemeById(THEME_ID_PURE_NIGHT);
|
||||
}
|
||||
try {
|
||||
final int themeId = Integer.parseInt(themeIdString);
|
||||
final KeyboardTheme theme = searchKeyboardThemeById(themeId);
|
||||
if (theme != null) {
|
||||
return theme;
|
||||
}
|
||||
Log.w(TAG, "Unknown keyboard theme in preference: " + themeIdString);
|
||||
} catch (final NumberFormatException e) {
|
||||
Log.w(TAG, "Illegal keyboard theme in preference: " + themeIdString, e);
|
||||
}
|
||||
// Remove preference that contains unknown or illegal theme id.
|
||||
prefs.edit().remove(KEYBOARD_THEME_KEY).remove(Settings.PREF_KEYBOARD_COLOR).apply();
|
||||
return getDefaultKeyboardTheme();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,538 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.NinePatchDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyDrawParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyVisualAttributes;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.TypefaceUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* A view that renders a virtual {@link Keyboard}.
|
||||
*
|
||||
* @attr ref R.styleable#KeyboardView_keyBackground
|
||||
* @attr ref R.styleable#KeyboardView_functionalKeyBackground
|
||||
* @attr ref R.styleable#KeyboardView_spacebarBackground
|
||||
* @attr ref R.styleable#KeyboardView_spacebarIconWidthRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLabelFlags
|
||||
* @attr ref R.styleable#KeyboardView_keyHintLetterPadding
|
||||
* @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
|
||||
* @attr ref R.styleable#KeyboardView_keyTextShadowRadius
|
||||
* @attr ref R.styleable#KeyboardView_verticalCorrection
|
||||
* @attr ref R.styleable#Keyboard_Key_keyTypeface
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLetterSize
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLabelSize
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyLabelOffCenterRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyHintLabelOffCenterRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
|
||||
* @attr ref R.styleable#Keyboard_Key_keyTextColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
|
||||
* @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
|
||||
* @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
|
||||
*/
|
||||
public class KeyboardView extends View {
|
||||
// XML attributes
|
||||
private final KeyVisualAttributes mKeyVisualAttributes;
|
||||
// Default keyLabelFlags from {@link KeyboardTheme}.
|
||||
// Currently only "alignHintLabelToBottom" is supported.
|
||||
private final int mDefaultKeyLabelFlags;
|
||||
private final float mKeyHintLetterPadding;
|
||||
private final float mKeyShiftedLetterHintPadding;
|
||||
private final float mKeyTextShadowRadius;
|
||||
private final float mVerticalCorrection;
|
||||
private final Drawable mKeyBackground;
|
||||
private final Drawable mFunctionalKeyBackground;
|
||||
private final Drawable mSpacebarBackground;
|
||||
private final float mSpacebarIconWidthRatio;
|
||||
private final Rect mKeyBackgroundPadding = new Rect();
|
||||
private static final float KET_TEXT_SHADOW_RADIUS_DISABLED = -1.0f;
|
||||
public int mCustomColor = 0;
|
||||
|
||||
// The maximum key label width in the proportion to the key width.
|
||||
private static final float MAX_LABEL_RATIO = 0.90f;
|
||||
|
||||
// Main keyboard
|
||||
// TODO: Consider having a dummy keyboard object to make this @NonNull
|
||||
private Keyboard mKeyboard;
|
||||
private final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
|
||||
|
||||
// Drawing
|
||||
/**
|
||||
* True if all keys should be drawn
|
||||
*/
|
||||
private boolean mInvalidateAllKeys;
|
||||
/**
|
||||
* The keys that should be drawn
|
||||
*/
|
||||
private final HashSet<Key> mInvalidatedKeys = new HashSet<>();
|
||||
/**
|
||||
* The working rectangle for clipping
|
||||
*/
|
||||
private final Rect mClipRect = new Rect();
|
||||
/**
|
||||
* The keyboard bitmap buffer for faster updates
|
||||
*/
|
||||
private Bitmap mOffscreenBuffer;
|
||||
/**
|
||||
* The canvas for the above mutable keyboard bitmap
|
||||
*/
|
||||
private final Canvas mOffscreenCanvas = new Canvas();
|
||||
private final Paint mPaint = new Paint();
|
||||
private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
|
||||
|
||||
public KeyboardView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.keyboardViewStyle);
|
||||
}
|
||||
|
||||
public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
|
||||
mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
|
||||
mKeyBackground.getPadding(mKeyBackgroundPadding);
|
||||
final Drawable functionalKeyBackground = keyboardViewAttr.getDrawable(
|
||||
R.styleable.KeyboardView_functionalKeyBackground);
|
||||
mFunctionalKeyBackground = (functionalKeyBackground != null) ? functionalKeyBackground
|
||||
: mKeyBackground;
|
||||
final Drawable spacebarBackground = keyboardViewAttr.getDrawable(
|
||||
R.styleable.KeyboardView_spacebarBackground);
|
||||
mSpacebarBackground = (spacebarBackground != null) ? spacebarBackground : mKeyBackground;
|
||||
mSpacebarIconWidthRatio = keyboardViewAttr.getFloat(
|
||||
R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
|
||||
mKeyHintLetterPadding = keyboardViewAttr.getDimension(
|
||||
R.styleable.KeyboardView_keyHintLetterPadding, 0.0f);
|
||||
mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
|
||||
R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f);
|
||||
mKeyTextShadowRadius = keyboardViewAttr.getFloat(
|
||||
R.styleable.KeyboardView_keyTextShadowRadius, KET_TEXT_SHADOW_RADIUS_DISABLED);
|
||||
mVerticalCorrection = keyboardViewAttr.getDimension(
|
||||
R.styleable.KeyboardView_verticalCorrection, 0.0f);
|
||||
keyboardViewAttr.recycle();
|
||||
|
||||
final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
|
||||
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
|
||||
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
|
||||
keyAttr.recycle();
|
||||
|
||||
mPaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private static void blendAlpha(final Paint paint, final int alpha) {
|
||||
final int color = paint.getColor();
|
||||
paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
|
||||
Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
|
||||
* view will re-layout itself to accommodate the keyboard.
|
||||
*
|
||||
* @param keyboard the keyboard to display in this view
|
||||
* @see Keyboard
|
||||
* @see #getKeyboard()
|
||||
*/
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
mKeyboard = keyboard;
|
||||
final int keyHeight = keyboard.mMostCommonKeyHeight;
|
||||
mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
|
||||
mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
|
||||
final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(getContext());
|
||||
mCustomColor = Settings.readKeyboardColor(prefs, getContext());
|
||||
invalidateAllKeys();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current keyboard being displayed by this view.
|
||||
*
|
||||
* @return the currently attached keyboard
|
||||
* @see #setKeyboard(Keyboard)
|
||||
*/
|
||||
public Keyboard getKeyboard() {
|
||||
return mKeyboard;
|
||||
}
|
||||
|
||||
protected float getVerticalCorrection() {
|
||||
return mVerticalCorrection;
|
||||
}
|
||||
|
||||
protected KeyDrawParams getKeyDrawParams() {
|
||||
return mKeyDrawParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
// The main keyboard expands to the entire this {@link KeyboardView}.
|
||||
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
|
||||
final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(final Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (canvas.isHardwareAccelerated()) {
|
||||
onDrawKeyboard(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
|
||||
if (bufferNeedsUpdates || mOffscreenBuffer == null) {
|
||||
if (maybeAllocateOffscreenBuffer()) {
|
||||
mInvalidateAllKeys = true;
|
||||
// TODO: Stop using the offscreen canvas even when in software rendering
|
||||
mOffscreenCanvas.setBitmap(mOffscreenBuffer);
|
||||
}
|
||||
onDrawKeyboard(mOffscreenCanvas);
|
||||
}
|
||||
canvas.drawBitmap(mOffscreenBuffer, 0.0f, 0.0f, null);
|
||||
}
|
||||
|
||||
private boolean maybeAllocateOffscreenBuffer() {
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
if (width == 0 || height == 0) {
|
||||
return false;
|
||||
}
|
||||
if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
|
||||
&& mOffscreenBuffer.getHeight() == height) {
|
||||
return false;
|
||||
}
|
||||
freeOffscreenBuffer();
|
||||
mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void freeOffscreenBuffer() {
|
||||
mOffscreenCanvas.setBitmap(null);
|
||||
mOffscreenCanvas.setMatrix(null);
|
||||
if (mOffscreenBuffer != null) {
|
||||
mOffscreenBuffer.recycle();
|
||||
mOffscreenBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDrawKeyboard(final Canvas canvas) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Paint paint = mPaint;
|
||||
final Drawable background = getBackground();
|
||||
if (Color.alpha(mCustomColor) > 0 && keyboard.getKey(Constants.CODE_SPACE) != null) {
|
||||
setBackgroundColor(mCustomColor);
|
||||
}
|
||||
// Calculate clip region and set.
|
||||
final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
|
||||
final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
|
||||
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
|
||||
if (drawAllKeys || isHardwareAccelerated) {
|
||||
if (!isHardwareAccelerated && background != null) {
|
||||
// Need to draw keyboard background on {@link #mOffscreenBuffer}.
|
||||
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
|
||||
background.draw(canvas);
|
||||
}
|
||||
// Draw all keys.
|
||||
for (final Key key : keyboard.getSortedKeys()) {
|
||||
onDrawKey(key, canvas, paint);
|
||||
}
|
||||
} else {
|
||||
for (final Key key : mInvalidatedKeys) {
|
||||
if (!keyboard.hasKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (background != null) {
|
||||
// Need to redraw key's background on {@link #mOffscreenBuffer}.
|
||||
final int x = key.getX() + getPaddingLeft();
|
||||
final int y = key.getY() + getPaddingTop();
|
||||
mClipRect.set(x, y, x + key.getWidth(), y + key.getHeight());
|
||||
canvas.save();
|
||||
canvas.clipRect(mClipRect);
|
||||
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
|
||||
background.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
onDrawKey(key, canvas, paint);
|
||||
}
|
||||
}
|
||||
|
||||
mInvalidatedKeys.clear();
|
||||
mInvalidateAllKeys = false;
|
||||
}
|
||||
|
||||
private void onDrawKey(final Key key, final Canvas canvas,
|
||||
final Paint paint) {
|
||||
final int keyDrawX = key.getX() + getPaddingLeft();
|
||||
final int keyDrawY = key.getY() + getPaddingTop();
|
||||
canvas.translate(keyDrawX, keyDrawY);
|
||||
|
||||
final KeyVisualAttributes attr = key.getVisualAttributes();
|
||||
final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(key.getHeight(), attr);
|
||||
params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
|
||||
|
||||
if (!key.isSpacer()) {
|
||||
final Drawable background = key.selectBackgroundDrawable(
|
||||
mKeyBackground, mFunctionalKeyBackground, mSpacebarBackground);
|
||||
if (background != null) {
|
||||
onDrawKeyBackground(key, canvas, background);
|
||||
}
|
||||
}
|
||||
onDrawKeyTopVisuals(key, canvas, paint, params);
|
||||
|
||||
canvas.translate(-keyDrawX, -keyDrawY);
|
||||
}
|
||||
|
||||
// Draw key background.
|
||||
protected void onDrawKeyBackground(final Key key, final Canvas canvas,
|
||||
final Drawable background) {
|
||||
final int keyWidth = key.getWidth();
|
||||
final int keyHeight = key.getHeight();
|
||||
final Rect padding = mKeyBackgroundPadding;
|
||||
final int bgWidth = keyWidth + padding.left + padding.right;
|
||||
final int bgHeight = keyHeight + padding.top + padding.bottom;
|
||||
final int bgX = -padding.left;
|
||||
final int bgY = -padding.top;
|
||||
final Rect bounds = background.getBounds();
|
||||
if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
|
||||
background.setBounds(0, 0, bgWidth, bgHeight);
|
||||
}
|
||||
canvas.translate(bgX, bgY);
|
||||
background.draw(canvas);
|
||||
canvas.translate(-bgX, -bgY);
|
||||
}
|
||||
|
||||
// Draw key top visuals.
|
||||
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas,
|
||||
final Paint paint, final KeyDrawParams params) {
|
||||
final int keyWidth = key.getWidth();
|
||||
final int keyHeight = key.getHeight();
|
||||
final float centerX = keyWidth * 0.5f;
|
||||
final float centerY = keyHeight * 0.5f;
|
||||
|
||||
// Draw key label.
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
final Drawable icon = (keyboard == null) ? null
|
||||
: key.getIcon(keyboard.mIconsSet, params.mAnimAlpha);
|
||||
float labelX = centerX;
|
||||
float labelBaseline = centerY;
|
||||
final String label = key.getLabel();
|
||||
if (label != null) {
|
||||
paint.setTypeface(key.selectTypeface(params));
|
||||
paint.setTextSize(key.selectTextSize(params));
|
||||
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
|
||||
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
|
||||
|
||||
// Vertical label text alignment.
|
||||
labelBaseline = centerY + labelCharHeight / 2.0f;
|
||||
|
||||
// Horizontal label text alignment
|
||||
if (key.isAlignLabelOffCenter()) {
|
||||
// The label is placed off center of the key. Used mainly on "phone number" layout.
|
||||
labelX = centerX + params.mLabelOffCenterRatio * labelCharWidth;
|
||||
paint.setTextAlign(Align.LEFT);
|
||||
} else {
|
||||
labelX = centerX;
|
||||
paint.setTextAlign(Align.CENTER);
|
||||
}
|
||||
if (key.needsAutoXScale()) {
|
||||
final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) /
|
||||
TypefaceUtils.getStringWidth(label, paint));
|
||||
if (key.needsAutoScale()) {
|
||||
final float autoSize = paint.getTextSize() * ratio;
|
||||
paint.setTextSize(autoSize);
|
||||
} else {
|
||||
paint.setTextScaleX(ratio);
|
||||
}
|
||||
}
|
||||
|
||||
paint.setColor(key.selectTextColor(params));
|
||||
// Set a drop shadow for the text if the shadow radius is positive value.
|
||||
if (mKeyTextShadowRadius > 0.0f) {
|
||||
paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor);
|
||||
} else {
|
||||
paint.clearShadowLayer();
|
||||
}
|
||||
|
||||
blendAlpha(paint, params.mAnimAlpha);
|
||||
canvas.drawText(label, 0, label.length(), labelX, labelBaseline, paint);
|
||||
// Turn off drop shadow and reset x-scale.
|
||||
paint.clearShadowLayer();
|
||||
paint.setTextScaleX(1.0f);
|
||||
}
|
||||
|
||||
// Draw hint label.
|
||||
final String hintLabel = key.getHintLabel();
|
||||
if (hintLabel != null) {
|
||||
paint.setTextSize(key.selectHintTextSize(params));
|
||||
paint.setColor(key.selectHintTextColor(params));
|
||||
// TODO: Should add a way to specify type face for hint letters
|
||||
paint.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
blendAlpha(paint, params.mAnimAlpha);
|
||||
final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint);
|
||||
final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint);
|
||||
final float hintX, hintBaseline;
|
||||
if (key.hasHintLabel()) {
|
||||
// The hint label is placed just right of the key label. Used mainly on
|
||||
// "phone number" layout.
|
||||
hintX = labelX + params.mHintLabelOffCenterRatio * labelCharWidth;
|
||||
if (key.isAlignHintLabelToBottom(mDefaultKeyLabelFlags)) {
|
||||
hintBaseline = labelBaseline;
|
||||
} else {
|
||||
hintBaseline = centerY + labelCharHeight / 2.0f;
|
||||
}
|
||||
paint.setTextAlign(Align.LEFT);
|
||||
} else if (key.hasShiftedLetterHint()) {
|
||||
// The hint label is placed at top-right corner of the key. Used mainly on tablet.
|
||||
hintX = keyWidth - mKeyShiftedLetterHintPadding - labelCharWidth / 2.0f;
|
||||
paint.getFontMetrics(mFontMetrics);
|
||||
hintBaseline = -mFontMetrics.top;
|
||||
paint.setTextAlign(Align.CENTER);
|
||||
} else { // key.hasHintLetter()
|
||||
// The hint letter is placed at top-right corner of the key. Used mainly on phone.
|
||||
final float hintDigitWidth = TypefaceUtils.getReferenceDigitWidth(paint);
|
||||
final float hintLabelWidth = TypefaceUtils.getStringWidth(hintLabel, paint);
|
||||
hintX = keyWidth - mKeyHintLetterPadding
|
||||
- Math.max(hintDigitWidth, hintLabelWidth) / 2.0f;
|
||||
hintBaseline = -paint.ascent();
|
||||
paint.setTextAlign(Align.CENTER);
|
||||
}
|
||||
final float adjustmentY = params.mHintLabelVerticalAdjustment * labelCharHeight;
|
||||
canvas.drawText(
|
||||
hintLabel, 0, hintLabel.length(), hintX, hintBaseline + adjustmentY, paint);
|
||||
}
|
||||
|
||||
// Draw key icon.
|
||||
if (label == null && icon != null) {
|
||||
final int iconWidth;
|
||||
if (key.getCode() == Constants.CODE_SPACE && icon instanceof NinePatchDrawable) {
|
||||
iconWidth = (int) (keyWidth * mSpacebarIconWidthRatio);
|
||||
} else {
|
||||
iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
|
||||
}
|
||||
final int iconHeight = icon.getIntrinsicHeight();
|
||||
final int iconY;
|
||||
if (key.isAlignIconToBottom()) {
|
||||
iconY = keyHeight - iconHeight;
|
||||
} else {
|
||||
iconY = (keyHeight - iconHeight) / 2; // Align vertically center.
|
||||
}
|
||||
final int iconX = (keyWidth - iconWidth) / 2; // Align horizontally center.
|
||||
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void drawIcon(final Canvas canvas, final Drawable icon,
|
||||
final int x, final int y, final int width, final int height) {
|
||||
canvas.translate(x, y);
|
||||
icon.setBounds(0, 0, width, height);
|
||||
icon.draw(canvas);
|
||||
canvas.translate(-x, -y);
|
||||
}
|
||||
|
||||
public Paint newLabelPaint(final Key key) {
|
||||
final Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
if (key == null) {
|
||||
paint.setTypeface(mKeyDrawParams.mTypeface);
|
||||
paint.setTextSize(mKeyDrawParams.mLabelSize);
|
||||
} else {
|
||||
paint.setColor(key.selectTextColor(mKeyDrawParams));
|
||||
paint.setTypeface(key.selectTypeface(mKeyDrawParams));
|
||||
paint.setTextSize(key.selectTextSize(mKeyDrawParams));
|
||||
}
|
||||
return paint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
|
||||
* because the keyboard renders the keys to an off-screen buffer and an invalidate() only
|
||||
* draws the cached buffer.
|
||||
*
|
||||
* @see #invalidateKey(Key)
|
||||
*/
|
||||
public void invalidateAllKeys() {
|
||||
mInvalidatedKeys.clear();
|
||||
mInvalidateAllKeys = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
|
||||
* one key is changing it's content. Any changes that affect the position or size of the key
|
||||
* may not be honored.
|
||||
*
|
||||
* @param key key in the attached {@link Keyboard}.
|
||||
* @see #invalidateAllKeys
|
||||
*/
|
||||
public void invalidateKey(final Key key) {
|
||||
if (mInvalidateAllKeys || key == null) {
|
||||
return;
|
||||
}
|
||||
mInvalidatedKeys.add(key);
|
||||
final int x = key.getX() + getPaddingLeft();
|
||||
final int y = key.getY() + getPaddingTop();
|
||||
invalidate(x, y, x + key.getWidth(), y + key.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
freeOffscreenBuffer();
|
||||
}
|
||||
|
||||
public void deallocateMemory() {
|
||||
freeOffscreenBuffer();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,662 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.animation.AnimatorInflater;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.DrawingPreviewPlacerView;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.DrawingProxy;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyDrawParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyPreviewChoreographer;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyPreviewDrawParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyPreviewView;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.MoreKeySpec;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.NonDistinctMultitouchHelper;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.TimerHandler;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputMethodManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LanguageOnSpacebarUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.TypefaceUtils;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* A view that is responsible for detecting key presses and touch movements.
|
||||
*
|
||||
* @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextRatio
|
||||
* @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextColor
|
||||
* @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha
|
||||
* @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator
|
||||
* @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator
|
||||
* @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator
|
||||
* @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance
|
||||
* @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime
|
||||
* @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance
|
||||
* @attr ref R.styleable#MainKeyboardView_keySelectionByDraggingFinger
|
||||
* @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_keyRepeatInterval
|
||||
* @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_keyPreviewLayout
|
||||
* @attr ref R.styleable#MainKeyboardView_keyPreviewOffset
|
||||
* @attr ref R.styleable#MainKeyboardView_keyPreviewHeight
|
||||
* @attr ref R.styleable#MainKeyboardView_keyPreviewLingerTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_keyPreviewDismissAnimator
|
||||
* @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardLayout
|
||||
* @attr ref R.styleable#MainKeyboardView_backgroundDimAlpha
|
||||
* @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureFloatingPreviewTextLingerTimeout
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime
|
||||
* @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold
|
||||
* @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
|
||||
*/
|
||||
public final class MainKeyboardView extends KeyboardView implements MoreKeysPanel.Controller, DrawingProxy {
|
||||
private static final String TAG = MainKeyboardView.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Listener for {@link KeyboardActionListener}.
|
||||
*/
|
||||
private KeyboardActionListener mKeyboardActionListener;
|
||||
|
||||
/* Space key and its icon and background. */
|
||||
private Key mSpaceKey;
|
||||
// Stuff to draw language name on spacebar.
|
||||
private final int mLanguageOnSpacebarFinalAlpha;
|
||||
private final ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
|
||||
private int mLanguageOnSpacebarFormatType;
|
||||
private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
|
||||
private final float mLanguageOnSpacebarTextRatio;
|
||||
private float mLanguageOnSpacebarTextSize;
|
||||
private final int mLanguageOnSpacebarTextColor;
|
||||
// The minimum x-scale to fit the language name on spacebar.
|
||||
private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
|
||||
|
||||
// Stuff to draw altCodeWhileTyping keys.
|
||||
private final ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
|
||||
private final ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
|
||||
private final int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
|
||||
|
||||
// Drawing preview placer view
|
||||
private final DrawingPreviewPlacerView mDrawingPreviewPlacerView;
|
||||
private final int[] mOriginCoords = CoordinateUtils.newInstance();
|
||||
|
||||
// Key preview
|
||||
private final KeyPreviewDrawParams mKeyPreviewDrawParams;
|
||||
private final KeyPreviewChoreographer mKeyPreviewChoreographer;
|
||||
|
||||
// More keys keyboard
|
||||
private final Paint mBackgroundDimAlphaPaint = new Paint();
|
||||
private final View mMoreKeysKeyboardContainer;
|
||||
private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = new WeakHashMap<>();
|
||||
private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
|
||||
// More keys panel (used by both more keys keyboard and more suggestions view)
|
||||
// TODO: Consider extending to support multiple more keys panels
|
||||
private MoreKeysPanel mMoreKeysPanel;
|
||||
|
||||
private final KeyDetector mKeyDetector;
|
||||
private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
|
||||
|
||||
private final TimerHandler mTimerHandler;
|
||||
private final int mLanguageOnSpacebarHorizontalMargin;
|
||||
|
||||
public MainKeyboardView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.mainKeyboardViewStyle);
|
||||
}
|
||||
|
||||
public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final DrawingPreviewPlacerView drawingPreviewPlacerView =
|
||||
new DrawingPreviewPlacerView(context, attrs);
|
||||
|
||||
final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
|
||||
final int ignoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
|
||||
mTimerHandler = new TimerHandler(this, ignoreAltCodeKeyTimeout);
|
||||
|
||||
final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension(
|
||||
R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f);
|
||||
final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension(
|
||||
R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
|
||||
mKeyDetector = new KeyDetector(
|
||||
keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
|
||||
|
||||
PointerTracker.init(mainKeyboardViewAttr, mTimerHandler, this /* DrawingProxy */);
|
||||
|
||||
final boolean hasDistinctMultitouch = context.getPackageManager()
|
||||
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
|
||||
mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null
|
||||
: new NonDistinctMultitouchHelper();
|
||||
|
||||
final int backgroundDimAlpha = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_backgroundDimAlpha, 0);
|
||||
mBackgroundDimAlphaPaint.setColor(Color.BLACK);
|
||||
mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha);
|
||||
mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
|
||||
R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f);
|
||||
mLanguageOnSpacebarTextColor = mainKeyboardViewAttr.getColor(
|
||||
R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0);
|
||||
mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
|
||||
Constants.Color.ALPHA_OPAQUE);
|
||||
final int languageOnSpacebarFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
|
||||
final int altCodeKeyWhileTypingFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
|
||||
final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
|
||||
|
||||
mKeyPreviewDrawParams = new KeyPreviewDrawParams(mainKeyboardViewAttr);
|
||||
mKeyPreviewChoreographer = new KeyPreviewChoreographer(mKeyPreviewDrawParams);
|
||||
|
||||
final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0);
|
||||
mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean(
|
||||
R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
|
||||
|
||||
mainKeyboardViewAttr.recycle();
|
||||
|
||||
mDrawingPreviewPlacerView = drawingPreviewPlacerView;
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null);
|
||||
mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
|
||||
languageOnSpacebarFadeoutAnimatorResId, this);
|
||||
mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
|
||||
altCodeKeyWhileTypingFadeoutAnimatorResId, this);
|
||||
mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
|
||||
altCodeKeyWhileTypingFadeinAnimatorResId, this);
|
||||
|
||||
mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
|
||||
|
||||
mLanguageOnSpacebarHorizontalMargin = (int) getResources().getDimension(
|
||||
R.dimen.config_language_on_spacebar_horizontal_margin);
|
||||
}
|
||||
|
||||
private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
|
||||
if (resId == 0) {
|
||||
// TODO: Stop returning null.
|
||||
return null;
|
||||
}
|
||||
final ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(
|
||||
getContext(), resId);
|
||||
if (animator != null) {
|
||||
animator.setTarget(target);
|
||||
}
|
||||
return animator;
|
||||
}
|
||||
|
||||
private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
|
||||
final ObjectAnimator animatorToStart) {
|
||||
if (animatorToCancel == null || animatorToStart == null) {
|
||||
// TODO: Stop using null as a no-operation animator.
|
||||
return;
|
||||
}
|
||||
float startFraction = 0.0f;
|
||||
if (animatorToCancel.isStarted()) {
|
||||
animatorToCancel.cancel();
|
||||
startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
|
||||
}
|
||||
final long startTime = (long) (animatorToStart.getDuration() * startFraction);
|
||||
animatorToStart.start();
|
||||
animatorToStart.setCurrentPlayTime(startTime);
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#startWhileTypingAnimation(int)}.
|
||||
|
||||
/**
|
||||
* Called when a while-typing-animation should be started.
|
||||
*
|
||||
* @param fadeInOrOut {@link DrawingProxy#FADE_IN} starts while-typing-fade-in animation.
|
||||
* {@link DrawingProxy#FADE_OUT} starts while-typing-fade-out animation.
|
||||
*/
|
||||
@Override
|
||||
public void startWhileTypingAnimation(final int fadeInOrOut) {
|
||||
switch (fadeInOrOut) {
|
||||
case DrawingProxy.FADE_IN:
|
||||
cancelAndStartAnimators(
|
||||
mAltCodeKeyWhileTypingFadeoutAnimator, mAltCodeKeyWhileTypingFadeinAnimator);
|
||||
break;
|
||||
case DrawingProxy.FADE_OUT:
|
||||
cancelAndStartAnimators(
|
||||
mAltCodeKeyWhileTypingFadeinAnimator, mAltCodeKeyWhileTypingFadeoutAnimator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
|
||||
mLanguageOnSpacebarAnimAlpha = alpha;
|
||||
invalidateKey(mSpaceKey);
|
||||
}
|
||||
|
||||
public void setKeyboardActionListener(final KeyboardActionListener listener) {
|
||||
mKeyboardActionListener = listener;
|
||||
PointerTracker.setKeyboardActionListener(listener);
|
||||
}
|
||||
|
||||
// TODO: We should reconsider which coordinate system should be used to represent keyboard
|
||||
// event.
|
||||
public int getKeyX(final int x) {
|
||||
return Constants.isValidCoordinate(x) ? mKeyDetector.getTouchX(x) : x;
|
||||
}
|
||||
|
||||
// TODO: We should reconsider which coordinate system should be used to represent keyboard
|
||||
// event.
|
||||
public int getKeyY(final int y) {
|
||||
return Constants.isValidCoordinate(y) ? mKeyDetector.getTouchY(y) : y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
|
||||
* view will re-layout itself to accommodate the keyboard.
|
||||
*
|
||||
* @param keyboard the keyboard to display in this view
|
||||
* @see Keyboard
|
||||
* @see #getKeyboard()
|
||||
*/
|
||||
@Override
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
// Remove any pending messages, except dismissing preview and key repeat.
|
||||
mTimerHandler.cancelLongPressTimers();
|
||||
super.setKeyboard(keyboard);
|
||||
mKeyDetector.setKeyboard(
|
||||
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
|
||||
PointerTracker.setKeyDetector(mKeyDetector);
|
||||
mMoreKeysKeyboardCache.clear();
|
||||
|
||||
mSpaceKey = keyboard.getKey(Constants.CODE_SPACE);
|
||||
final int keyHeight = keyboard.mMostCommonKeyHeight;
|
||||
mLanguageOnSpacebarTextSize = keyHeight * mLanguageOnSpacebarTextRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the key preview popup. This is a popup that shows a magnified
|
||||
* version of the depressed key. By default the preview is enabled.
|
||||
*
|
||||
* @param previewEnabled whether or not to enable the key feedback preview
|
||||
* @param delay the delay after which the preview is dismissed
|
||||
*/
|
||||
public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
|
||||
mKeyPreviewDrawParams.setPopupEnabled(previewEnabled, delay);
|
||||
}
|
||||
|
||||
private void locatePreviewPlacerView() {
|
||||
getLocationInWindow(mOriginCoords);
|
||||
mDrawingPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords);
|
||||
}
|
||||
|
||||
private void installPreviewPlacerView() {
|
||||
final View rootView = getRootView();
|
||||
if (rootView == null) {
|
||||
Log.w(TAG, "Cannot find root view");
|
||||
return;
|
||||
}
|
||||
final ViewGroup windowContentView = rootView.findViewById(android.R.id.content);
|
||||
// Note: It'd be very weird if we get null by android.R.id.content.
|
||||
if (windowContentView == null) {
|
||||
Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView");
|
||||
return;
|
||||
}
|
||||
windowContentView.addView(mDrawingPreviewPlacerView);
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#onKeyPressed(Key,boolean)}.
|
||||
@Override
|
||||
public void onKeyPressed(final Key key, final boolean withPreview) {
|
||||
key.onPressed();
|
||||
invalidateKey(key);
|
||||
if (withPreview && !key.noKeyPreview()) {
|
||||
showKeyPreview(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void showKeyPreview(final Key key) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
|
||||
if (!previewParams.isPopupEnabled()) {
|
||||
previewParams.setVisibleOffset(-Math.round(keyboard.mVerticalGap));
|
||||
return;
|
||||
}
|
||||
|
||||
locatePreviewPlacerView();
|
||||
getLocationInWindow(mOriginCoords);
|
||||
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, getKeyDrawParams(),
|
||||
mOriginCoords, mDrawingPreviewPlacerView, isHardwareAccelerated());
|
||||
}
|
||||
|
||||
private void dismissKeyPreviewWithoutDelay(final Key key) {
|
||||
mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */);
|
||||
invalidateKey(key);
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#onKeyReleased(Key,boolean)}.
|
||||
@Override
|
||||
public void onKeyReleased(final Key key, final boolean withAnimation) {
|
||||
key.onReleased();
|
||||
invalidateKey(key);
|
||||
if (!key.noKeyPreview()) {
|
||||
if (withAnimation) {
|
||||
dismissKeyPreview(key);
|
||||
} else {
|
||||
dismissKeyPreviewWithoutDelay(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissKeyPreview(final Key key) {
|
||||
if (isHardwareAccelerated()) {
|
||||
mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */);
|
||||
return;
|
||||
}
|
||||
// TODO: Implement preference option to control key preview method and duration.
|
||||
mTimerHandler.postDismissKeyPreview(key, mKeyPreviewDrawParams.getLingerTimeout());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
installPreviewPlacerView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mDrawingPreviewPlacerView.removeAllViews();
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy@showMoreKeysKeyboard(Key,PointerTracker)}.
|
||||
//@Override
|
||||
public MoreKeysPanel showMoreKeysKeyboard(final Key key,
|
||||
final PointerTracker tracker) {
|
||||
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
||||
if (moreKeys == null) {
|
||||
return null;
|
||||
}
|
||||
Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
|
||||
if (moreKeysKeyboard == null) {
|
||||
// {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at
|
||||
// {@link KeyPreviewChoreographer#placeKeyPreview(Key,TextView,KeyboardIconsSet,KeyDrawParams,int,int[]},
|
||||
// though there may be some chances that the value is zero. <code>width == 0</code>
|
||||
// will cause zero-division error at
|
||||
// {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}.
|
||||
final boolean isSingleMoreKeyWithPreview = mKeyPreviewDrawParams.isPopupEnabled()
|
||||
&& !key.noKeyPreview() && moreKeys.length == 1
|
||||
&& mKeyPreviewDrawParams.getVisibleWidth() > 0;
|
||||
final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder(
|
||||
getContext(), key, getKeyboard(), isSingleMoreKeyWithPreview,
|
||||
mKeyPreviewDrawParams.getVisibleWidth(),
|
||||
mKeyPreviewDrawParams.getVisibleHeight(), newLabelPaint(key));
|
||||
moreKeysKeyboard = builder.build();
|
||||
mMoreKeysKeyboardCache.put(key, moreKeysKeyboard);
|
||||
}
|
||||
|
||||
final MoreKeysKeyboardView moreKeysKeyboardView =
|
||||
mMoreKeysKeyboardContainer.findViewById(R.id.more_keys_keyboard_view);
|
||||
moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
|
||||
mMoreKeysKeyboardContainer.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
final int[] lastCoords = CoordinateUtils.newInstance();
|
||||
tracker.getLastCoordinates(lastCoords);
|
||||
final boolean keyPreviewEnabled = mKeyPreviewDrawParams.isPopupEnabled()
|
||||
&& !key.noKeyPreview();
|
||||
// The more keys keyboard is usually horizontally aligned with the center of the parent key.
|
||||
// If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
|
||||
// keys keyboard is placed at the touch point of the parent key.
|
||||
final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
|
||||
? CoordinateUtils.x(lastCoords)
|
||||
: key.getX() + key.getWidth() / 2;
|
||||
// The more keys keyboard is usually vertically aligned with the top edge of the parent key
|
||||
// (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
|
||||
// aligned with the bottom edge of the visible part of the key preview.
|
||||
// {@code mPreviewVisibleOffset} has been set appropriately in
|
||||
// {@link KeyboardView#showKeyPreview(PointerTracker)}.
|
||||
final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset()
|
||||
+ Math.round(moreKeysKeyboard.mBottomPadding);
|
||||
moreKeysKeyboardView.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
|
||||
return moreKeysKeyboardView;
|
||||
}
|
||||
|
||||
public boolean isInDraggingFinger() {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
return true;
|
||||
}
|
||||
return PointerTracker.isAnyInDraggingFinger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
|
||||
locatePreviewPlacerView();
|
||||
// Dismiss another {@link MoreKeysPanel} that may be being showed.
|
||||
onDismissMoreKeysPanel();
|
||||
// Dismiss all key previews that may be being showed.
|
||||
PointerTracker.setReleasedKeyGraphicsToAllKeys();
|
||||
// Dismiss sliding key input preview that may be being showed.
|
||||
panel.showInParent(mDrawingPreviewPlacerView);
|
||||
mMoreKeysPanel = panel;
|
||||
}
|
||||
|
||||
public boolean isShowingMoreKeysPanel() {
|
||||
return mMoreKeysPanel != null && mMoreKeysPanel.isShowingInParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelMoreKeysPanel() {
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissMoreKeysPanel() {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
mMoreKeysPanel.removeFromParent();
|
||||
mMoreKeysPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void startDoubleTapShiftKeyTimer() {
|
||||
mTimerHandler.startDoubleTapShiftKeyTimer();
|
||||
}
|
||||
|
||||
public void cancelDoubleTapShiftKeyTimer() {
|
||||
mTimerHandler.cancelDoubleTapShiftKeyTimer();
|
||||
}
|
||||
|
||||
public boolean isInDoubleTapShiftKeyTimeout() {
|
||||
return mTimerHandler.isInDoubleTapShiftKeyTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (getKeyboard() == null) {
|
||||
return false;
|
||||
}
|
||||
if (mNonDistinctMultitouchHelper != null) {
|
||||
if (event.getPointerCount() > 1 && mTimerHandler.isInKeyRepeat()) {
|
||||
// Key repeating timer will be canceled if 2 or more keys are in action.
|
||||
mTimerHandler.cancelKeyRepeatTimers();
|
||||
}
|
||||
// Non distinct multitouch screen support
|
||||
mNonDistinctMultitouchHelper.processMotionEvent(event, mKeyDetector);
|
||||
return true;
|
||||
}
|
||||
return processMotionEvent(event);
|
||||
}
|
||||
|
||||
public boolean processMotionEvent(final MotionEvent event) {
|
||||
final int index = event.getActionIndex();
|
||||
final int id = event.getPointerId(index);
|
||||
final PointerTracker tracker = PointerTracker.getPointerTracker(id);
|
||||
// When a more keys panel is showing, we should ignore other fingers' single touch events
|
||||
// other than the finger that is showing the more keys panel.
|
||||
if (isShowingMoreKeysPanel() && !tracker.isShowingMoreKeysPanel()
|
||||
&& PointerTracker.getActivePointerTrackerCount() == 1) {
|
||||
return true;
|
||||
}
|
||||
tracker.processMotionEvent(event, mKeyDetector);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void cancelAllOngoingEvents() {
|
||||
mTimerHandler.cancelAllMessages();
|
||||
PointerTracker.setReleasedKeyGraphicsToAllKeys();
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
PointerTracker.cancelAllPointerTrackers();
|
||||
}
|
||||
|
||||
public void closing() {
|
||||
cancelAllOngoingEvents();
|
||||
mMoreKeysKeyboardCache.clear();
|
||||
}
|
||||
|
||||
public void onHideWindow() {
|
||||
onDismissMoreKeysPanel();
|
||||
}
|
||||
|
||||
public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
|
||||
final int languageOnSpacebarFormatType) {
|
||||
if (subtypeChanged) {
|
||||
KeyPreviewView.clearTextCache();
|
||||
}
|
||||
mLanguageOnSpacebarFormatType = languageOnSpacebarFormatType;
|
||||
final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
|
||||
if (animator == null) {
|
||||
mLanguageOnSpacebarFormatType = LanguageOnSpacebarUtils.FORMAT_TYPE_NONE;
|
||||
} else {
|
||||
if (subtypeChanged
|
||||
&& languageOnSpacebarFormatType != LanguageOnSpacebarUtils.FORMAT_TYPE_NONE) {
|
||||
setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
|
||||
if (animator.isStarted()) {
|
||||
animator.cancel();
|
||||
}
|
||||
animator.start();
|
||||
} else {
|
||||
if (!animator.isStarted()) {
|
||||
mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidateKey(mSpaceKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
|
||||
final KeyDrawParams params) {
|
||||
if (key.altCodeWhileTyping()) {
|
||||
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
|
||||
}
|
||||
super.onDrawKeyTopVisuals(key, canvas, paint, params);
|
||||
final int code = key.getCode();
|
||||
if (code == Constants.CODE_SPACE) {
|
||||
// If more than one language is enabled in current input method
|
||||
final RichInputMethodManager imm = RichInputMethodManager.getInstance();
|
||||
if (imm.hasMultipleEnabledSubtypes()) {
|
||||
drawLanguageOnSpacebar(key, canvas, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) {
|
||||
final int maxTextWidth = width - mLanguageOnSpacebarHorizontalMargin * 2;
|
||||
paint.setTextScaleX(1.0f);
|
||||
final float textWidth = TypefaceUtils.getStringWidth(text, paint);
|
||||
if (textWidth < width) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final float scaleX = maxTextWidth / textWidth;
|
||||
if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
paint.setTextScaleX(scaleX);
|
||||
return TypefaceUtils.getStringWidth(text, paint) < maxTextWidth;
|
||||
}
|
||||
|
||||
// Layout language name on spacebar.
|
||||
private String layoutLanguageOnSpacebar(final Paint paint,
|
||||
final Subtype subtype, final int width) {
|
||||
// Choose appropriate language name to fit into the width.
|
||||
if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarUtils.FORMAT_TYPE_FULL_LOCALE) {
|
||||
final String fullText =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInLocale(subtype.getLocale());
|
||||
if (fitsTextIntoWidth(width, fullText, paint)) {
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
|
||||
final String middleText =
|
||||
LocaleResourceUtils.getLanguageDisplayNameInLocale(subtype.getLocale());
|
||||
if (fitsTextIntoWidth(width, middleText, paint)) {
|
||||
return middleText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private void drawLanguageOnSpacebar(final Key key, final Canvas canvas, final Paint paint) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
final int width = key.getWidth();
|
||||
final int height = key.getHeight();
|
||||
paint.setTextAlign(Align.CENTER);
|
||||
paint.setTypeface(Typeface.DEFAULT);
|
||||
paint.setTextSize(mLanguageOnSpacebarTextSize);
|
||||
final String language = layoutLanguageOnSpacebar(paint, keyboard.mId.mSubtype, width);
|
||||
// Draw language text with shadow
|
||||
final float descent = paint.descent();
|
||||
final float textHeight = -paint.ascent() + descent;
|
||||
final float baseline = height / 2 + textHeight / 2;
|
||||
paint.setColor(mLanguageOnSpacebarTextColor);
|
||||
paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
|
||||
canvas.drawText(language, width / 2, baseline - descent, paint);
|
||||
paint.clearShadowLayer();
|
||||
paint.setTextScaleX(1.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
public final class MoreKeysDetector extends KeyDetector {
|
||||
private final int mSlideAllowanceSquare;
|
||||
private final int mSlideAllowanceSquareTop;
|
||||
|
||||
public MoreKeysDetector(float slideAllowance) {
|
||||
super();
|
||||
mSlideAllowanceSquare = (int) (slideAllowance * slideAllowance);
|
||||
// Top slide allowance is slightly longer (sqrt(2) times) than other edges.
|
||||
mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean alwaysAllowsKeySelectionByDraggingFinger() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key detectHitKey(final int x, final int y) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return null;
|
||||
}
|
||||
final int touchX = getTouchX(x);
|
||||
final int touchY = getTouchY(y);
|
||||
|
||||
Key nearestKey = null;
|
||||
int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
|
||||
for (final Key key : keyboard.getSortedKeys()) {
|
||||
final int dist = key.squaredDistanceToHitboxEdge(touchX, touchY);
|
||||
if (dist < nearestDist) {
|
||||
nearestKey = key;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
return nearestKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardBuilder;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.MoreKeySpec;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.TypefaceUtils;
|
||||
|
||||
public final class MoreKeysKeyboard extends Keyboard {
|
||||
private static final String TAG = MoreKeysKeyboard.class.getSimpleName();
|
||||
private final int mDefaultKeyCoordX;
|
||||
private static final float FLOAT_THRESHOLD = 0.0001f;
|
||||
|
||||
MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
|
||||
super(params);
|
||||
mDefaultKeyCoordX = Math.round(params.getDefaultKeyCoordX() + params.mOffsetX
|
||||
+ (params.mDefaultKeyPaddedWidth - params.mHorizontalGap) / 2);
|
||||
}
|
||||
|
||||
public int getDefaultCoordX() {
|
||||
return mDefaultKeyCoordX;
|
||||
}
|
||||
|
||||
static class MoreKeysKeyboardParams extends KeyboardParams {
|
||||
public boolean mIsMoreKeysFixedOrder;
|
||||
/* package */ int mTopRowAdjustment;
|
||||
public int mNumRows;
|
||||
public int mNumColumns;
|
||||
public int mTopKeys;
|
||||
public int mLeftKeys;
|
||||
public int mRightKeys; // includes default key.
|
||||
public float mColumnWidth;
|
||||
public float mOffsetX;
|
||||
|
||||
public MoreKeysKeyboardParams() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set keyboard parameters of more keys keyboard.
|
||||
*
|
||||
* @param numKeys number of keys in this more keys keyboard.
|
||||
* @param numColumn number of columns of this more keys keyboard.
|
||||
* @param keyPaddedWidth more keys keyboard key width in pixel, including horizontal gap.
|
||||
* @param rowHeight more keys keyboard row height in pixel, including vertical gap.
|
||||
* @param coordXInParent coordinate x of the key preview in parent keyboard.
|
||||
* @param parentKeyboardWidth parent keyboard width in pixel.
|
||||
* @param isMoreKeysFixedColumn true if more keys keyboard should have
|
||||
* <code>numColumn</code> columns. Otherwise more keys keyboard should have
|
||||
* <code>numColumn</code> columns at most.
|
||||
* @param isMoreKeysFixedOrder true if the order of more keys is determined by the order in
|
||||
* the more keys' specification. Otherwise the order of more keys is automatically
|
||||
* determined.
|
||||
*/
|
||||
public void setParameters(final int numKeys, final int numColumn,
|
||||
final float keyPaddedWidth, final float rowHeight,
|
||||
final float coordXInParent, final int parentKeyboardWidth,
|
||||
final boolean isMoreKeysFixedColumn,
|
||||
final boolean isMoreKeysFixedOrder) {
|
||||
// Add the horizontal padding because there is no horizontal gap on the outside edge,
|
||||
// but it is included in the key width, so this compensates for simple division and
|
||||
// comparison.
|
||||
final float availableWidth = parentKeyboardWidth - mLeftPadding - mRightPadding
|
||||
+ mHorizontalGap;
|
||||
if (availableWidth < keyPaddedWidth) {
|
||||
throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
|
||||
+ availableWidth + " " + keyPaddedWidth);
|
||||
}
|
||||
mIsMoreKeysFixedOrder = isMoreKeysFixedOrder;
|
||||
mDefaultKeyPaddedWidth = keyPaddedWidth;
|
||||
mDefaultRowHeight = rowHeight;
|
||||
|
||||
final int maxColumns = getMaxKeys(availableWidth, keyPaddedWidth);
|
||||
if (isMoreKeysFixedColumn) {
|
||||
int requestedNumColumns = Math.min(numKeys, numColumn);
|
||||
if (maxColumns < requestedNumColumns) {
|
||||
Log.e(TAG, "Keyboard is too small to hold the requested more keys columns: "
|
||||
+ availableWidth + " " + keyPaddedWidth + " " + numKeys + " "
|
||||
+ requestedNumColumns + ". The number of columns was reduced.");
|
||||
mNumColumns = maxColumns;
|
||||
} else {
|
||||
mNumColumns = requestedNumColumns;
|
||||
}
|
||||
mNumRows = getNumRows(numKeys, mNumColumns);
|
||||
} else {
|
||||
int defaultNumColumns = Math.min(maxColumns, numColumn);
|
||||
mNumRows = getNumRows(numKeys, defaultNumColumns);
|
||||
mNumColumns = getOptimizedColumns(numKeys, defaultNumColumns, mNumRows);
|
||||
}
|
||||
final int topKeys = numKeys % mNumColumns;
|
||||
mTopKeys = topKeys == 0 ? mNumColumns : topKeys;
|
||||
|
||||
final int numLeftKeys = (mNumColumns - 1) / 2;
|
||||
final int numRightKeys = mNumColumns - numLeftKeys; // including default key.
|
||||
// Determine the maximum number of keys we can lay out on both side of the left edge of
|
||||
// a key centered on the parent key. Also, account for horizontal padding because there
|
||||
// is no horizontal gap on the outside edge.
|
||||
final float leftWidth = Math.max(coordXInParent - mLeftPadding - keyPaddedWidth / 2
|
||||
+ mHorizontalGap / 2, 0);
|
||||
final float rightWidth = Math.max(parentKeyboardWidth - coordXInParent
|
||||
+ keyPaddedWidth / 2 - mRightPadding + mHorizontalGap / 2, 0);
|
||||
int maxLeftKeys = getMaxKeys(leftWidth, keyPaddedWidth);
|
||||
int maxRightKeys = getMaxKeys(rightWidth, keyPaddedWidth);
|
||||
// Handle the case where the number of columns fits but doesn't have enough room
|
||||
// for the default key to be centered on the parent key.
|
||||
if (numKeys >= mNumColumns && mNumColumns == maxColumns
|
||||
&& maxLeftKeys + maxRightKeys < maxColumns) {
|
||||
final float extraLeft = leftWidth - maxLeftKeys * keyPaddedWidth;
|
||||
final float extraRight = rightWidth - maxRightKeys * keyPaddedWidth;
|
||||
// Put the extra key on whatever side has more space
|
||||
if (extraLeft > extraRight) {
|
||||
maxLeftKeys++;
|
||||
} else {
|
||||
maxRightKeys++;
|
||||
}
|
||||
}
|
||||
|
||||
int leftKeys, rightKeys;
|
||||
if (numLeftKeys > maxLeftKeys) {
|
||||
leftKeys = maxLeftKeys;
|
||||
rightKeys = mNumColumns - leftKeys;
|
||||
} else if (numRightKeys > maxRightKeys) {
|
||||
// Make sure the default key is included even if it doesn't exactly fit (the default
|
||||
// key just won't be completely centered on the parent key)
|
||||
rightKeys = Math.max(maxRightKeys, 1);
|
||||
leftKeys = mNumColumns - rightKeys;
|
||||
} else {
|
||||
leftKeys = numLeftKeys;
|
||||
rightKeys = numRightKeys;
|
||||
}
|
||||
mLeftKeys = leftKeys;
|
||||
mRightKeys = rightKeys;
|
||||
|
||||
// Adjustment of the top row.
|
||||
mTopRowAdjustment = getTopRowAdjustment();
|
||||
mColumnWidth = mDefaultKeyPaddedWidth;
|
||||
mBaseWidth = mNumColumns * mColumnWidth;
|
||||
// Need to subtract the right most column's gutter only.
|
||||
mOccupiedWidth = Math.round(mBaseWidth + mLeftPadding + mRightPadding - mHorizontalGap);
|
||||
mBaseHeight = mNumRows * mDefaultRowHeight;
|
||||
// Need to subtract the bottom row's gutter only.
|
||||
mOccupiedHeight = Math.round(mBaseHeight + mTopPadding + mBottomPadding - mVerticalGap);
|
||||
|
||||
// The proximity grid size can be reduced because the more keys keyboard is probably
|
||||
// smaller and doesn't need extra precision from smaller cells.
|
||||
mGridWidth = Math.min(mGridWidth, mNumColumns);
|
||||
mGridHeight = Math.min(mGridHeight, mNumRows);
|
||||
}
|
||||
|
||||
private int getTopRowAdjustment() {
|
||||
final int numOffCenterKeys = Math.abs(mRightKeys - 1 - mLeftKeys);
|
||||
// Don't center if there are more keys in the top row than can be centered around the
|
||||
// default more key or if there is an odd number of keys in the top row (already will
|
||||
// be centered).
|
||||
if (mTopKeys > mNumColumns - numOffCenterKeys || mTopKeys % 2 == 1) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Return key position according to column count (0 is default).
|
||||
/* package */int getColumnPos(final int n) {
|
||||
return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
|
||||
}
|
||||
|
||||
private int getFixedOrderColumnPos(final int n) {
|
||||
final int col = n % mNumColumns;
|
||||
final int row = n / mNumColumns;
|
||||
if (!isTopRow(row)) {
|
||||
return col - mLeftKeys;
|
||||
}
|
||||
final int rightSideKeys = mTopKeys / 2;
|
||||
final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
|
||||
final int pos = col - leftSideKeys;
|
||||
final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
|
||||
final int numRightKeys = mRightKeys - 1;
|
||||
if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
|
||||
return pos;
|
||||
} else if (numRightKeys < rightSideKeys) {
|
||||
return pos - (rightSideKeys - numRightKeys);
|
||||
} else { // numLeftKeys < leftSideKeys
|
||||
return pos + (leftSideKeys - numLeftKeys);
|
||||
}
|
||||
}
|
||||
|
||||
private int getAutomaticColumnPos(final int n) {
|
||||
final int col = n % mNumColumns;
|
||||
final int row = n / mNumColumns;
|
||||
int leftKeys = mLeftKeys;
|
||||
if (isTopRow(row)) {
|
||||
leftKeys += mTopRowAdjustment;
|
||||
}
|
||||
if (col == 0) {
|
||||
// default position.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
int right = 1; // include default position key.
|
||||
int left = 0;
|
||||
int i = 0;
|
||||
while (true) {
|
||||
// Assign right key if available.
|
||||
if (right < mRightKeys) {
|
||||
pos = right;
|
||||
right++;
|
||||
i++;
|
||||
}
|
||||
if (i >= col)
|
||||
break;
|
||||
// Assign left key if available.
|
||||
if (left < leftKeys) {
|
||||
left++;
|
||||
pos = -left;
|
||||
i++;
|
||||
}
|
||||
if (i >= col)
|
||||
break;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
|
||||
final int remainings = numKeys % numColumns;
|
||||
return remainings == 0 ? 0 : numColumns - remainings;
|
||||
}
|
||||
|
||||
private static int getOptimizedColumns(final int numKeys, final int maxColumns,
|
||||
final int numRows) {
|
||||
int numColumns = Math.min(numKeys, maxColumns);
|
||||
while (getTopRowEmptySlots(numKeys, numColumns) >= numRows) {
|
||||
numColumns--;
|
||||
}
|
||||
return numColumns;
|
||||
}
|
||||
|
||||
private static int getNumRows(final int numKeys, final int numColumn) {
|
||||
return (numKeys + numColumn - 1) / numColumn;
|
||||
}
|
||||
|
||||
private static int getMaxKeys(final float keyboardWidth, final float keyPaddedWidth) {
|
||||
// This is effectively the same as returning (int)(keyboardWidth / keyPaddedWidth)
|
||||
// except this handles floating point errors better since rounding in the wrong
|
||||
// directing here doesn't cause an issue, but truncating incorrectly from an error
|
||||
// could be a problem (eg: the keyboard width is an exact multiple of the key width
|
||||
// could return one less than the expected number).
|
||||
final int maxKeys = Math.round(keyboardWidth / keyPaddedWidth);
|
||||
if (maxKeys * keyPaddedWidth > keyboardWidth + FLOAT_THRESHOLD) {
|
||||
return maxKeys - 1;
|
||||
}
|
||||
return maxKeys;
|
||||
}
|
||||
|
||||
public float getDefaultKeyCoordX() {
|
||||
return mLeftKeys * mColumnWidth + mLeftPadding;
|
||||
}
|
||||
|
||||
public float getX(final int n, final int row) {
|
||||
final float x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
|
||||
if (isTopRow(row)) {
|
||||
return x + mTopRowAdjustment * (mColumnWidth / 2);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY(final int row) {
|
||||
return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
|
||||
}
|
||||
|
||||
private boolean isTopRow(final int rowCount) {
|
||||
return mNumRows > 1 && rowCount == mNumRows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
|
||||
private final Key mParentKey;
|
||||
|
||||
private static final float LABEL_PADDING_RATIO = 0.2f;
|
||||
|
||||
/**
|
||||
* The builder of MoreKeysKeyboard.
|
||||
*
|
||||
* @param context the context of {@link MoreKeysKeyboardView}.
|
||||
* @param key the {@link Key} that invokes more keys keyboard.
|
||||
* @param keyboard the {@link Keyboard} that contains the parentKey.
|
||||
* @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single
|
||||
* "more key" and its key popup preview is enabled.
|
||||
* @param keyPreviewVisibleWidth the width of visible part of key popup preview.
|
||||
* @param keyPreviewVisibleHeight the height of visible part of key popup preview
|
||||
* @param paintToMeasure the {@link Paint} object to measure a "more key" width
|
||||
*/
|
||||
public Builder(final Context context, final Key key, final Keyboard keyboard,
|
||||
final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
|
||||
final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
|
||||
super(context, new MoreKeysKeyboardParams());
|
||||
load(keyboard.mMoreKeysTemplate, keyboard.mId);
|
||||
|
||||
// TODO: More keys keyboard's vertical gap is currently calculated heuristically.
|
||||
// Should revise the algorithm.
|
||||
mParams.mVerticalGap = keyboard.mVerticalGap / 2;
|
||||
// This {@link MoreKeysKeyboard} is invoked from the <code>key</code>.
|
||||
mParentKey = key;
|
||||
|
||||
final float keyPaddedWidth, rowHeight;
|
||||
if (isSingleMoreKeyWithPreview) {
|
||||
// Use pre-computed width and height if this more keys keyboard has only one key to
|
||||
// mitigate visual flicker between key preview and more keys keyboard.
|
||||
// The bottom paddings don't need to be considered because the vertical positions
|
||||
// of both backgrounds and the keyboard were already adjusted with their bottom
|
||||
// paddings deducted. The keyboard's left/right/top paddings do need to be deducted
|
||||
// so the key including the paddings matches the key preview.
|
||||
final float keyboardHorizontalPadding = mParams.mLeftPadding
|
||||
+ mParams.mRightPadding;
|
||||
final float baseKeyPaddedWidth = keyPreviewVisibleWidth + mParams.mHorizontalGap;
|
||||
if (keyboardHorizontalPadding > baseKeyPaddedWidth - FLOAT_THRESHOLD) {
|
||||
// If the padding doesn't fit we'll just add it outside of the key preview.
|
||||
keyPaddedWidth = baseKeyPaddedWidth;
|
||||
} else {
|
||||
keyPaddedWidth = baseKeyPaddedWidth - keyboardHorizontalPadding;
|
||||
// Keep the more keys keyboard with uneven padding lined up with the key
|
||||
// preview rather than centering the more keys keyboard's key with the parent
|
||||
// key.
|
||||
mParams.mOffsetX = (mParams.mRightPadding - mParams.mLeftPadding) / 2;
|
||||
}
|
||||
final float baseKeyPaddedHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
|
||||
if (mParams.mTopPadding > baseKeyPaddedHeight - FLOAT_THRESHOLD) {
|
||||
// If the padding doesn't fit we'll just add it outside of the key preview.
|
||||
rowHeight = baseKeyPaddedHeight;
|
||||
} else {
|
||||
rowHeight = baseKeyPaddedHeight - mParams.mTopPadding;
|
||||
}
|
||||
} else {
|
||||
final float defaultKeyWidth = mParams.mDefaultKeyPaddedWidth
|
||||
- mParams.mHorizontalGap;
|
||||
final float padding = context.getResources().getDimension(
|
||||
R.dimen.config_more_keys_keyboard_key_horizontal_padding)
|
||||
+ (key.hasLabelsInMoreKeys()
|
||||
? defaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
|
||||
keyPaddedWidth = getMaxKeyWidth(key, defaultKeyWidth, padding, paintToMeasure)
|
||||
+ mParams.mHorizontalGap;
|
||||
rowHeight = keyboard.mMostCommonKeyHeight + keyboard.mVerticalGap;
|
||||
}
|
||||
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
||||
mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyPaddedWidth,
|
||||
rowHeight, key.getX() + key.getWidth() / 2f, keyboard.mId.mWidth,
|
||||
key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder());
|
||||
}
|
||||
|
||||
private static float getMaxKeyWidth(final Key parentKey, final float minKeyWidth,
|
||||
final float padding, final Paint paint) {
|
||||
float maxWidth = minKeyWidth;
|
||||
for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
|
||||
final String label = spec.mLabel;
|
||||
// If the label is single letter, minKeyWidth is enough to hold the label.
|
||||
if (label != null && StringUtils.codePointCount(label) > 1) {
|
||||
maxWidth = Math.max(maxWidth,
|
||||
TypefaceUtils.getStringWidth(label, paint) + padding);
|
||||
}
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MoreKeysKeyboard build() {
|
||||
final MoreKeysKeyboardParams params = mParams;
|
||||
final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
|
||||
final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
|
||||
for (int n = 0; n < moreKeys.length; n++) {
|
||||
final MoreKeySpec moreKeySpec = moreKeys[n];
|
||||
final int row = n / params.mNumColumns;
|
||||
final float width = params.mDefaultKeyPaddedWidth - params.mHorizontalGap;
|
||||
final float height = params.mDefaultRowHeight - params.mVerticalGap;
|
||||
final float keyLeftEdge = params.getX(n, row);
|
||||
final float keyTopEdge = params.getY(row);
|
||||
final float keyRightEdge = keyLeftEdge + width;
|
||||
final float keyBottomEdge = keyTopEdge + height;
|
||||
|
||||
final float keyboardLeftEdge = params.mLeftPadding;
|
||||
final float keyboardRightEdge = params.mOccupiedWidth - params.mRightPadding;
|
||||
final float keyboardTopEdge = params.mTopPadding;
|
||||
final float keyboardBottomEdge = params.mOccupiedHeight - params.mBottomPadding;
|
||||
|
||||
final float keyLeftPadding = keyLeftEdge < keyboardLeftEdge + FLOAT_THRESHOLD
|
||||
? params.mLeftPadding : params.mHorizontalGap / 2;
|
||||
final float keyRightPadding = keyRightEdge > keyboardRightEdge - FLOAT_THRESHOLD
|
||||
? params.mRightPadding : params.mHorizontalGap / 2;
|
||||
final float keyTopPadding = keyTopEdge < keyboardTopEdge + FLOAT_THRESHOLD
|
||||
? params.mTopPadding : params.mVerticalGap / 2;
|
||||
final float keyBottomPadding = keyBottomEdge > keyboardBottomEdge - FLOAT_THRESHOLD
|
||||
? params.mBottomPadding : params.mVerticalGap / 2;
|
||||
|
||||
final Key key = moreKeySpec.buildKey(keyLeftEdge, keyTopEdge, width, height,
|
||||
keyLeftPadding, keyRightPadding, keyTopPadding, keyBottomPadding,
|
||||
moreKeyFlags);
|
||||
params.onAddKey(key);
|
||||
}
|
||||
return new MoreKeysKeyboard(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
|
||||
/**
|
||||
* A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
|
||||
* detecting key presses and touch movements.
|
||||
*/
|
||||
public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
|
||||
private final int[] mCoordinates = CoordinateUtils.newInstance();
|
||||
|
||||
protected final KeyDetector mKeyDetector;
|
||||
private Controller mController = EMPTY_CONTROLLER;
|
||||
protected KeyboardActionListener mListener;
|
||||
private int mOriginX;
|
||||
private int mOriginY;
|
||||
private Key mCurrentKey;
|
||||
|
||||
private int mActivePointerId;
|
||||
|
||||
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
|
||||
}
|
||||
|
||||
public MoreKeysKeyboardView(final Context context, final AttributeSet attrs,
|
||||
final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
final TypedArray moreKeysKeyboardViewAttr = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.MoreKeysKeyboardView, defStyle, R.style.MoreKeysKeyboardView);
|
||||
moreKeysKeyboardViewAttr.recycle();
|
||||
mKeyDetector = new MoreKeysDetector(getResources().getDimension(
|
||||
R.dimen.config_more_keys_keyboard_slide_allowance));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard != null) {
|
||||
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
|
||||
final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
|
||||
setMeasuredDimension(width, height);
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
super.setKeyboard(keyboard);
|
||||
mKeyDetector.setKeyboard(
|
||||
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMoreKeysPanel(final View parentView, final Controller controller,
|
||||
final int pointX, final int pointY, final KeyboardActionListener listener) {
|
||||
mController = controller;
|
||||
mListener = listener;
|
||||
final View container = getContainerView();
|
||||
// The coordinates of panel's left-top corner in parentView's coordinate system.
|
||||
// We need to consider background drawable paddings.
|
||||
final int x = pointX - getDefaultCoordX() - container.getPaddingLeft() - getPaddingLeft();
|
||||
final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom()
|
||||
+ getPaddingBottom();
|
||||
|
||||
parentView.getLocationInWindow(mCoordinates);
|
||||
// Ensure the horizontal position of the panel does not extend past the parentView edges.
|
||||
final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth();
|
||||
final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates);
|
||||
final int panelY = y + CoordinateUtils.y(mCoordinates);
|
||||
container.setX(panelX);
|
||||
container.setY(panelY);
|
||||
|
||||
mOriginX = x + container.getPaddingLeft();
|
||||
mOriginY = y + container.getPaddingTop();
|
||||
controller.onShowMoreKeysPanel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default x coordinate for showing this panel.
|
||||
*/
|
||||
protected int getDefaultCoordX() {
|
||||
return ((MoreKeysKeyboard) getKeyboard()).getDefaultCoordX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownEvent(final int x, final int y, final int pointerId) {
|
||||
mActivePointerId = pointerId;
|
||||
mCurrentKey = detectKey(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoveEvent(final int x, final int y, final int pointerId) {
|
||||
if (mActivePointerId != pointerId) {
|
||||
return;
|
||||
}
|
||||
final boolean hasOldKey = (mCurrentKey != null);
|
||||
mCurrentKey = detectKey(x, y);
|
||||
if (hasOldKey && mCurrentKey == null) {
|
||||
// A more keys keyboard is canceled when detecting no key.
|
||||
mController.onCancelMoreKeysPanel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpEvent(final int x, final int y, final int pointerId) {
|
||||
if (mActivePointerId != pointerId) {
|
||||
return;
|
||||
}
|
||||
// Calling {@link #detectKey(int,int,int)} here is harmless because the last move event and
|
||||
// the following up event share the same coordinates.
|
||||
mCurrentKey = detectKey(x, y);
|
||||
if (mCurrentKey != null) {
|
||||
updateReleaseKeyGraphics(mCurrentKey);
|
||||
onKeyInput(mCurrentKey);
|
||||
mCurrentKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the specific action for this panel when the user presses a key on the panel.
|
||||
*/
|
||||
protected void onKeyInput(final Key key) {
|
||||
final int code = key.getCode();
|
||||
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
mListener.onTextInput(mCurrentKey.getOutputText());
|
||||
} else if (code != Constants.CODE_UNSPECIFIED) {
|
||||
mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
|
||||
}
|
||||
}
|
||||
|
||||
private Key detectKey(int x, int y) {
|
||||
final Key oldKey = mCurrentKey;
|
||||
final Key newKey = mKeyDetector.detectHitKey(x, y);
|
||||
if (newKey == oldKey) {
|
||||
return newKey;
|
||||
}
|
||||
// A new key is detected.
|
||||
if (oldKey != null) {
|
||||
updateReleaseKeyGraphics(oldKey);
|
||||
invalidateKey(oldKey);
|
||||
}
|
||||
if (newKey != null) {
|
||||
updatePressKeyGraphics(newKey);
|
||||
invalidateKey(newKey);
|
||||
}
|
||||
return newKey;
|
||||
}
|
||||
|
||||
private void updateReleaseKeyGraphics(final Key key) {
|
||||
key.onReleased();
|
||||
invalidateKey(key);
|
||||
}
|
||||
|
||||
private void updatePressKeyGraphics(final Key key) {
|
||||
key.onPressed();
|
||||
invalidateKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismissMoreKeysPanel() {
|
||||
if (!isShowingInParent()) {
|
||||
return;
|
||||
}
|
||||
mController.onDismissMoreKeysPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int translateX(final int x) {
|
||||
return x - mOriginX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int translateY(final int y) {
|
||||
return y - mOriginY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent me) {
|
||||
final int action = me.getActionMasked();
|
||||
final int index = me.getActionIndex();
|
||||
final int x = (int) me.getX(index);
|
||||
final int y = (int) me.getY(index);
|
||||
final int pointerId = me.getPointerId(index);
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
onDownEvent(x, y, pointerId);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
onUpEvent(x, y, pointerId);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
onMoveEvent(x, y, pointerId);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private View getContainerView() {
|
||||
return (View) getParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showInParent(final ViewGroup parentView) {
|
||||
removeFromParent();
|
||||
parentView.addView(getContainerView());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFromParent() {
|
||||
final View containerView = getContainerView();
|
||||
final ViewGroup currentParent = (ViewGroup) containerView.getParent();
|
||||
if (currentParent != null) {
|
||||
currentParent.removeView(containerView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShowingInParent() {
|
||||
return (getContainerView().getParent() != null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
public interface MoreKeysPanel {
|
||||
interface Controller {
|
||||
/**
|
||||
* Add the {@link MoreKeysPanel} to the target view.
|
||||
*
|
||||
* @param panel the panel to be shown.
|
||||
*/
|
||||
void onShowMoreKeysPanel(final MoreKeysPanel panel);
|
||||
|
||||
/**
|
||||
* Remove the current {@link MoreKeysPanel} from the target view.
|
||||
*/
|
||||
void onDismissMoreKeysPanel();
|
||||
|
||||
/**
|
||||
* Instructs the parent to cancel the panel (e.g., when entering a different input mode).
|
||||
*/
|
||||
void onCancelMoreKeysPanel();
|
||||
}
|
||||
|
||||
Controller EMPTY_CONTROLLER = new Controller() {
|
||||
@Override
|
||||
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissMoreKeysPanel() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelMoreKeysPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the layout and event handling of this {@link MoreKeysPanel} and calls the
|
||||
* controller's onShowMoreKeysPanel to add the panel's container view.
|
||||
*
|
||||
* @param parentView the parent view of this {@link MoreKeysPanel}
|
||||
* @param controller the controller that can dismiss this {@link MoreKeysPanel}
|
||||
* @param pointX x coordinate of this {@link MoreKeysPanel}
|
||||
* @param pointY y coordinate of this {@link MoreKeysPanel}
|
||||
* @param listener the listener that will receive keyboard action from this
|
||||
* {@link MoreKeysPanel}.
|
||||
*/
|
||||
// TODO: Currently the MoreKeysPanel is inside a container view that is added to the parent.
|
||||
// Consider the simpler approach of placing the MoreKeysPanel itself into the parent view.
|
||||
void showMoreKeysPanel(View parentView, Controller controller, int pointX,
|
||||
int pointY, KeyboardActionListener listener);
|
||||
|
||||
/**
|
||||
* Dismisses the more keys panel and calls the controller's onDismissMoreKeysPanel to remove
|
||||
* the panel's container view.
|
||||
*/
|
||||
void dismissMoreKeysPanel();
|
||||
|
||||
/**
|
||||
* Process a move event on the more keys panel.
|
||||
*
|
||||
* @param x translated x coordinate of the touch point
|
||||
* @param y translated y coordinate of the touch point
|
||||
* @param pointerId pointer id touch point
|
||||
*/
|
||||
void onMoveEvent(final int x, final int y, final int pointerId);
|
||||
|
||||
/**
|
||||
* Process a down event on the more keys panel.
|
||||
*
|
||||
* @param x translated x coordinate of the touch point
|
||||
* @param y translated y coordinate of the touch point
|
||||
* @param pointerId pointer id touch point
|
||||
*/
|
||||
void onDownEvent(final int x, final int y, final int pointerId);
|
||||
|
||||
/**
|
||||
* Process an up event on the more keys panel.
|
||||
*
|
||||
* @param x translated x coordinate of the touch point
|
||||
* @param y translated y coordinate of the touch point
|
||||
* @param pointerId pointer id touch point
|
||||
*/
|
||||
void onUpEvent(final int x, final int y, final int pointerId);
|
||||
|
||||
/**
|
||||
* Translate X-coordinate of touch event to the local X-coordinate of this
|
||||
* {@link MoreKeysPanel}.
|
||||
*
|
||||
* @param x the global X-coordinate
|
||||
* @return the local X-coordinate to this {@link MoreKeysPanel}
|
||||
*/
|
||||
int translateX(int x);
|
||||
|
||||
/**
|
||||
* Translate Y-coordinate of touch event to the local Y-coordinate of this
|
||||
* {@link MoreKeysPanel}.
|
||||
*
|
||||
* @param y the global Y-coordinate
|
||||
* @return the local Y-coordinate to this {@link MoreKeysPanel}
|
||||
*/
|
||||
int translateY(int y);
|
||||
|
||||
/**
|
||||
* Show this {@link MoreKeysPanel} in the parent view.
|
||||
*
|
||||
* @param parentView the {@link ViewGroup} that hosts this {@link MoreKeysPanel}.
|
||||
*/
|
||||
void showInParent(ViewGroup parentView);
|
||||
|
||||
/**
|
||||
* Remove this {@link MoreKeysPanel} from the parent view.
|
||||
*/
|
||||
void removeFromParent();
|
||||
|
||||
/**
|
||||
* Return whether the panel is currently being shown.
|
||||
*/
|
||||
boolean isShowingInParent();
|
||||
}
|
||||
|
|
@ -0,0 +1,910 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.amnesica.kryptey.inputmethod.keyboard;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.BogusMoveEventDetector;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.DrawingProxy;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.PointerTrackerQueue;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.internal.TimerProxy;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.define.DebugFlags;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class PointerTracker implements PointerTrackerQueue.Element {
|
||||
private static final String TAG = PointerTracker.class.getSimpleName();
|
||||
private static final boolean DEBUG_EVENT = false;
|
||||
private static final boolean DEBUG_MOVE_EVENT = false;
|
||||
private static final boolean DEBUG_LISTENER = false;
|
||||
private static final boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED || DEBUG_EVENT;
|
||||
|
||||
static final class PointerTrackerParams {
|
||||
public final boolean mKeySelectionByDraggingFinger;
|
||||
public final int mTouchNoiseThresholdTime;
|
||||
public final int mTouchNoiseThresholdDistance;
|
||||
public final int mKeyRepeatStartTimeout;
|
||||
public final int mKeyRepeatInterval;
|
||||
public final int mLongPressShiftLockTimeout;
|
||||
|
||||
public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) {
|
||||
mKeySelectionByDraggingFinger = mainKeyboardViewAttr.getBoolean(
|
||||
R.styleable.MainKeyboardView_keySelectionByDraggingFinger, false);
|
||||
mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
|
||||
mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize(
|
||||
R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
|
||||
mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
|
||||
mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_keyRepeatInterval, 0);
|
||||
mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters for pointer handling.
|
||||
private static PointerTrackerParams sParams;
|
||||
private static final int sPointerStep = (int) (10.0 * Resources.getSystem().getDisplayMetrics().density);
|
||||
|
||||
private static final ArrayList<PointerTracker> sTrackers = new ArrayList<>();
|
||||
private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue();
|
||||
|
||||
public final int mPointerId;
|
||||
|
||||
private static DrawingProxy sDrawingProxy;
|
||||
private static TimerProxy sTimerProxy;
|
||||
private static KeyboardActionListener sListener = KeyboardActionListener.EMPTY_LISTENER;
|
||||
|
||||
// The {@link KeyDetector} is set whenever the down event is processed. Also this is updated
|
||||
// when new {@link Keyboard} is set by {@link #setKeyDetector(KeyDetector)}.
|
||||
private KeyDetector mKeyDetector = new KeyDetector();
|
||||
private Keyboard mKeyboard;
|
||||
private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector();
|
||||
|
||||
// The position and time at which first down event occurred.
|
||||
private final int[] mDownCoordinates = CoordinateUtils.newInstance();
|
||||
|
||||
// The current key where this pointer is.
|
||||
private Key mCurrentKey = null;
|
||||
// The position where the current key was recognized for the first time.
|
||||
private int mKeyX;
|
||||
private int mKeyY;
|
||||
|
||||
// Last pointer position.
|
||||
private int mLastX;
|
||||
private int mLastY;
|
||||
private int mStartX;
|
||||
//private int mStartY;
|
||||
private long mStartTime;
|
||||
private boolean mCursorMoved = false;
|
||||
|
||||
// true if keyboard layout has been changed.
|
||||
private boolean mKeyboardLayoutHasBeenChanged;
|
||||
|
||||
// true if this pointer is no longer triggering any action because it has been canceled.
|
||||
private boolean mIsTrackingForActionDisabled;
|
||||
|
||||
// the more keys panel currently being shown. equals null if no panel is active.
|
||||
private MoreKeysPanel mMoreKeysPanel;
|
||||
|
||||
private static final int MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT = 3;
|
||||
// true if this pointer is in the dragging finger mode.
|
||||
boolean mIsInDraggingFinger;
|
||||
// true if this pointer is sliding from a modifier key and in the sliding key input mode,
|
||||
// so that further modifier keys should be ignored.
|
||||
boolean mIsInSlidingKeyInput;
|
||||
// if not a NOT_A_CODE, the key of this code is repeating
|
||||
private int mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
|
||||
|
||||
// true if dragging finger is allowed.
|
||||
private boolean mIsAllowedDraggingFinger;
|
||||
|
||||
// TODO: Add PointerTrackerFactory singleton and move some class static methods into it.
|
||||
public static void init(final TypedArray mainKeyboardViewAttr, final TimerProxy timerProxy,
|
||||
final DrawingProxy drawingProxy) {
|
||||
sParams = new PointerTrackerParams(mainKeyboardViewAttr);
|
||||
|
||||
final Resources res = mainKeyboardViewAttr.getResources();
|
||||
BogusMoveEventDetector.init(res);
|
||||
|
||||
sTimerProxy = timerProxy;
|
||||
sDrawingProxy = drawingProxy;
|
||||
}
|
||||
|
||||
public static PointerTracker getPointerTracker(final int id) {
|
||||
final ArrayList<PointerTracker> trackers = sTrackers;
|
||||
|
||||
// Create pointer trackers until we can get 'id+1'-th tracker, if needed.
|
||||
for (int i = trackers.size(); i <= id; i++) {
|
||||
final PointerTracker tracker = new PointerTracker(i);
|
||||
trackers.add(tracker);
|
||||
}
|
||||
|
||||
return trackers.get(id);
|
||||
}
|
||||
|
||||
public static boolean isAnyInDraggingFinger() {
|
||||
return sPointerTrackerQueue.isAnyInDraggingFinger();
|
||||
}
|
||||
|
||||
public static void cancelAllPointerTrackers() {
|
||||
sPointerTrackerQueue.cancelAllPointerTrackers();
|
||||
}
|
||||
|
||||
public static void setKeyboardActionListener(final KeyboardActionListener listener) {
|
||||
sListener = listener;
|
||||
}
|
||||
|
||||
public static void setKeyDetector(final KeyDetector keyDetector) {
|
||||
final Keyboard keyboard = keyDetector.getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
final int trackersSize = sTrackers.size();
|
||||
for (int i = 0; i < trackersSize; ++i) {
|
||||
final PointerTracker tracker = sTrackers.get(i);
|
||||
tracker.setKeyDetectorInner(keyDetector);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setReleasedKeyGraphicsToAllKeys() {
|
||||
final int trackersSize = sTrackers.size();
|
||||
for (int i = 0; i < trackersSize; ++i) {
|
||||
final PointerTracker tracker = sTrackers.get(i);
|
||||
tracker.setReleasedKeyGraphics(tracker.getKey(), true /* withAnimation */);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dismissAllMoreKeysPanels() {
|
||||
final int trackersSize = sTrackers.size();
|
||||
for (int i = 0; i < trackersSize; ++i) {
|
||||
final PointerTracker tracker = sTrackers.get(i);
|
||||
tracker.dismissMoreKeysPanel();
|
||||
}
|
||||
}
|
||||
|
||||
private PointerTracker(final int id) {
|
||||
mPointerId = id;
|
||||
}
|
||||
|
||||
// Returns true if keyboard has been changed by this callback.
|
||||
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
|
||||
final int repeatCount) {
|
||||
// While gesture input is going on, this method should be a no-operation. But when gesture
|
||||
// input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
|
||||
// are set to false. To keep this method is a no-operation,
|
||||
// <code>mIsTrackingForActionDisabled</code> should also be taken account of.
|
||||
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
|
||||
if (DEBUG_LISTENER) {
|
||||
Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId,
|
||||
(key == null ? "none" : Constants.printableCode(key.getCode())),
|
||||
ignoreModifierKey ? " ignoreModifier" : "",
|
||||
repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
|
||||
}
|
||||
if (ignoreModifierKey) {
|
||||
return false;
|
||||
}
|
||||
sListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1);
|
||||
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
|
||||
mKeyboardLayoutHasBeenChanged = false;
|
||||
sTimerProxy.startTypingStateTimer(key);
|
||||
return keyboardLayoutHasBeenChanged;
|
||||
}
|
||||
|
||||
// Note that we need primaryCode argument because the keyboard may in shifted state and the
|
||||
// primaryCode is different from {@link Key#mKeyCode}.
|
||||
private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
|
||||
final int y, final boolean isKeyRepeat) {
|
||||
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
|
||||
final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState();
|
||||
final int code = altersCode ? key.getAltCode() : primaryCode;
|
||||
if (DEBUG_LISTENER) {
|
||||
final String output = code == Constants.CODE_OUTPUT_TEXT
|
||||
? key.getOutputText() : Constants.printableCode(code);
|
||||
Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
|
||||
output, ignoreModifierKey ? " ignoreModifier" : "",
|
||||
altersCode ? " altersCode" : ""));
|
||||
}
|
||||
if (ignoreModifierKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
sListener.onTextInput(key.getOutputText());
|
||||
} else if (code != Constants.CODE_UNSPECIFIED) {
|
||||
sListener.onCodeInput(code,
|
||||
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we need primaryCode argument because the keyboard may be in shifted state and the
|
||||
// primaryCode is different from {@link Key#mKeyCode}.
|
||||
private void callListenerOnRelease(final Key key, final int primaryCode,
|
||||
final boolean withSliding) {
|
||||
// See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}.
|
||||
final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier();
|
||||
if (DEBUG_LISTENER) {
|
||||
Log.d(TAG, String.format("[%d] onRelease : %s%s%s", mPointerId,
|
||||
Constants.printableCode(primaryCode),
|
||||
withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : ""));
|
||||
}
|
||||
if (ignoreModifierKey) {
|
||||
return;
|
||||
}
|
||||
sListener.onReleaseKey(primaryCode, withSliding);
|
||||
}
|
||||
|
||||
private void callListenerOnFinishSlidingInput() {
|
||||
if (DEBUG_LISTENER) {
|
||||
Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId));
|
||||
}
|
||||
sListener.onFinishSlidingInput();
|
||||
}
|
||||
|
||||
private void setKeyDetectorInner(final KeyDetector keyDetector) {
|
||||
final Keyboard keyboard = keyDetector.getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
|
||||
return;
|
||||
}
|
||||
mKeyDetector = keyDetector;
|
||||
mKeyboard = keyboard;
|
||||
// Mark that keyboard layout has been changed.
|
||||
mKeyboardLayoutHasBeenChanged = true;
|
||||
final int keyPaddedWidth = mKeyboard.mMostCommonKeyWidth
|
||||
+ Math.round(mKeyboard.mHorizontalGap);
|
||||
final int keyPaddedHeight = mKeyboard.mMostCommonKeyHeight
|
||||
+ Math.round(mKeyboard.mVerticalGap);
|
||||
// Keep {@link #mCurrentKey} that comes from previous keyboard. The key preview of
|
||||
// {@link #mCurrentKey} will be dismissed by {@setReleasedKeyGraphics(Key)} via
|
||||
// {@link onMoveEventInternal(int,int,long)} or {@link #onUpEventInternal(int,int,long)}.
|
||||
mBogusMoveEventDetector.setKeyboardGeometry(keyPaddedWidth, keyPaddedHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInDraggingFinger() {
|
||||
return mIsInDraggingFinger;
|
||||
}
|
||||
|
||||
public Key getKey() {
|
||||
return mCurrentKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModifier() {
|
||||
return mCurrentKey != null && mCurrentKey.isModifier();
|
||||
}
|
||||
|
||||
public Key getKeyOn(final int x, final int y) {
|
||||
return mKeyDetector.detectHitKey(x, y);
|
||||
}
|
||||
|
||||
private void setReleasedKeyGraphics(final Key key, final boolean withAnimation) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sDrawingProxy.onKeyReleased(key, withAnimation);
|
||||
|
||||
if (key.isShift()) {
|
||||
for (final Key shiftKey : mKeyboard.mShiftKeys) {
|
||||
if (shiftKey != key) {
|
||||
sDrawingProxy.onKeyReleased(shiftKey, false /* withAnimation */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key.altCodeWhileTyping()) {
|
||||
final int altCode = key.getAltCode();
|
||||
final Key altKey = mKeyboard.getKey(altCode);
|
||||
if (altKey != null) {
|
||||
sDrawingProxy.onKeyReleased(altKey, false /* withAnimation */);
|
||||
}
|
||||
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
|
||||
if (k != key && k.getAltCode() == altCode) {
|
||||
sDrawingProxy.onKeyReleased(k, false /* withAnimation */);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPressedKeyGraphics(final Key key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
|
||||
final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState();
|
||||
|
||||
sDrawingProxy.onKeyPressed(key, true);
|
||||
|
||||
if (key.isShift()) {
|
||||
for (final Key shiftKey : mKeyboard.mShiftKeys) {
|
||||
if (shiftKey != key) {
|
||||
sDrawingProxy.onKeyPressed(shiftKey, false /* withPreview */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (altersCode) {
|
||||
final int altCode = key.getAltCode();
|
||||
final Key altKey = mKeyboard.getKey(altCode);
|
||||
if (altKey != null) {
|
||||
sDrawingProxy.onKeyPressed(altKey, false /* withPreview */);
|
||||
}
|
||||
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
|
||||
if (k != key && k.getAltCode() == altCode) {
|
||||
sDrawingProxy.onKeyPressed(k, false /* withPreview */);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void getLastCoordinates(final int[] outCoords) {
|
||||
CoordinateUtils.set(outCoords, mLastX, mLastY);
|
||||
}
|
||||
|
||||
private Key onDownKey(final int x, final int y) {
|
||||
CoordinateUtils.set(mDownCoordinates, x, y);
|
||||
mBogusMoveEventDetector.onDownKey();
|
||||
return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
|
||||
}
|
||||
|
||||
private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
|
||||
return (int) Math.hypot(x1 - x2, y1 - y2);
|
||||
}
|
||||
|
||||
private Key onMoveKeyInternal(final int x, final int y) {
|
||||
mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
|
||||
mLastX = x;
|
||||
mLastY = y;
|
||||
return mKeyDetector.detectHitKey(x, y);
|
||||
}
|
||||
|
||||
private Key onMoveKey(final int x, final int y) {
|
||||
return onMoveKeyInternal(x, y);
|
||||
}
|
||||
|
||||
private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
|
||||
mCurrentKey = newKey;
|
||||
mKeyX = x;
|
||||
mKeyY = y;
|
||||
return newKey;
|
||||
}
|
||||
|
||||
/* package */
|
||||
static int getActivePointerTrackerCount() {
|
||||
return sPointerTrackerQueue.size();
|
||||
}
|
||||
|
||||
public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) {
|
||||
final int action = me.getActionMasked();
|
||||
final long eventTime = me.getEventTime();
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
// When this pointer is the only active pointer and is showing a more keys panel,
|
||||
// we should ignore other pointers' motion event.
|
||||
final boolean shouldIgnoreOtherPointers =
|
||||
isShowingMoreKeysPanel() && getActivePointerTrackerCount() == 1;
|
||||
final int pointerCount = me.getPointerCount();
|
||||
for (int index = 0; index < pointerCount; index++) {
|
||||
final int id = me.getPointerId(index);
|
||||
if (shouldIgnoreOtherPointers && id != mPointerId) {
|
||||
continue;
|
||||
}
|
||||
final int x = (int) me.getX(index);
|
||||
final int y = (int) me.getY(index);
|
||||
final PointerTracker tracker = getPointerTracker(id);
|
||||
tracker.onMoveEvent(x, y, eventTime);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int index = me.getActionIndex();
|
||||
final int x = (int) me.getX(index);
|
||||
final int y = (int) me.getY(index);
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
onDownEvent(x, y, eventTime, keyDetector);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
onUpEvent(x, y, eventTime);
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
onCancelEvent(x, y, eventTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownEvent(final int x, final int y, final long eventTime,
|
||||
final KeyDetector keyDetector) {
|
||||
setKeyDetectorInner(keyDetector);
|
||||
if (DEBUG_EVENT) {
|
||||
printTouchEvent("onDownEvent:", x, y, eventTime);
|
||||
}
|
||||
// Naive up-to-down noise filter.
|
||||
final long deltaT = eventTime;
|
||||
if (deltaT < sParams.mTouchNoiseThresholdTime) {
|
||||
final int distance = getDistance(x, y, mLastX, mLastY);
|
||||
if (distance < sParams.mTouchNoiseThresholdDistance) {
|
||||
if (DEBUG_MODE)
|
||||
Log.w(TAG, String.format("[%d] onDownEvent:"
|
||||
+ " ignore potential noise: time=%d distance=%d",
|
||||
mPointerId, deltaT, distance));
|
||||
cancelTrackingForAction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final Key key = getKeyOn(x, y);
|
||||
mBogusMoveEventDetector.onActualDownEvent(x, y);
|
||||
if (key != null && key.isModifier()) {
|
||||
// Before processing a down event of modifier key, all pointers already being
|
||||
// tracked should be released.
|
||||
sPointerTrackerQueue.releaseAllPointers(eventTime);
|
||||
}
|
||||
sPointerTrackerQueue.add(this);
|
||||
onDownEventInternal(x, y);
|
||||
}
|
||||
|
||||
/* package */ boolean isShowingMoreKeysPanel() {
|
||||
return (mMoreKeysPanel != null);
|
||||
}
|
||||
|
||||
private void dismissMoreKeysPanel() {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
mMoreKeysPanel.dismissMoreKeysPanel();
|
||||
mMoreKeysPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDownEventInternal(final int x, final int y) {
|
||||
Key key = onDownKey(x, y);
|
||||
// Key selection by dragging finger is allowed when 1) key selection by dragging finger is
|
||||
// enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this
|
||||
// pointer's KeyDetector always allows key selection by dragging finger, such as
|
||||
// {@link MoreKeysKeyboard}.
|
||||
mIsAllowedDraggingFinger = sParams.mKeySelectionByDraggingFinger
|
||||
|| (key != null && key.isModifier())
|
||||
|| mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger();
|
||||
mKeyboardLayoutHasBeenChanged = false;
|
||||
mIsTrackingForActionDisabled = false;
|
||||
resetKeySelectionByDraggingFinger();
|
||||
if (key != null) {
|
||||
// This onPress call may have changed keyboard layout. Those cases are detected at
|
||||
// {@link #setKeyboard}. In those cases, we should update key according to the new
|
||||
// keyboard layout.
|
||||
if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
|
||||
key = onDownKey(x, y);
|
||||
}
|
||||
|
||||
startRepeatKey(key);
|
||||
startLongPressTimer(key);
|
||||
setPressedKeyGraphics(key);
|
||||
mStartX = x;
|
||||
//mStartY = y;
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private void startKeySelectionByDraggingFinger(final Key key) {
|
||||
if (!mIsInDraggingFinger) {
|
||||
mIsInSlidingKeyInput = key.isModifier();
|
||||
}
|
||||
mIsInDraggingFinger = true;
|
||||
}
|
||||
|
||||
private void resetKeySelectionByDraggingFinger() {
|
||||
mIsInDraggingFinger = false;
|
||||
mIsInSlidingKeyInput = false;
|
||||
}
|
||||
|
||||
private void onMoveEvent(final int x, final int y, final long eventTime) {
|
||||
if (DEBUG_MOVE_EVENT) {
|
||||
printTouchEvent("onMoveEvent:", x, y, eventTime);
|
||||
}
|
||||
if (mIsTrackingForActionDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
final int translatedX = mMoreKeysPanel.translateX(x);
|
||||
final int translatedY = mMoreKeysPanel.translateY(y);
|
||||
mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId);
|
||||
onMoveKey(x, y);
|
||||
return;
|
||||
}
|
||||
onMoveEventInternal(x, y, eventTime);
|
||||
}
|
||||
|
||||
private void processDraggingFingerInToNewKey(final Key newKey, final int x, final int y) {
|
||||
// This onPress call may have changed keyboard layout. Those cases are detected
|
||||
// at {@link #setKeyboard}. In those cases, we should update key according
|
||||
// to the new keyboard layout.
|
||||
Key key = newKey;
|
||||
if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
|
||||
| ||||