mirror of
https://github.com/amnesica/KryptEY.git
synced 2024-10-01 01:05:58 -04:00
Initial commit
This commit is contained in:
commit
07abe383cd
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>
|
||||
|
||||
![GitHub version](https://img.shields.io/badge/version-v0.1.5-brightgreen)
|
||||
![Chatkontrolle stoppen](https://img.shields.io/badge/chatkontrolle-stoppen-blueviolet)
|
||||
![Stop scanning me](https://img.shields.io/badge/stop-scanning%20me-blueviolet)
|
||||
|
||||
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 */)) {
|
||||
key = onMoveKey(x, y);
|
||||
}
|
||||
onMoveToNewKey(key, x, y);
|
||||
if (mIsTrackingForActionDisabled) {
|
||||
return;
|
||||
}
|
||||
startLongPressTimer(key);
|
||||
setPressedKeyGraphics(key);
|
||||
}
|
||||
|
||||
private void processDraggingFingerOutFromOldKey(final Key oldKey) {
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
|
||||
startKeySelectionByDraggingFinger(oldKey);
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
}
|
||||
|
||||
private void dragFingerFromOldKeyToNewKey(final Key key, final int x, final int y,
|
||||
final long eventTime, final Key oldKey) {
|
||||
// The pointer has been slid in to the new key from the previous key, we must call
|
||||
// onRelease() first to notify that the previous key has been released, then call
|
||||
// onPress() to notify that the new key is being pressed.
|
||||
processDraggingFingerOutFromOldKey(oldKey);
|
||||
startRepeatKey(key);
|
||||
if (mIsAllowedDraggingFinger) {
|
||||
processDraggingFingerInToNewKey(key, x, y);
|
||||
}
|
||||
// HACK: If there are currently multiple touches, register the key even if the finger
|
||||
// slides off the key. This defends against noise from some touch panels when there are
|
||||
// close multiple touches.
|
||||
// Caveat: When in chording input mode with a modifier key, we don't use this hack.
|
||||
else if (getActivePointerTrackerCount() > 1
|
||||
&& !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
|
||||
if (DEBUG_MODE) {
|
||||
Log.w(TAG, String.format("[%d] onMoveEvent:"
|
||||
+ " detected sliding finger while multi touching", mPointerId));
|
||||
}
|
||||
onUpEvent(x, y, eventTime);
|
||||
cancelTrackingForAction();
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
} else {
|
||||
cancelTrackingForAction();
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
}
|
||||
}
|
||||
|
||||
private void dragFingerOutFromOldKey(final Key oldKey, final int x, final int y) {
|
||||
// The pointer has been slid out from the previous key, we must call onRelease() to
|
||||
// notify that the previous key has been released.
|
||||
processDraggingFingerOutFromOldKey(oldKey);
|
||||
if (mIsAllowedDraggingFinger) {
|
||||
onMoveToNewKey(null, x, y);
|
||||
} else {
|
||||
cancelTrackingForAction();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMoveEventInternal(final int x, final int y, final long eventTime) {
|
||||
final Key oldKey = mCurrentKey;
|
||||
|
||||
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE && Settings.getInstance().getCurrent().mSpaceSwipeEnabled) {
|
||||
//Pointer slider
|
||||
int steps = (x - mStartX) / sPointerStep;
|
||||
final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
|
||||
if (steps != 0 && mStartTime + longpressTimeout < System.currentTimeMillis()) {
|
||||
mCursorMoved = true;
|
||||
mStartX += steps * sPointerStep;
|
||||
sListener.onMovePointer(steps);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE && Settings.getInstance().getCurrent().mDeleteSwipeEnabled) {
|
||||
//Delete slider
|
||||
int steps = (x - mStartX) / sPointerStep;
|
||||
if (steps != 0) {
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
mCursorMoved = true;
|
||||
mStartX += steps * sPointerStep;
|
||||
sListener.onMoveDeletePointer(steps);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final Key newKey = onMoveKey(x, y);
|
||||
if (newKey != null) {
|
||||
if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, newKey)) {
|
||||
dragFingerFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey);
|
||||
} else if (oldKey == null) {
|
||||
// The pointer has been slid in to the new key, but the finger was not on any keys.
|
||||
// In this case, we must call onPress() to notify that the new key is being pressed.
|
||||
processDraggingFingerInToNewKey(newKey, x, y);
|
||||
}
|
||||
} else { // newKey == null
|
||||
if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, newKey)) {
|
||||
dragFingerOutFromOldKey(oldKey, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onUpEvent(final int x, final int y, final long eventTime) {
|
||||
if (DEBUG_EVENT) {
|
||||
printTouchEvent("onUpEvent :", x, y, eventTime);
|
||||
}
|
||||
|
||||
sTimerProxy.cancelUpdateBatchInputTimer(this);
|
||||
if (mCurrentKey != null && mCurrentKey.isModifier()) {
|
||||
// Before processing an up event of modifier key, all pointers already being
|
||||
// tracked should be released.
|
||||
sPointerTrackerQueue.releaseAllPointersExcept(this, eventTime);
|
||||
} else {
|
||||
sPointerTrackerQueue.releaseAllPointersOlderThan(this, eventTime);
|
||||
}
|
||||
onUpEventInternal(x, y);
|
||||
sPointerTrackerQueue.remove(this);
|
||||
}
|
||||
|
||||
// Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
|
||||
// This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
|
||||
// "virtual" up event.
|
||||
@Override
|
||||
public void onPhantomUpEvent(final long eventTime) {
|
||||
if (DEBUG_EVENT) {
|
||||
printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime);
|
||||
}
|
||||
onUpEventInternal(mLastX, mLastY);
|
||||
cancelTrackingForAction();
|
||||
}
|
||||
|
||||
private void onUpEventInternal(final int x, final int y) {
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
final boolean isInDraggingFinger = mIsInDraggingFinger;
|
||||
final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
|
||||
resetKeySelectionByDraggingFinger();
|
||||
final Key currentKey = mCurrentKey;
|
||||
mCurrentKey = null;
|
||||
final int currentRepeatingKeyCode = mCurrentRepeatingKeyCode;
|
||||
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
|
||||
// Release the last pressed key.
|
||||
setReleasedKeyGraphics(currentKey, true /* withAnimation */);
|
||||
|
||||
if (mCursorMoved && currentKey.getCode() == Constants.CODE_DELETE) {
|
||||
sListener.onUpWithDeletePointerActive();
|
||||
}
|
||||
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
if (!mIsTrackingForActionDisabled) {
|
||||
final int translatedX = mMoreKeysPanel.translateX(x);
|
||||
final int translatedY = mMoreKeysPanel.translateY(y);
|
||||
mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId);
|
||||
}
|
||||
dismissMoreKeysPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCursorMoved) {
|
||||
mCursorMoved = false;
|
||||
return;
|
||||
}
|
||||
if (mIsTrackingForActionDisabled) {
|
||||
return;
|
||||
}
|
||||
if (currentKey != null && currentKey.isRepeatable()
|
||||
&& (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) {
|
||||
return;
|
||||
}
|
||||
detectAndSendKey(currentKey, mKeyX, mKeyY);
|
||||
if (isInSlidingKeyInput) {
|
||||
callListenerOnFinishSlidingInput();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTrackingForAction() {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
return;
|
||||
}
|
||||
mIsTrackingForActionDisabled = true;
|
||||
}
|
||||
|
||||
public void onLongPressed() {
|
||||
sTimerProxy.cancelLongPressTimersOf(this);
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
return;
|
||||
}
|
||||
if (mCursorMoved) {
|
||||
return;
|
||||
}
|
||||
final Key key = getKey();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (key.hasNoPanelAutoMoreKey()) {
|
||||
cancelKeyTracking();
|
||||
final int moreKeyCode = key.getMoreKeys()[0].mCode;
|
||||
sListener.onPressKey(moreKeyCode, 0 /* repeatCont */, true /* isSinglePointer */);
|
||||
sListener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE,
|
||||
Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
|
||||
sListener.onReleaseKey(moreKeyCode, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
final int code = key.getCode();
|
||||
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
|
||||
// Long pressing the space key invokes IME switcher dialog.
|
||||
if (sListener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
|
||||
cancelKeyTracking();
|
||||
sListener.onReleaseKey(code, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setReleasedKeyGraphics(key, false /* withAnimation */);
|
||||
final MoreKeysPanel moreKeysPanel = sDrawingProxy.showMoreKeysKeyboard(key, this);
|
||||
if (moreKeysPanel == null) {
|
||||
return;
|
||||
}
|
||||
final int translatedX = moreKeysPanel.translateX(mLastX);
|
||||
final int translatedY = moreKeysPanel.translateY(mLastY);
|
||||
moreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId);
|
||||
mMoreKeysPanel = moreKeysPanel;
|
||||
}
|
||||
|
||||
private void cancelKeyTracking() {
|
||||
resetKeySelectionByDraggingFinger();
|
||||
cancelTrackingForAction();
|
||||
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
|
||||
sPointerTrackerQueue.remove(this);
|
||||
}
|
||||
|
||||
private void onCancelEvent(final int x, final int y, final long eventTime) {
|
||||
if (DEBUG_EVENT) {
|
||||
printTouchEvent("onCancelEvt:", x, y, eventTime);
|
||||
}
|
||||
|
||||
cancelAllPointerTrackers();
|
||||
sPointerTrackerQueue.releaseAllPointers(eventTime);
|
||||
onCancelEventInternal();
|
||||
}
|
||||
|
||||
private void onCancelEventInternal() {
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
|
||||
resetKeySelectionByDraggingFinger();
|
||||
dismissMoreKeysPanel();
|
||||
}
|
||||
|
||||
private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) {
|
||||
final Key curKey = mCurrentKey;
|
||||
if (newKey == curKey) {
|
||||
return false;
|
||||
}
|
||||
if (curKey == null /* && newKey != null */) {
|
||||
return true;
|
||||
}
|
||||
// Here curKey points to the different key from newKey.
|
||||
final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
|
||||
mIsInSlidingKeyInput);
|
||||
final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToHitboxEdge(x, y);
|
||||
if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
|
||||
if (DEBUG_MODE) {
|
||||
final float distanceToEdgeRatio = (float) Math.sqrt(distanceFromKeyEdgeSquared)
|
||||
/ (mKeyboard.mMostCommonKeyWidth + mKeyboard.mHorizontalGap);
|
||||
Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
|
||||
+ " %.2f key width from key edge", mPointerId, distanceToEdgeRatio));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!mIsAllowedDraggingFinger && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
|
||||
if (DEBUG_MODE) {
|
||||
final float keyDiagonal = (float) Math.hypot(
|
||||
mKeyboard.mMostCommonKeyWidth + mKeyboard.mHorizontalGap,
|
||||
mKeyboard.mMostCommonKeyHeight + mKeyboard.mVerticalGap);
|
||||
final float lengthFromDownRatio =
|
||||
mBogusMoveEventDetector.getAccumulatedDistanceFromDownKey() / keyDiagonal;
|
||||
Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
|
||||
+ " %.2f key diagonal from virtual down point",
|
||||
mPointerId, lengthFromDownRatio));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startLongPressTimer(final Key key) {
|
||||
// Note that we need to cancel all active long press shift key timers if any whenever we
|
||||
// start a new long press timer for both non-shift and shift keys.
|
||||
sTimerProxy.cancelLongPressShiftKeyTimer();
|
||||
if (key == null) return;
|
||||
if (!key.isLongPressEnabled()) return;
|
||||
// Caveat: Please note that isLongPressEnabled() can be true even if the current key
|
||||
// doesn't have its more keys. (e.g. spacebar, globe key) If we are in the dragging finger
|
||||
// mode, we will disable long press timer of such key.
|
||||
// We always need to start the long press timer if the key has its more keys regardless of
|
||||
// whether or not we are in the dragging finger mode.
|
||||
if (mIsInDraggingFinger && key.getMoreKeys() == null) return;
|
||||
|
||||
final int delay = getLongPressTimeout(key.getCode());
|
||||
if (delay <= 0) return;
|
||||
sTimerProxy.startLongPressTimerOf(this, delay);
|
||||
}
|
||||
|
||||
private int getLongPressTimeout(final int code) {
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
return sParams.mLongPressShiftLockTimeout;
|
||||
}
|
||||
final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout;
|
||||
if (mIsInSlidingKeyInput) {
|
||||
// We use longer timeout for sliding finger input started from the modifier key.
|
||||
return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
|
||||
}
|
||||
if (code == Constants.CODE_SPACE) {
|
||||
// Cursor can be moved in space
|
||||
return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
|
||||
}
|
||||
return longpressTimeout;
|
||||
}
|
||||
|
||||
private void detectAndSendKey(final Key key, final int x, final int y) {
|
||||
if (key == null) return;
|
||||
|
||||
final int code = key.getCode();
|
||||
callListenerOnCodeInput(key, code, x, y, false /* isKeyRepeat */);
|
||||
callListenerOnRelease(key, code, false /* withSliding */);
|
||||
}
|
||||
|
||||
private void startRepeatKey(final Key key) {
|
||||
if (key == null) return;
|
||||
if (!key.isRepeatable()) return;
|
||||
// Don't start key repeat when we are in the dragging finger mode.
|
||||
if (mIsInDraggingFinger) return;
|
||||
final int startRepeatCount = 1;
|
||||
startKeyRepeatTimer(startRepeatCount);
|
||||
}
|
||||
|
||||
public void onKeyRepeat(final int code, final int repeatCount) {
|
||||
final Key key = getKey();
|
||||
if (key == null || key.getCode() != code) {
|
||||
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
|
||||
return;
|
||||
}
|
||||
mCurrentRepeatingKeyCode = code;
|
||||
final int nextRepeatCount = repeatCount + 1;
|
||||
startKeyRepeatTimer(nextRepeatCount);
|
||||
callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
|
||||
callListenerOnCodeInput(key, code, mKeyX, mKeyY, true /* isKeyRepeat */);
|
||||
}
|
||||
|
||||
private void startKeyRepeatTimer(final int repeatCount) {
|
||||
final int delay =
|
||||
(repeatCount == 1) ? sParams.mKeyRepeatStartTimeout : sParams.mKeyRepeatInterval;
|
||||
sTimerProxy.startKeyRepeatTimerOf(this, repeatCount, delay);
|
||||
}
|
||||
|
||||
private void printTouchEvent(final String title, final int x, final int y,
|
||||
final long eventTime) {
|
||||
final Key key = mKeyDetector.detectHitKey(x, y);
|
||||
final String code = (key == null ? "none" : Constants.printableCode(key.getCode()));
|
||||
Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
|
||||
(mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code));
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ProximityInfo {
|
||||
private static final List<Key> EMPTY_KEY_LIST = Collections.emptyList();
|
||||
|
||||
private final int mGridWidth;
|
||||
private final int mGridHeight;
|
||||
private final int mGridSize;
|
||||
private final int mCellWidth;
|
||||
private final int mCellHeight;
|
||||
// TODO: Find a proper name for mKeyboardMinWidth
|
||||
private final int mKeyboardMinWidth;
|
||||
private final int mKeyboardHeight;
|
||||
private final List<Key> mSortedKeys;
|
||||
private final List<Key>[] mGridNeighbors;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ProximityInfo(final int gridWidth, final int gridHeight, final int minWidth, final int height,
|
||||
final List<Key> sortedKeys) {
|
||||
mGridWidth = gridWidth;
|
||||
mGridHeight = gridHeight;
|
||||
mGridSize = mGridWidth * mGridHeight;
|
||||
mCellWidth = (minWidth + mGridWidth - 1) / mGridWidth;
|
||||
mCellHeight = (height + mGridHeight - 1) / mGridHeight;
|
||||
mKeyboardMinWidth = minWidth;
|
||||
mKeyboardHeight = height;
|
||||
mSortedKeys = sortedKeys;
|
||||
mGridNeighbors = new List[mGridSize];
|
||||
if (minWidth == 0 || height == 0) {
|
||||
// No proximity required. Keyboard might be more keys keyboard.
|
||||
return;
|
||||
}
|
||||
computeNearestNeighbors();
|
||||
}
|
||||
|
||||
private void computeNearestNeighbors() {
|
||||
final int keyCount = mSortedKeys.size();
|
||||
final int gridSize = mGridNeighbors.length;
|
||||
final int maxKeyRight = mGridWidth * mCellWidth;
|
||||
final int maxKeyBottom = mGridHeight * mCellHeight;
|
||||
|
||||
// For large layouts, 'neighborsFlatBuffer' is about 80k of memory: gridSize is usually 512,
|
||||
// keycount is about 40 and a pointer to a Key is 4 bytes. This contains, for each cell,
|
||||
// enough space for as many keys as there are on the keyboard. Hence, every
|
||||
// keycount'th element is the start of a new cell, and each of these virtual subarrays
|
||||
// start empty with keycount spaces available. This fills up gradually in the loop below.
|
||||
// Since in the practice each cell does not have a lot of neighbors, most of this space is
|
||||
// actually just empty padding in this fixed-size buffer.
|
||||
final Key[] neighborsFlatBuffer = new Key[gridSize * keyCount];
|
||||
final int[] neighborCountPerCell = new int[gridSize];
|
||||
for (final Key key : mSortedKeys) {
|
||||
if (key.isSpacer()) continue;
|
||||
|
||||
// Iterate through all of the cells that overlap with the clickable region of the
|
||||
// current key and add the key as a neighbor.
|
||||
final int keyX = key.getX();
|
||||
final int keyY = key.getY();
|
||||
final int keyTop = keyY - key.getTopPadding();
|
||||
final int keyBottom = Math.min(keyY + key.getHeight() + key.getBottomPadding(),
|
||||
maxKeyBottom);
|
||||
final int keyLeft = keyX - key.getLeftPadding();
|
||||
final int keyRight = Math.min(keyX + key.getWidth() + key.getRightPadding(),
|
||||
maxKeyRight);
|
||||
final int yDeltaToGrid = keyTop % mCellHeight;
|
||||
final int xDeltaToGrid = keyLeft % mCellWidth;
|
||||
final int yStart = keyTop - yDeltaToGrid;
|
||||
final int xStart = keyLeft - xDeltaToGrid;
|
||||
int baseIndexOfCurrentRow = (yStart / mCellHeight) * mGridWidth + (xStart / mCellWidth);
|
||||
for (int cellTop = yStart; cellTop < keyBottom; cellTop += mCellHeight) {
|
||||
int index = baseIndexOfCurrentRow;
|
||||
for (int cellLeft = xStart; cellLeft < keyRight; cellLeft += mCellWidth) {
|
||||
neighborsFlatBuffer[index * keyCount + neighborCountPerCell[index]] = key;
|
||||
++neighborCountPerCell[index];
|
||||
++index;
|
||||
}
|
||||
baseIndexOfCurrentRow += mGridWidth;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < gridSize; ++i) {
|
||||
final int indexStart = i * keyCount;
|
||||
final int indexEnd = indexStart + neighborCountPerCell[i];
|
||||
final ArrayList<Key> neighbors = new ArrayList<>(indexEnd - indexStart);
|
||||
for (int index = indexStart; index < indexEnd; index++) {
|
||||
neighbors.add(neighborsFlatBuffer[index]);
|
||||
}
|
||||
mGridNeighbors[i] = Collections.unmodifiableList(neighbors);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Key> getNearestKeys(final int x, final int y) {
|
||||
if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) {
|
||||
int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth);
|
||||
if (index < mGridSize) {
|
||||
return mGridNeighbors[index];
|
||||
}
|
||||
}
|
||||
return EMPTY_KEY_LIST;
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public final class AlphabetShiftState {
|
||||
private static final String TAG = AlphabetShiftState.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final int UNSHIFTED = 0;
|
||||
private static final int MANUAL_SHIFTED = 1;
|
||||
private static final int AUTOMATIC_SHIFTED = 2;
|
||||
private static final int SHIFT_LOCKED = 3;
|
||||
private static final int SHIFT_LOCK_SHIFTED = 4;
|
||||
|
||||
private int mState = UNSHIFTED;
|
||||
|
||||
public void setShifted(boolean newShiftState) {
|
||||
final int oldState = mState;
|
||||
if (newShiftState) {
|
||||
switch (oldState) {
|
||||
case UNSHIFTED:
|
||||
mState = MANUAL_SHIFTED;
|
||||
break;
|
||||
case SHIFT_LOCKED:
|
||||
mState = SHIFT_LOCK_SHIFTED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (oldState) {
|
||||
case MANUAL_SHIFTED:
|
||||
case AUTOMATIC_SHIFTED:
|
||||
mState = UNSHIFTED;
|
||||
break;
|
||||
case SHIFT_LOCK_SHIFTED:
|
||||
mState = SHIFT_LOCKED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "setShifted(" + newShiftState + "): " + toString(oldState) + " > " + this);
|
||||
}
|
||||
|
||||
public void setShiftLocked(boolean newShiftLockState) {
|
||||
final int oldState = mState;
|
||||
if (newShiftLockState) {
|
||||
switch (oldState) {
|
||||
case UNSHIFTED:
|
||||
case MANUAL_SHIFTED:
|
||||
case AUTOMATIC_SHIFTED:
|
||||
mState = SHIFT_LOCKED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
mState = UNSHIFTED;
|
||||
}
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "setShiftLocked(" + newShiftLockState + "): " + toString(oldState)
|
||||
+ " > " + this);
|
||||
}
|
||||
|
||||
public void setAutomaticShifted() {
|
||||
mState = AUTOMATIC_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isShiftedOrShiftLocked() {
|
||||
return mState != UNSHIFTED;
|
||||
}
|
||||
|
||||
public boolean isShiftLocked() {
|
||||
return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isShiftLockShifted() {
|
||||
return mState == SHIFT_LOCK_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isAutomaticShifted() {
|
||||
return mState == AUTOMATIC_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isManualShifted() {
|
||||
return mState == MANUAL_SHIFTED || mState == SHIFT_LOCK_SHIFTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(mState);
|
||||
}
|
||||
|
||||
private static String toString(int state) {
|
||||
switch (state) {
|
||||
case UNSHIFTED:
|
||||
return "UNSHIFTED";
|
||||
case MANUAL_SHIFTED:
|
||||
return "MANUAL_SHIFTED";
|
||||
case AUTOMATIC_SHIFTED:
|
||||
return "AUTOMATIC_SHIFTED";
|
||||
case SHIFT_LOCKED:
|
||||
return "SHIFT_LOCKED";
|
||||
case SHIFT_LOCK_SHIFTED:
|
||||
return "SHIFT_LOCK_SHIFTED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.internal;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.define.DebugFlags;
|
||||
|
||||
// This hack is applied to certain classes of tablets.
|
||||
public final class BogusMoveEventDetector {
|
||||
private static final String TAG = BogusMoveEventDetector.class.getSimpleName();
|
||||
private static final boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED;
|
||||
|
||||
// Move these thresholds to resource.
|
||||
// These thresholds' unit is a diagonal length of a key.
|
||||
private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f;
|
||||
|
||||
private static boolean sNeedsProximateBogusDownMoveUpEventHack;
|
||||
|
||||
public static void init(final Resources res) {
|
||||
// The proximate bogus down move up event hack is needed for a device such like,
|
||||
// 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi.
|
||||
// Though it seems odd to use screen density as criteria of the quality of the touch
|
||||
// screen, the small table that has a less density screen than hdpi most likely has been
|
||||
// made with the touch screen that needs the hack.
|
||||
final int screenMetrics = res.getInteger(R.integer.config_screen_metrics);
|
||||
final boolean isLargeTablet = (screenMetrics == Constants.SCREEN_METRICS_LARGE_TABLET);
|
||||
final boolean isSmallTablet = (screenMetrics == Constants.SCREEN_METRICS_SMALL_TABLET);
|
||||
final int densityDpi = res.getDisplayMetrics().densityDpi;
|
||||
final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH);
|
||||
final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen);
|
||||
if (DEBUG_MODE) {
|
||||
final int sw = res.getConfiguration().smallestScreenWidthDp;
|
||||
Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack
|
||||
+ " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi
|
||||
+ " screenMetrics=" + screenMetrics);
|
||||
}
|
||||
sNeedsProximateBogusDownMoveUpEventHack = needsTheHack;
|
||||
}
|
||||
|
||||
private int mAccumulatedDistanceThreshold;
|
||||
|
||||
// Accumulated distance from actual and artificial down keys.
|
||||
/* package */ int mAccumulatedDistanceFromDownKey;
|
||||
private int mActualDownX;
|
||||
private int mActualDownY;
|
||||
|
||||
public void setKeyboardGeometry(final int keyPaddedWidth, final int keyPaddedHeight) {
|
||||
final float keyDiagonal = (float) Math.hypot(keyPaddedWidth, keyPaddedHeight);
|
||||
mAccumulatedDistanceThreshold = (int) (
|
||||
keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD);
|
||||
}
|
||||
|
||||
public void onActualDownEvent(final int x, final int y) {
|
||||
mActualDownX = x;
|
||||
mActualDownY = y;
|
||||
}
|
||||
|
||||
public void onDownKey() {
|
||||
mAccumulatedDistanceFromDownKey = 0;
|
||||
}
|
||||
|
||||
public void onMoveKey(final int distance) {
|
||||
mAccumulatedDistanceFromDownKey += distance;
|
||||
}
|
||||
|
||||
public boolean hasTraveledLongDistance(final int x, final int y) {
|
||||
if (!sNeedsProximateBogusDownMoveUpEventHack) {
|
||||
return false;
|
||||
}
|
||||
final int dx = Math.abs(x - mActualDownX);
|
||||
final int dy = Math.abs(y - mActualDownY);
|
||||
// A bogus move event should be a horizontal movement. A vertical movement might be
|
||||
// a sloppy typing and should be ignored.
|
||||
return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold;
|
||||
}
|
||||
|
||||
public int getAccumulatedDistanceFromDownKey() {
|
||||
return mAccumulatedDistanceFromDownKey;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
|
||||
public final class DrawingPreviewPlacerView extends RelativeLayout {
|
||||
private final int[] mKeyboardViewOrigin = CoordinateUtils.newInstance();
|
||||
|
||||
public DrawingPreviewPlacerView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
public void setKeyboardViewGeometry(final int[] originCoords) {
|
||||
CoordinateUtils.copy(mKeyboardViewOrigin, originCoords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(final Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
final int originX = CoordinateUtils.x(mKeyboardViewOrigin);
|
||||
final int originY = CoordinateUtils.y(mKeyboardViewOrigin);
|
||||
canvas.translate(originX, originY);
|
||||
canvas.translate(-originX, -originY);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.MoreKeysPanel;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.PointerTracker;
|
||||
|
||||
public interface DrawingProxy {
|
||||
/**
|
||||
* Called when a key is being pressed.
|
||||
*
|
||||
* @param key the {@link Key} that is being pressed.
|
||||
* @param withPreview true if key popup preview should be displayed.
|
||||
*/
|
||||
void onKeyPressed(Key key, boolean withPreview);
|
||||
|
||||
/**
|
||||
* Called when a key is being released.
|
||||
*
|
||||
* @param key the {@link Key} that is being released.
|
||||
* @param withAnimation when true, key popup preview should be dismissed with animation.
|
||||
*/
|
||||
void onKeyReleased(Key key, boolean withAnimation);
|
||||
|
||||
/**
|
||||
* Start showing more keys keyboard of a key that is being long pressed.
|
||||
*
|
||||
* @param key the {@link Key} that is being long pressed and showing more keys keyboard.
|
||||
* @param tracker the {@link PointerTracker} that detects this long pressing.
|
||||
* @return {@link MoreKeysPanel} that is being shown. null if there is no need to show more keys
|
||||
* keyboard.
|
||||
*/
|
||||
MoreKeysPanel showMoreKeysKeyboard(Key key, PointerTracker tracker);
|
||||
|
||||
/**
|
||||
* Start a while-typing-animation.
|
||||
*
|
||||
* @param fadeInOrOut {@link #FADE_IN} starts while-typing-fade-in animation.
|
||||
* {@link #FADE_OUT} starts while-typing-fade-out animation.
|
||||
*/
|
||||
void startWhileTypingAnimation(int fadeInOrOut);
|
||||
|
||||
int FADE_IN = 0;
|
||||
int FADE_OUT = 1;
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
|
||||
public final class KeyDrawParams {
|
||||
public Typeface mTypeface = Typeface.DEFAULT;
|
||||
|
||||
public int mLetterSize;
|
||||
public int mLabelSize;
|
||||
public int mLargeLetterSize;
|
||||
public int mHintLetterSize;
|
||||
public int mShiftedLetterHintSize;
|
||||
public int mHintLabelSize;
|
||||
public int mPreviewTextSize;
|
||||
|
||||
public int mTextColor;
|
||||
public int mTextInactivatedColor;
|
||||
public int mTextShadowColor;
|
||||
public int mFunctionalTextColor;
|
||||
public int mHintLetterColor;
|
||||
public int mHintLabelColor;
|
||||
public int mShiftedLetterHintInactivatedColor;
|
||||
public int mShiftedLetterHintActivatedColor;
|
||||
public int mPreviewTextColor;
|
||||
|
||||
public float mHintLabelVerticalAdjustment;
|
||||
public float mLabelOffCenterRatio;
|
||||
public float mHintLabelOffCenterRatio;
|
||||
|
||||
public int mAnimAlpha;
|
||||
|
||||
public KeyDrawParams() {
|
||||
}
|
||||
|
||||
private KeyDrawParams(final KeyDrawParams copyFrom) {
|
||||
mTypeface = copyFrom.mTypeface;
|
||||
|
||||
mLetterSize = copyFrom.mLetterSize;
|
||||
mLabelSize = copyFrom.mLabelSize;
|
||||
mLargeLetterSize = copyFrom.mLargeLetterSize;
|
||||
mHintLetterSize = copyFrom.mHintLetterSize;
|
||||
mShiftedLetterHintSize = copyFrom.mShiftedLetterHintSize;
|
||||
mHintLabelSize = copyFrom.mHintLabelSize;
|
||||
mPreviewTextSize = copyFrom.mPreviewTextSize;
|
||||
|
||||
mTextColor = copyFrom.mTextColor;
|
||||
mTextInactivatedColor = copyFrom.mTextInactivatedColor;
|
||||
mTextShadowColor = copyFrom.mTextShadowColor;
|
||||
mFunctionalTextColor = copyFrom.mFunctionalTextColor;
|
||||
mHintLetterColor = copyFrom.mHintLetterColor;
|
||||
mHintLabelColor = copyFrom.mHintLabelColor;
|
||||
mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor;
|
||||
mShiftedLetterHintActivatedColor = copyFrom.mShiftedLetterHintActivatedColor;
|
||||
mPreviewTextColor = copyFrom.mPreviewTextColor;
|
||||
|
||||
mHintLabelVerticalAdjustment = copyFrom.mHintLabelVerticalAdjustment;
|
||||
mLabelOffCenterRatio = copyFrom.mLabelOffCenterRatio;
|
||||
mHintLabelOffCenterRatio = copyFrom.mHintLabelOffCenterRatio;
|
||||
|
||||
mAnimAlpha = copyFrom.mAnimAlpha;
|
||||
}
|
||||
|
||||
public void updateParams(final int keyHeight, final KeyVisualAttributes attr) {
|
||||
if (attr == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attr.mTypeface != null) {
|
||||
mTypeface = attr.mTypeface;
|
||||
}
|
||||
|
||||
mLetterSize = selectTextSizeFromDimensionOrRatio(keyHeight,
|
||||
attr.mLetterSize, attr.mLetterRatio, mLetterSize);
|
||||
mLabelSize = selectTextSizeFromDimensionOrRatio(keyHeight,
|
||||
attr.mLabelSize, attr.mLabelRatio, mLabelSize);
|
||||
mLargeLetterSize = selectTextSize(keyHeight, attr.mLargeLetterRatio, mLargeLetterSize);
|
||||
mHintLetterSize = selectTextSize(keyHeight, attr.mHintLetterRatio, mHintLetterSize);
|
||||
mShiftedLetterHintSize = selectTextSize(keyHeight,
|
||||
attr.mShiftedLetterHintRatio, mShiftedLetterHintSize);
|
||||
mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize);
|
||||
mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize);
|
||||
|
||||
mTextColor = selectColor(attr.mTextColor, mTextColor);
|
||||
mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
|
||||
mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
|
||||
mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor);
|
||||
mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);
|
||||
mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor);
|
||||
mShiftedLetterHintInactivatedColor = selectColor(
|
||||
attr.mShiftedLetterHintInactivatedColor, mShiftedLetterHintInactivatedColor);
|
||||
mShiftedLetterHintActivatedColor = selectColor(
|
||||
attr.mShiftedLetterHintActivatedColor, mShiftedLetterHintActivatedColor);
|
||||
mPreviewTextColor = selectColor(attr.mPreviewTextColor, mPreviewTextColor);
|
||||
|
||||
mHintLabelVerticalAdjustment = selectFloatIfNonZero(
|
||||
attr.mHintLabelVerticalAdjustment, mHintLabelVerticalAdjustment);
|
||||
mLabelOffCenterRatio = selectFloatIfNonZero(
|
||||
attr.mLabelOffCenterRatio, mLabelOffCenterRatio);
|
||||
mHintLabelOffCenterRatio = selectFloatIfNonZero(
|
||||
attr.mHintLabelOffCenterRatio, mHintLabelOffCenterRatio);
|
||||
}
|
||||
|
||||
public KeyDrawParams mayCloneAndUpdateParams(final int keyHeight,
|
||||
final KeyVisualAttributes attr) {
|
||||
if (attr == null) {
|
||||
return this;
|
||||
}
|
||||
final KeyDrawParams newParams = new KeyDrawParams(this);
|
||||
newParams.updateParams(keyHeight, attr);
|
||||
return newParams;
|
||||
}
|
||||
|
||||
private static int selectTextSizeFromDimensionOrRatio(final int keyHeight,
|
||||
final int dimens, final float ratio, final int defaultDimens) {
|
||||
if (ResourceUtils.isValidDimensionPixelSize(dimens)) {
|
||||
return dimens;
|
||||
}
|
||||
if (ResourceUtils.isValidFraction(ratio)) {
|
||||
return (int) (keyHeight * ratio);
|
||||
}
|
||||
return defaultDimens;
|
||||
}
|
||||
|
||||
private static int selectTextSize(final int keyHeight, final float ratio,
|
||||
final int defaultSize) {
|
||||
if (ResourceUtils.isValidFraction(ratio)) {
|
||||
return (int) (keyHeight * ratio);
|
||||
}
|
||||
return defaultSize;
|
||||
}
|
||||
|
||||
private static int selectColor(final int attrColor, final int defaultColor) {
|
||||
if (attrColor != 0) {
|
||||
return attrColor;
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
private static float selectFloatIfNonZero(final float attrFloat, final float defaultFloat) {
|
||||
if (attrFloat != 0) {
|
||||
return attrFloat;
|
||||
}
|
||||
return defaultFloat;
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ViewLayoutUtils;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class controls pop up key previews. This class decides:
|
||||
* - what kind of key previews should be shown.
|
||||
* - where key previews should be placed.
|
||||
* - how key previews should be shown and dismissed.
|
||||
*/
|
||||
public final class KeyPreviewChoreographer {
|
||||
// Free {@link KeyPreviewView} pool that can be used for key preview.
|
||||
private final ArrayDeque<KeyPreviewView> mFreeKeyPreviewViews = new ArrayDeque<>();
|
||||
// Map from {@link Key} to {@link KeyPreviewView} that is currently being displayed as key
|
||||
// preview.
|
||||
private final HashMap<Key, KeyPreviewView> mShowingKeyPreviewViews = new HashMap<>();
|
||||
|
||||
private final KeyPreviewDrawParams mParams;
|
||||
|
||||
public KeyPreviewChoreographer(final KeyPreviewDrawParams params) {
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
public KeyPreviewView getKeyPreviewView(final Key key, final ViewGroup placerView) {
|
||||
KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.remove(key);
|
||||
if (keyPreviewView != null) {
|
||||
keyPreviewView.setScaleX(1);
|
||||
keyPreviewView.setScaleY(1);
|
||||
return keyPreviewView;
|
||||
}
|
||||
keyPreviewView = mFreeKeyPreviewViews.poll();
|
||||
if (keyPreviewView != null) {
|
||||
keyPreviewView.setScaleX(1);
|
||||
keyPreviewView.setScaleY(1);
|
||||
return keyPreviewView;
|
||||
}
|
||||
final Context context = placerView.getContext();
|
||||
keyPreviewView = new KeyPreviewView(context, null /* attrs */);
|
||||
keyPreviewView.setBackgroundResource(mParams.mPreviewBackgroundResId);
|
||||
placerView.addView(keyPreviewView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0));
|
||||
return keyPreviewView;
|
||||
}
|
||||
|
||||
public void dismissKeyPreview(final Key key, final boolean withAnimation) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
final KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.get(key);
|
||||
if (keyPreviewView == null) {
|
||||
return;
|
||||
}
|
||||
final Object tag = keyPreviewView.getTag();
|
||||
if (withAnimation) {
|
||||
if (tag instanceof KeyPreviewAnimators) {
|
||||
final KeyPreviewAnimators animators = (KeyPreviewAnimators) tag;
|
||||
animators.startDismiss();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Dismiss preview without animation.
|
||||
mShowingKeyPreviewViews.remove(key);
|
||||
if (tag instanceof Animator) {
|
||||
((Animator) tag).cancel();
|
||||
}
|
||||
keyPreviewView.setTag(null);
|
||||
keyPreviewView.setVisibility(View.INVISIBLE);
|
||||
mFreeKeyPreviewViews.add(keyPreviewView);
|
||||
}
|
||||
|
||||
public void placeAndShowKeyPreview(final Key key, final KeyboardIconsSet iconsSet,
|
||||
final KeyDrawParams drawParams, final int[] keyboardOrigin,
|
||||
final ViewGroup placerView, final boolean withAnimation) {
|
||||
final KeyPreviewView keyPreviewView = getKeyPreviewView(key, placerView);
|
||||
placeKeyPreview(
|
||||
key, keyPreviewView, iconsSet, drawParams, keyboardOrigin);
|
||||
showKeyPreview(key, keyPreviewView, withAnimation);
|
||||
}
|
||||
|
||||
private void placeKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
|
||||
final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams,
|
||||
final int[] originCoords) {
|
||||
keyPreviewView.setPreviewVisual(key, iconsSet, drawParams);
|
||||
keyPreviewView.measure(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mParams.setGeometry(keyPreviewView);
|
||||
final int previewWidth = Math.max(keyPreviewView.getMeasuredWidth(), mParams.mMinPreviewWidth);
|
||||
final int previewHeight = mParams.mPreviewHeight;
|
||||
final int keyWidth = key.getWidth();
|
||||
// The key preview is horizontally aligned with the center of the visible part of the
|
||||
// parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
|
||||
// the left/right background is used if such background is specified.
|
||||
int previewX = key.getX() - (previewWidth - keyWidth) / 2
|
||||
+ CoordinateUtils.x(originCoords);
|
||||
// The key preview is placed vertically above the top edge of the parent key with an
|
||||
// arbitrary offset.
|
||||
final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset
|
||||
+ CoordinateUtils.y(originCoords);
|
||||
|
||||
ViewLayoutUtils.placeViewAt(
|
||||
keyPreviewView, previewX, previewY, previewWidth, previewHeight);
|
||||
//keyPreviewView.setPivotX(previewWidth / 2.0f);
|
||||
//keyPreviewView.setPivotY(previewHeight);
|
||||
}
|
||||
|
||||
void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
|
||||
final boolean withAnimation) {
|
||||
if (!withAnimation) {
|
||||
keyPreviewView.setVisibility(View.VISIBLE);
|
||||
mShowingKeyPreviewViews.put(key, keyPreviewView);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show preview with animation.
|
||||
final Animator dismissAnimator = createDismissAnimator(key, keyPreviewView);
|
||||
final KeyPreviewAnimators animators = new KeyPreviewAnimators(dismissAnimator);
|
||||
keyPreviewView.setTag(animators);
|
||||
showKeyPreview(key, keyPreviewView, false /* withAnimation */);
|
||||
}
|
||||
|
||||
private Animator createDismissAnimator(final Key key, final KeyPreviewView keyPreviewView) {
|
||||
final Animator dismissAnimator = mParams.createDismissAnimator(keyPreviewView);
|
||||
dismissAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animator) {
|
||||
dismissKeyPreview(key, false /* withAnimation */);
|
||||
}
|
||||
});
|
||||
return dismissAnimator;
|
||||
}
|
||||
|
||||
private static class KeyPreviewAnimators extends AnimatorListenerAdapter {
|
||||
private final Animator mDismissAnimator;
|
||||
|
||||
public KeyPreviewAnimators(final Animator dismissAnimator) {
|
||||
mDismissAnimator = dismissAnimator;
|
||||
}
|
||||
|
||||
public void startDismiss() {
|
||||
mDismissAnimator.start();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorInflater;
|
||||
import android.content.res.TypedArray;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
|
||||
public final class KeyPreviewDrawParams {
|
||||
// XML attributes of {@link MainKeyboardView}.
|
||||
public final int mPreviewOffset;
|
||||
public final int mPreviewHeight;
|
||||
public final int mMinPreviewWidth;
|
||||
public final int mPreviewBackgroundResId;
|
||||
private final int mDismissAnimatorResId;
|
||||
private int mLingerTimeout;
|
||||
private boolean mShowPopup = true;
|
||||
|
||||
// The graphical geometry of the key preview.
|
||||
// <-width->
|
||||
// +-------+ ^
|
||||
// | | |
|
||||
// |preview| height (visible)
|
||||
// | | |
|
||||
// + + ^ v
|
||||
// \ / |offset
|
||||
// +-\ /-+ v
|
||||
// | +-+ |
|
||||
// |parent |
|
||||
// | key|
|
||||
// +-------+
|
||||
// The background of a {@link TextView} being used for a key preview may have invisible
|
||||
// paddings. To align the more keys keyboard panel's visible part with the visible part of
|
||||
// the background, we need to record the width and height of key preview that don't include
|
||||
// invisible paddings.
|
||||
private int mVisibleWidth;
|
||||
private int mVisibleHeight;
|
||||
// The key preview may have an arbitrary offset and its background that may have a bottom
|
||||
// padding. To align the more keys keyboard and the key preview we also need to record the
|
||||
// offset between the top edge of parent key and the bottom of the visible part of key
|
||||
// preview background.
|
||||
private int mVisibleOffset;
|
||||
|
||||
public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr) {
|
||||
mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
|
||||
R.styleable.MainKeyboardView_keyPreviewOffset, 0);
|
||||
mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
|
||||
R.styleable.MainKeyboardView_keyPreviewHeight, 0);
|
||||
mMinPreviewWidth = mainKeyboardViewAttr.getDimensionPixelSize(
|
||||
R.styleable.MainKeyboardView_keyPreviewWidth, 0);
|
||||
mPreviewBackgroundResId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_keyPreviewBackground, 0);
|
||||
mLingerTimeout = mainKeyboardViewAttr.getInt(
|
||||
R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0);
|
||||
mDismissAnimatorResId = mainKeyboardViewAttr.getResourceId(
|
||||
R.styleable.MainKeyboardView_keyPreviewDismissAnimator, 0);
|
||||
}
|
||||
|
||||
public void setVisibleOffset(final int previewVisibleOffset) {
|
||||
mVisibleOffset = previewVisibleOffset;
|
||||
}
|
||||
|
||||
public int getVisibleOffset() {
|
||||
return mVisibleOffset;
|
||||
}
|
||||
|
||||
public void setGeometry(final View previewTextView) {
|
||||
final int previewWidth = Math.max(previewTextView.getMeasuredWidth(), mMinPreviewWidth);
|
||||
|
||||
// The width and height of visible part of the key preview background. The content marker
|
||||
// of the background 9-patch have to cover the visible part of the background.
|
||||
mVisibleWidth = previewWidth - previewTextView.getPaddingLeft()
|
||||
- previewTextView.getPaddingRight();
|
||||
mVisibleHeight = mPreviewHeight - previewTextView.getPaddingTop()
|
||||
- previewTextView.getPaddingBottom();
|
||||
// The distance between the top edge of the parent key and the bottom of the visible part
|
||||
// of the key preview background.
|
||||
setVisibleOffset(mPreviewOffset - previewTextView.getPaddingBottom());
|
||||
}
|
||||
|
||||
public int getVisibleWidth() {
|
||||
return mVisibleWidth;
|
||||
}
|
||||
|
||||
public int getVisibleHeight() {
|
||||
return mVisibleHeight;
|
||||
}
|
||||
|
||||
public void setPopupEnabled(final boolean enabled, final int lingerTimeout) {
|
||||
mShowPopup = enabled;
|
||||
mLingerTimeout = lingerTimeout;
|
||||
}
|
||||
|
||||
public boolean isPopupEnabled() {
|
||||
return mShowPopup;
|
||||
}
|
||||
|
||||
public int getLingerTimeout() {
|
||||
return mLingerTimeout;
|
||||
}
|
||||
|
||||
private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR =
|
||||
new AccelerateInterpolator();
|
||||
|
||||
public Animator createDismissAnimator(final View target) {
|
||||
final Animator animator = AnimatorInflater.loadAnimator(
|
||||
target.getContext(), mDismissAnimatorResId);
|
||||
animator.setTarget(target);
|
||||
animator.setInterpolator(ACCELERATE_INTERPOLATOR);
|
||||
return animator;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* The pop up key preview view.
|
||||
*/
|
||||
public class KeyPreviewView extends TextView {
|
||||
private final Rect mBackgroundPadding = new Rect();
|
||||
private static final HashSet<String> sNoScaleXTextSet = new HashSet<>();
|
||||
|
||||
public KeyPreviewView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public KeyPreviewView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setGravity(Gravity.CENTER);
|
||||
}
|
||||
|
||||
public void setPreviewVisual(final Key key, final KeyboardIconsSet iconsSet,
|
||||
final KeyDrawParams drawParams) {
|
||||
// What we show as preview should match what we show on a key top in onDraw().
|
||||
final int iconId = key.getIconId();
|
||||
if (iconId != KeyboardIconsSet.ICON_UNDEFINED) {
|
||||
setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet));
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setCompoundDrawables(null, null, null, null);
|
||||
setTextColor(drawParams.mPreviewTextColor);
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectPreviewTextSize(drawParams));
|
||||
setTypeface(key.selectPreviewTypeface(drawParams));
|
||||
// TODO Should take care of temporaryShiftLabel here.
|
||||
setTextAndScaleX(key.getPreviewLabel());
|
||||
}
|
||||
|
||||
private void setTextAndScaleX(final String text) {
|
||||
setTextScaleX(1.0f);
|
||||
setText(text);
|
||||
if (sNoScaleXTextSet.contains(text)) {
|
||||
return;
|
||||
}
|
||||
// TODO: Override {@link #setBackground(Drawable)} that is supported from API 16 and
|
||||
// calculate maximum text width.
|
||||
final Drawable background = getBackground();
|
||||
if (background == null) {
|
||||
return;
|
||||
}
|
||||
background.getPadding(mBackgroundPadding);
|
||||
final int maxWidth = background.getIntrinsicWidth() - mBackgroundPadding.left
|
||||
- mBackgroundPadding.right;
|
||||
final float width = getTextWidth(text, getPaint());
|
||||
if (width <= maxWidth) {
|
||||
sNoScaleXTextSet.add(text);
|
||||
return;
|
||||
}
|
||||
setTextScaleX(maxWidth / width);
|
||||
}
|
||||
|
||||
public static void clearTextCache() {
|
||||
sNoScaleXTextSet.clear();
|
||||
}
|
||||
|
||||
private static float getTextWidth(final String text, final TextPaint paint) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return 0.0f;
|
||||
}
|
||||
final int len = text.length();
|
||||
final float[] widths = new float[len];
|
||||
final int count = paint.getTextWidths(text, 0, len, widths);
|
||||
float width = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
width += widths[i];
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/*public void setPreviewBackground(boolean customColorEnabled, int customColor) {
|
||||
final Drawable background = getBackground();
|
||||
if (customColorEnabled)
|
||||
background.setColorFilter(customColor, PorterDuff.Mode.OVERLAY);
|
||||
}*/
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
|
||||
import static com.amnesica.kryptey.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
|
||||
/**
|
||||
* The string parser of the key specification.
|
||||
* <p>
|
||||
* Each key specification is one of the following:
|
||||
* - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
|
||||
* - Label optionally followed by code point (keyLabel|!code/code_name).
|
||||
* - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
|
||||
* - Icon followed by code point (!icon/icon_name|!code/code_name).
|
||||
* Label and keyOutputText are one of the following:
|
||||
* - Literal string.
|
||||
* - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
|
||||
* - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
|
||||
* Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
|
||||
* Code is one of the following:
|
||||
* - Code point presented by hexadecimal string prefixed with "0x"
|
||||
* - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
|
||||
* Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
|
||||
* Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
|
||||
* as well.
|
||||
*/
|
||||
// TODO: Rename to KeySpec and make this class to the key specification object.
|
||||
public final class KeySpecParser {
|
||||
// Constants for parsing.
|
||||
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
|
||||
private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
|
||||
private static final String PREFIX_HEX = "0x";
|
||||
|
||||
private KeySpecParser() {
|
||||
// Intentional empty constructor for utility class.
|
||||
}
|
||||
|
||||
private static boolean hasIcon(final String keySpec) {
|
||||
return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
|
||||
}
|
||||
|
||||
private static boolean hasCode(final String keySpec, final int labelEnd) {
|
||||
if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) {
|
||||
return false;
|
||||
}
|
||||
if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) {
|
||||
return true;
|
||||
}
|
||||
// This is a workaround to have a key that has a supplementary code point. We can't put a
|
||||
// string in resource as a XML entity of a supplementary code point or a surrogate pair.
|
||||
return keySpec.startsWith(PREFIX_HEX, labelEnd + 1);
|
||||
}
|
||||
|
||||
private static String parseEscape(final String text) {
|
||||
if (text.indexOf(BACKSLASH) < 0) {
|
||||
return text;
|
||||
}
|
||||
final int length = text.length();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (int pos = 0; pos < length; pos++) {
|
||||
final char c = text.charAt(pos);
|
||||
if (c == BACKSLASH && pos + 1 < length) {
|
||||
// Skip escape char
|
||||
pos++;
|
||||
sb.append(text.charAt(pos));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static int indexOfLabelEnd(final String keySpec) {
|
||||
final int length = keySpec.length();
|
||||
if (keySpec.indexOf(BACKSLASH) < 0) {
|
||||
final int labelEnd = keySpec.indexOf(VERTICAL_BAR);
|
||||
if (labelEnd == 0) {
|
||||
if (length == 1) {
|
||||
// Treat a sole vertical bar as a special case of key label.
|
||||
return -1;
|
||||
}
|
||||
throw new KeySpecParserError("Empty label");
|
||||
}
|
||||
return labelEnd;
|
||||
}
|
||||
for (int pos = 0; pos < length; pos++) {
|
||||
final char c = keySpec.charAt(pos);
|
||||
if (c == BACKSLASH && pos + 1 < length) {
|
||||
// Skip escape char
|
||||
pos++;
|
||||
} else if (c == VERTICAL_BAR) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) {
|
||||
return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
|
||||
}
|
||||
|
||||
private static String getAfterLabelEnd(final String keySpec, final int labelEnd) {
|
||||
return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
|
||||
}
|
||||
|
||||
private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) {
|
||||
if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
|
||||
return;
|
||||
}
|
||||
throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
|
||||
}
|
||||
|
||||
public static String getLabel(final String keySpec) {
|
||||
if (keySpec == null) {
|
||||
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
|
||||
return null;
|
||||
}
|
||||
if (hasIcon(keySpec)) {
|
||||
return null;
|
||||
}
|
||||
final int labelEnd = indexOfLabelEnd(keySpec);
|
||||
final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
|
||||
if (label.isEmpty()) {
|
||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
private static String getOutputTextInternal(final String keySpec, final int labelEnd) {
|
||||
if (labelEnd <= 0) {
|
||||
return null;
|
||||
}
|
||||
checkDoubleLabelEnd(keySpec, labelEnd);
|
||||
return parseEscape(getAfterLabelEnd(keySpec, labelEnd));
|
||||
}
|
||||
|
||||
public static String getOutputText(final String keySpec) {
|
||||
if (keySpec == null) {
|
||||
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
|
||||
return null;
|
||||
}
|
||||
final int labelEnd = indexOfLabelEnd(keySpec);
|
||||
if (hasCode(keySpec, labelEnd)) {
|
||||
return null;
|
||||
}
|
||||
final String outputText = getOutputTextInternal(keySpec, labelEnd);
|
||||
if (outputText != null) {
|
||||
if (StringUtils.codePointCount(outputText) == 1) {
|
||||
// If output text is one code point, it should be treated as a code.
|
||||
// See {@link #getCode(Resources, String)}.
|
||||
return null;
|
||||
}
|
||||
if (outputText.isEmpty()) {
|
||||
throw new KeySpecParserError("Empty outputText: " + keySpec);
|
||||
}
|
||||
return outputText;
|
||||
}
|
||||
final String label = getLabel(keySpec);
|
||||
if (label == null) {
|
||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||
}
|
||||
// Code is automatically generated for one letter label. See {@link getCode()}.
|
||||
return (StringUtils.codePointCount(label) == 1) ? null : label;
|
||||
}
|
||||
|
||||
public static int getCode(final String keySpec) {
|
||||
if (keySpec == null) {
|
||||
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
|
||||
return CODE_UNSPECIFIED;
|
||||
}
|
||||
final int labelEnd = indexOfLabelEnd(keySpec);
|
||||
if (hasCode(keySpec, labelEnd)) {
|
||||
checkDoubleLabelEnd(keySpec, labelEnd);
|
||||
return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED);
|
||||
}
|
||||
final String outputText = getOutputTextInternal(keySpec, labelEnd);
|
||||
if (outputText != null) {
|
||||
// If output text is one code point, it should be treated as a code.
|
||||
// See {@link #getOutputText(String)}.
|
||||
if (StringUtils.codePointCount(outputText) == 1) {
|
||||
return outputText.codePointAt(0);
|
||||
}
|
||||
return CODE_OUTPUT_TEXT;
|
||||
}
|
||||
final String label = getLabel(keySpec);
|
||||
if (label == null) {
|
||||
throw new KeySpecParserError("Empty label: " + keySpec);
|
||||
}
|
||||
// Code is automatically generated for one letter label.
|
||||
return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
|
||||
}
|
||||
|
||||
public static int parseCode(final String text, final int defaultCode) {
|
||||
if (text == null) {
|
||||
return defaultCode;
|
||||
}
|
||||
if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
|
||||
return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
|
||||
}
|
||||
// This is a workaround to have a key that has a supplementary code point. We can't put a
|
||||
// string in resource as a XML entity of a supplementary code point or a surrogate pair.
|
||||
if (text.startsWith(PREFIX_HEX)) {
|
||||
return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
|
||||
}
|
||||
return defaultCode;
|
||||
}
|
||||
|
||||
public static int getIconId(final String keySpec) {
|
||||
if (keySpec == null) {
|
||||
// TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
|
||||
return KeyboardIconsSet.ICON_UNDEFINED;
|
||||
}
|
||||
if (!hasIcon(keySpec)) {
|
||||
return KeyboardIconsSet.ICON_UNDEFINED;
|
||||
}
|
||||
final int labelEnd = indexOfLabelEnd(keySpec);
|
||||
final String iconName = getBeforeLabelEnd(keySpec, labelEnd)
|
||||
.substring(KeyboardIconsSet.PREFIX_ICON.length());
|
||||
return KeyboardIconsSet.getIconId(iconName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static final class KeySpecParserError extends RuntimeException {
|
||||
public KeySpecParserError(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
|
||||
public abstract class KeyStyle {
|
||||
private final KeyboardTextsSet mTextsSet;
|
||||
|
||||
public abstract String[] getStringArray(TypedArray a, int index);
|
||||
|
||||
public abstract String getString(TypedArray a, int index);
|
||||
|
||||
public abstract int getInt(TypedArray a, int index, int defaultValue);
|
||||
|
||||
public abstract int getFlags(TypedArray a, int index);
|
||||
|
||||
protected KeyStyle(final KeyboardTextsSet textsSet) {
|
||||
mTextsSet = textsSet;
|
||||
}
|
||||
|
||||
protected String parseString(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
return mTextsSet.resolveTextReference(a.getString(index));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String[] parseStringArray(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
final String text = mTextsSet.resolveTextReference(a.getString(index));
|
||||
return MoreKeySpec.splitKeySpecs(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class KeyStylesSet {
|
||||
private static final String TAG = KeyStylesSet.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private final HashMap<String, KeyStyle> mStyles = new HashMap<>();
|
||||
|
||||
private final KeyboardTextsSet mTextsSet;
|
||||
private final KeyStyle mEmptyKeyStyle;
|
||||
private static final String EMPTY_STYLE_NAME = "<empty>";
|
||||
|
||||
public KeyStylesSet(final KeyboardTextsSet textsSet) {
|
||||
mTextsSet = textsSet;
|
||||
mEmptyKeyStyle = new EmptyKeyStyle(textsSet);
|
||||
mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle);
|
||||
}
|
||||
|
||||
private static final class EmptyKeyStyle extends KeyStyle {
|
||||
EmptyKeyStyle(final KeyboardTextsSet textsSet) {
|
||||
super(textsSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getStringArray(final TypedArray a, final int index) {
|
||||
return parseStringArray(a, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(final TypedArray a, final int index) {
|
||||
return parseString(a, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(final TypedArray a, final int index, final int defaultValue) {
|
||||
return a.getInt(index, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags(final TypedArray a, final int index) {
|
||||
return a.getInt(index, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DeclaredKeyStyle extends KeyStyle {
|
||||
private final HashMap<String, KeyStyle> mStyles;
|
||||
private final String mParentStyleName;
|
||||
private final SparseArray<Object> mStyleAttributes = new SparseArray<>();
|
||||
|
||||
public DeclaredKeyStyle(final String parentStyleName,
|
||||
final KeyboardTextsSet textsSet,
|
||||
final HashMap<String, KeyStyle> styles) {
|
||||
super(textsSet);
|
||||
mParentStyleName = parentStyleName;
|
||||
mStyles = styles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getStringArray(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
return parseStringArray(a, index);
|
||||
}
|
||||
final Object value = mStyleAttributes.get(index);
|
||||
if (value != null) {
|
||||
final String[] array = (String[]) value;
|
||||
return Arrays.copyOf(array, array.length);
|
||||
}
|
||||
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
|
||||
return parentStyle.getStringArray(a, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
return parseString(a, index);
|
||||
}
|
||||
final Object value = mStyleAttributes.get(index);
|
||||
if (value != null) {
|
||||
return (String) value;
|
||||
}
|
||||
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
|
||||
return parentStyle.getString(a, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(final TypedArray a, final int index, final int defaultValue) {
|
||||
if (a.hasValue(index)) {
|
||||
return a.getInt(index, defaultValue);
|
||||
}
|
||||
final Object value = mStyleAttributes.get(index);
|
||||
if (value != null) {
|
||||
return (Integer) value;
|
||||
}
|
||||
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
|
||||
return parentStyle.getInt(a, index, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags(final TypedArray a, final int index) {
|
||||
final int parentFlags = mStyles.get(mParentStyleName).getFlags(a, index);
|
||||
final Integer value = (Integer) mStyleAttributes.get(index);
|
||||
final int styleFlags = (value != null) ? value : 0;
|
||||
final int flags = a.getInt(index, 0);
|
||||
return flags | styleFlags | parentFlags;
|
||||
}
|
||||
|
||||
public void readKeyAttributes(final TypedArray keyAttr) {
|
||||
// TODO: Currently not all Key attributes can be declared as style.
|
||||
readString(keyAttr, R.styleable.Keyboard_Key_altCode);
|
||||
readString(keyAttr, R.styleable.Keyboard_Key_keySpec);
|
||||
readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
|
||||
readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
|
||||
readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
|
||||
readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
|
||||
readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn);
|
||||
readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
|
||||
readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
|
||||
}
|
||||
|
||||
private void readString(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
mStyleAttributes.put(index, parseString(a, index));
|
||||
}
|
||||
}
|
||||
|
||||
private void readInt(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
mStyleAttributes.put(index, a.getInt(index, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private void readFlags(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
final Integer value = (Integer) mStyleAttributes.get(index);
|
||||
final int styleFlags = value != null ? value : 0;
|
||||
mStyleAttributes.put(index, a.getInt(index, 0) | styleFlags);
|
||||
}
|
||||
}
|
||||
|
||||
private void readStringArray(final TypedArray a, final int index) {
|
||||
if (a.hasValue(index)) {
|
||||
mStyleAttributes.put(index, parseStringArray(a, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void parseKeyStyleAttributes(final TypedArray keyStyleAttr, final TypedArray keyAttrs,
|
||||
final XmlPullParser parser) throws XmlPullParserException {
|
||||
final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
|
||||
if (styleName == null) {
|
||||
throw new XmlParseUtils.ParseException(
|
||||
KeyboardBuilder.TAG_KEY_STYLE + " has no styleName attribute", parser);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, String.format("<%s styleName=%s />",
|
||||
KeyboardBuilder.TAG_KEY_STYLE, styleName));
|
||||
if (mStyles.containsKey(styleName)) {
|
||||
Log.d(TAG, KeyboardBuilder.TAG_KEY_STYLE + " " + styleName + " is overridden at "
|
||||
+ parser.getPositionDescription());
|
||||
}
|
||||
}
|
||||
|
||||
final String parentStyleInAttr = keyStyleAttr.getString(
|
||||
R.styleable.Keyboard_KeyStyle_parentStyle);
|
||||
if (parentStyleInAttr != null && !mStyles.containsKey(parentStyleInAttr)) {
|
||||
throw new XmlParseUtils.ParseException(
|
||||
"Unknown parentStyle " + parentStyleInAttr, parser);
|
||||
}
|
||||
final String parentStyleName = (parentStyleInAttr == null) ? EMPTY_STYLE_NAME
|
||||
: parentStyleInAttr;
|
||||
final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName, mTextsSet, mStyles);
|
||||
style.readKeyAttributes(keyAttrs);
|
||||
mStyles.put(styleName, style);
|
||||
}
|
||||
|
||||
public KeyStyle getKeyStyle(final TypedArray keyAttr, final XmlPullParser parser)
|
||||
throws XmlParseUtils.ParseException {
|
||||
final String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
|
||||
if (styleName == null) {
|
||||
return mEmptyKeyStyle;
|
||||
}
|
||||
final KeyStyle style = mStyles.get(styleName);
|
||||
if (style == null) {
|
||||
throw new XmlParseUtils.ParseException("Unknown key style: " + styleName, parser);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
|
||||
public final class KeyVisualAttributes {
|
||||
public final Typeface mTypeface;
|
||||
|
||||
public final float mLetterRatio;
|
||||
public final int mLetterSize;
|
||||
public final float mLabelRatio;
|
||||
public final int mLabelSize;
|
||||
public final float mLargeLetterRatio;
|
||||
public final float mHintLetterRatio;
|
||||
public final float mShiftedLetterHintRatio;
|
||||
public final float mHintLabelRatio;
|
||||
public final float mPreviewTextRatio;
|
||||
|
||||
public final int mTextColor;
|
||||
public final int mTextInactivatedColor;
|
||||
public final int mTextShadowColor;
|
||||
public final int mFunctionalTextColor;
|
||||
public final int mHintLetterColor;
|
||||
public final int mHintLabelColor;
|
||||
public final int mShiftedLetterHintInactivatedColor;
|
||||
public final int mShiftedLetterHintActivatedColor;
|
||||
public final int mPreviewTextColor;
|
||||
|
||||
public final float mHintLabelVerticalAdjustment;
|
||||
public final float mLabelOffCenterRatio;
|
||||
public final float mHintLabelOffCenterRatio;
|
||||
|
||||
private static final int[] VISUAL_ATTRIBUTE_IDS = {
|
||||
R.styleable.Keyboard_Key_keyTypeface,
|
||||
R.styleable.Keyboard_Key_keyLetterSize,
|
||||
R.styleable.Keyboard_Key_keyLabelSize,
|
||||
R.styleable.Keyboard_Key_keyLargeLetterRatio,
|
||||
R.styleable.Keyboard_Key_keyHintLetterRatio,
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintRatio,
|
||||
R.styleable.Keyboard_Key_keyHintLabelRatio,
|
||||
R.styleable.Keyboard_Key_keyPreviewTextRatio,
|
||||
R.styleable.Keyboard_Key_keyTextColor,
|
||||
R.styleable.Keyboard_Key_keyTextInactivatedColor,
|
||||
R.styleable.Keyboard_Key_keyTextShadowColor,
|
||||
R.styleable.Keyboard_Key_functionalTextColor,
|
||||
R.styleable.Keyboard_Key_keyHintLetterColor,
|
||||
R.styleable.Keyboard_Key_keyHintLabelColor,
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor,
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor,
|
||||
R.styleable.Keyboard_Key_keyPreviewTextColor,
|
||||
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment,
|
||||
R.styleable.Keyboard_Key_keyLabelOffCenterRatio,
|
||||
R.styleable.Keyboard_Key_keyHintLabelOffCenterRatio
|
||||
};
|
||||
private static final SparseIntArray sVisualAttributeIds = new SparseIntArray();
|
||||
private static final int ATTR_DEFINED = 1;
|
||||
private static final int ATTR_NOT_FOUND = 0;
|
||||
|
||||
static {
|
||||
for (final int attrId : VISUAL_ATTRIBUTE_IDS) {
|
||||
sVisualAttributeIds.put(attrId, ATTR_DEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyVisualAttributes newInstance(final TypedArray keyAttr) {
|
||||
final int indexCount = keyAttr.getIndexCount();
|
||||
for (int i = 0; i < indexCount; i++) {
|
||||
final int attrId = keyAttr.getIndex(i);
|
||||
if (sVisualAttributeIds.get(attrId, ATTR_NOT_FOUND) == ATTR_NOT_FOUND) {
|
||||
continue;
|
||||
}
|
||||
return new KeyVisualAttributes(keyAttr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private KeyVisualAttributes(final TypedArray keyAttr) {
|
||||
if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) {
|
||||
mTypeface = Typeface.defaultFromStyle(
|
||||
keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL));
|
||||
} else {
|
||||
mTypeface = null;
|
||||
}
|
||||
|
||||
mLetterRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLetterSize);
|
||||
mLetterSize = ResourceUtils.getDimensionPixelSize(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLetterSize);
|
||||
mLabelRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLabelSize);
|
||||
mLabelSize = ResourceUtils.getDimensionPixelSize(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLabelSize);
|
||||
mLargeLetterRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLargeLetterRatio);
|
||||
mHintLetterRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyHintLetterRatio);
|
||||
mShiftedLetterHintRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintRatio);
|
||||
mHintLabelRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyHintLabelRatio);
|
||||
mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyPreviewTextRatio);
|
||||
|
||||
mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
|
||||
mTextInactivatedColor = keyAttr.getColor(
|
||||
R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
|
||||
mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
|
||||
mFunctionalTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0);
|
||||
mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
|
||||
mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
|
||||
mShiftedLetterHintInactivatedColor = keyAttr.getColor(
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
|
||||
mShiftedLetterHintActivatedColor = keyAttr.getColor(
|
||||
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
|
||||
mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
|
||||
|
||||
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);
|
||||
mLabelOffCenterRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyLabelOffCenterRatio, 0.0f);
|
||||
mHintLabelOffCenterRatio = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyHintLabelOffCenterRatio, 0.0f);
|
||||
}
|
||||
}
|
@ -0,0 +1,762 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Keyboard;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardId;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardTheme;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils.ParseException;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Keyboard Building helper.
|
||||
* <p>
|
||||
* This class parses Keyboard XML file and eventually build a Keyboard.
|
||||
* The Keyboard XML file looks like:
|
||||
* <pre>
|
||||
* <!-- xml/keyboard.xml -->
|
||||
* <Keyboard keyboard_attributes*>
|
||||
* <!-- Keyboard Content -->
|
||||
* <Row row_attributes*>
|
||||
* <!-- Row Content -->
|
||||
* <Key key_attributes* />
|
||||
* <Spacer horizontalGap="32.0dp" />
|
||||
* <include keyboardLayout="@xml/other_keys">
|
||||
* ...
|
||||
* </Row>
|
||||
* <include keyboardLayout="@xml/other_rows">
|
||||
* ...
|
||||
* </Keyboard>
|
||||
* </pre>
|
||||
* The XML file which is included in other file must have <merge> as root element,
|
||||
* such as:
|
||||
* <pre>
|
||||
* <!-- xml/other_keys.xml -->
|
||||
* <merge>
|
||||
* <Key key_attributes* />
|
||||
* ...
|
||||
* </merge>
|
||||
* </pre>
|
||||
* and
|
||||
* <pre>
|
||||
* <!-- xml/other_rows.xml -->
|
||||
* <merge>
|
||||
* <Row row_attributes*>
|
||||
* <Key key_attributes* />
|
||||
* </Row>
|
||||
* ...
|
||||
* </merge>
|
||||
* </pre>
|
||||
* You can also use switch-case-default tags to select Rows and Keys.
|
||||
* <pre>
|
||||
* <switch>
|
||||
* <case case_attribute*>
|
||||
* <!-- Any valid tags at switch position -->
|
||||
* </case>
|
||||
* ...
|
||||
* <default>
|
||||
* <!-- Any valid tags at switch position -->
|
||||
* </default>
|
||||
* </switch>
|
||||
* </pre>
|
||||
* You can declare Key style and specify styles within Key tags.
|
||||
* <pre>
|
||||
* <switch>
|
||||
* <case mode="email">
|
||||
* <key-style styleName="f1-key" parentStyle="modifier-key"
|
||||
* keyLabel=".com"
|
||||
* />
|
||||
* </case>
|
||||
* <case mode="url">
|
||||
* <key-style styleName="f1-key" parentStyle="modifier-key"
|
||||
* keyLabel="http://"
|
||||
* />
|
||||
* </case>
|
||||
* </switch>
|
||||
* ...
|
||||
* <Key keyStyle="shift-key" ... />
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
// TODO: Write unit tests for this class.
|
||||
public class KeyboardBuilder<KP extends KeyboardParams> {
|
||||
private static final String BUILDER_TAG = "Keyboard.Builder";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
// Keyboard XML Tags
|
||||
private static final String TAG_KEYBOARD = "Keyboard";
|
||||
private static final String TAG_ROW = "Row";
|
||||
private static final String TAG_KEY = "Key";
|
||||
private static final String TAG_SPACER = "Spacer";
|
||||
private static final String TAG_INCLUDE = "include";
|
||||
private static final String TAG_MERGE = "merge";
|
||||
private static final String TAG_SWITCH = "switch";
|
||||
private static final String TAG_CASE = "case";
|
||||
private static final String TAG_DEFAULT = "default";
|
||||
public static final String TAG_KEY_STYLE = "key-style";
|
||||
|
||||
private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
|
||||
private static final int DEFAULT_KEYBOARD_ROWS = 4;
|
||||
|
||||
protected final KP mParams;
|
||||
protected final Context mContext;
|
||||
protected final Resources mResources;
|
||||
|
||||
private float mCurrentY = 0;
|
||||
private KeyboardRow mCurrentRow = null;
|
||||
private Key mPreviousKeyInRow = null;
|
||||
private boolean mKeyboardDefined = false;
|
||||
|
||||
public KeyboardBuilder(final Context context, final KP params) {
|
||||
mContext = context;
|
||||
final Resources res = context.getResources();
|
||||
mResources = res;
|
||||
|
||||
mParams = params;
|
||||
|
||||
params.mGridWidth = res.getInteger(R.integer.config_keyboard_grid_width);
|
||||
params.mGridHeight = res.getInteger(R.integer.config_keyboard_grid_height);
|
||||
}
|
||||
|
||||
public void setAllowRedundantMoreKes(final boolean enabled) {
|
||||
mParams.mAllowRedundantMoreKeys = enabled;
|
||||
}
|
||||
|
||||
public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
|
||||
mParams.mId = id;
|
||||
final XmlResourceParser parser = mResources.getXml(xmlId);
|
||||
try {
|
||||
parseKeyboard(parser, false);
|
||||
if (!mKeyboardDefined) {
|
||||
throw new XmlParseUtils.ParseException("No " + TAG_KEYBOARD + " tag was found");
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
} catch (IOException e) {
|
||||
Log.w(BUILDER_TAG, "keyboard XML parse error", e);
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Keyboard build() {
|
||||
return new Keyboard(mParams);
|
||||
}
|
||||
|
||||
private int mIndent;
|
||||
private static final String SPACES = " ";
|
||||
|
||||
private static String spaces(final int count) {
|
||||
return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
|
||||
}
|
||||
|
||||
private void startTag(final String format, final Object... args) {
|
||||
Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
|
||||
}
|
||||
|
||||
private void endTag(final String format, final Object... args) {
|
||||
Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
|
||||
}
|
||||
|
||||
private void startEndTag(final String format, final Object... args) {
|
||||
Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
|
||||
mIndent--;
|
||||
}
|
||||
|
||||
private void parseKeyboard(final XmlPullParser parser, final boolean skip)
|
||||
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_KEYBOARD.equals(tag)) {
|
||||
if (DEBUG) startTag("<%s> %s%s", TAG_KEYBOARD, mParams.mId,
|
||||
skip ? " skipped" : "");
|
||||
if (!skip) {
|
||||
if (mKeyboardDefined) {
|
||||
throw new XmlParseUtils.ParseException("Only one " + TAG_KEYBOARD
|
||||
+ " tag can be defined", parser);
|
||||
}
|
||||
mKeyboardDefined = true;
|
||||
parseKeyboardAttributes(parser);
|
||||
startKeyboard();
|
||||
}
|
||||
parseKeyboardContent(parser, skip);
|
||||
} else if (TAG_SWITCH.equals(tag)) {
|
||||
parseSwitchKeyboard(parser, skip);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
|
||||
}
|
||||
} else if (event == XmlPullParser.END_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (DEBUG) endTag("</%s>", tag);
|
||||
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)) {
|
||||
return;
|
||||
}
|
||||
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardAttributes(final XmlPullParser parser) {
|
||||
final AttributeSet attr = Xml.asAttributeSet(parser);
|
||||
final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
|
||||
attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
|
||||
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
|
||||
try {
|
||||
final KeyboardParams params = mParams;
|
||||
final int height = params.mId.mHeight;
|
||||
final int width = params.mId.mWidth;
|
||||
// The bonus height isn't used to determine the other dimensions (gap/padding) to allow
|
||||
// those to stay consistent between layouts with and without the bonus height added.
|
||||
final int bonusHeight = (int) keyboardAttr.getFraction(R.styleable.Keyboard_bonusHeight,
|
||||
height, height, 0);
|
||||
params.mOccupiedHeight = height + bonusHeight;
|
||||
params.mOccupiedWidth = width;
|
||||
params.mTopPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_keyboardTopPadding, height, 0);
|
||||
params.mBottomPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_keyboardBottomPadding, height, 0);
|
||||
params.mLeftPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_keyboardLeftPadding, width, 0);
|
||||
params.mRightPadding = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_keyboardRightPadding, width, 0);
|
||||
|
||||
params.mHorizontalGap = keyboardAttr.getFraction(
|
||||
R.styleable.Keyboard_horizontalGap, width, width, 0);
|
||||
final float baseWidth = params.mOccupiedWidth - params.mLeftPadding
|
||||
- params.mRightPadding + params.mHorizontalGap;
|
||||
params.mBaseWidth = baseWidth;
|
||||
params.mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyWidth, baseWidth,
|
||||
baseWidth / DEFAULT_KEYBOARD_COLUMNS);
|
||||
// TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
|
||||
// rows are determined based on the entire keyboard height including top and bottom
|
||||
// paddings.
|
||||
params.mVerticalGap = keyboardAttr.getFraction(
|
||||
R.styleable.Keyboard_verticalGap, height, height, 0);
|
||||
final float baseHeight = params.mOccupiedHeight - params.mTopPadding
|
||||
- params.mBottomPadding + params.mVerticalGap;
|
||||
params.mBaseHeight = baseHeight;
|
||||
params.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
|
||||
|
||||
params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
|
||||
|
||||
params.mMoreKeysTemplate = keyboardAttr.getResourceId(
|
||||
R.styleable.Keyboard_moreKeysTemplate, 0);
|
||||
params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
|
||||
R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
|
||||
|
||||
params.mIconsSet.loadIcons(keyboardAttr);
|
||||
params.mTextsSet.setLocale(params.mId.getLocale(), mContext);
|
||||
} finally {
|
||||
keyAttr.recycle();
|
||||
keyboardAttr.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
|
||||
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_ROW.equals(tag)) {
|
||||
final KeyboardRow row = parseRowAttributes(parser);
|
||||
if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
|
||||
if (!skip) {
|
||||
startRow(row);
|
||||
}
|
||||
parseRowContent(parser, row, skip);
|
||||
} else if (TAG_INCLUDE.equals(tag)) {
|
||||
parseIncludeKeyboardContent(parser, skip);
|
||||
} else if (TAG_SWITCH.equals(tag)) {
|
||||
parseSwitchKeyboardContent(parser, skip);
|
||||
} else if (TAG_KEY_STYLE.equals(tag)) {
|
||||
parseKeyStyle(parser, skip);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
|
||||
}
|
||||
} else if (event == XmlPullParser.END_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (DEBUG) endTag("</%s>", tag);
|
||||
if (TAG_KEYBOARD.equals(tag)) {
|
||||
endKeyboard();
|
||||
return;
|
||||
}
|
||||
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
|
||||
return;
|
||||
}
|
||||
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardRow parseRowAttributes(final XmlPullParser parser)
|
||||
throws XmlPullParserException {
|
||||
final AttributeSet attr = Xml.asAttributeSet(parser);
|
||||
final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
|
||||
try {
|
||||
if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
|
||||
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
|
||||
}
|
||||
if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
|
||||
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
|
||||
}
|
||||
return new KeyboardRow(mResources, mParams, parser, mCurrentY);
|
||||
} finally {
|
||||
keyboardAttr.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
|
||||
final boolean skip) 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_KEY.equals(tag)) {
|
||||
parseKey(parser, row, skip);
|
||||
} else if (TAG_SPACER.equals(tag)) {
|
||||
parseSpacer(parser, row, skip);
|
||||
} else if (TAG_INCLUDE.equals(tag)) {
|
||||
parseIncludeRowContent(parser, row, skip);
|
||||
} else if (TAG_SWITCH.equals(tag)) {
|
||||
parseSwitchRowContent(parser, row, skip);
|
||||
} else if (TAG_KEY_STYLE.equals(tag)) {
|
||||
parseKeyStyle(parser, skip);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_ROW);
|
||||
}
|
||||
} else if (event == XmlPullParser.END_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (DEBUG) endTag("</%s>", tag);
|
||||
if (TAG_ROW.equals(tag)) {
|
||||
if (!skip) {
|
||||
endRow(row);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
|
||||
return;
|
||||
}
|
||||
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
if (skip) {
|
||||
XmlParseUtils.checkEndTag(TAG_KEY, parser);
|
||||
if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
|
||||
return;
|
||||
}
|
||||
final TypedArray keyAttr = mResources.obtainAttributes(
|
||||
Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
|
||||
final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
|
||||
final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec);
|
||||
if (TextUtils.isEmpty(keySpec)) {
|
||||
throw new ParseException("Empty keySpec", parser);
|
||||
}
|
||||
final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row);
|
||||
keyAttr.recycle();
|
||||
if (DEBUG) {
|
||||
startEndTag("<%s %s moreKeys=%s />", TAG_KEY, key, Arrays.toString(key.getMoreKeys()));
|
||||
}
|
||||
XmlParseUtils.checkEndTag(TAG_KEY, parser);
|
||||
endKey(key, row);
|
||||
}
|
||||
|
||||
private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
if (skip) {
|
||||
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
|
||||
if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
|
||||
return;
|
||||
}
|
||||
final TypedArray keyAttr = mResources.obtainAttributes(
|
||||
Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
|
||||
final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser);
|
||||
final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row);
|
||||
keyAttr.recycle();
|
||||
if (DEBUG) startEndTag("<%s />", TAG_SPACER);
|
||||
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
|
||||
endKey(spacer, row);
|
||||
}
|
||||
|
||||
private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
parseIncludeInternal(parser, null, skip);
|
||||
}
|
||||
|
||||
private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
|
||||
final boolean skip) throws XmlPullParserException, IOException {
|
||||
parseIncludeInternal(parser, row, skip);
|
||||
}
|
||||
|
||||
private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
|
||||
final boolean skip) throws XmlPullParserException, IOException {
|
||||
if (skip) {
|
||||
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
|
||||
if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
|
||||
return;
|
||||
}
|
||||
final AttributeSet attr = Xml.asAttributeSet(parser);
|
||||
final TypedArray keyboardAttr = mResources.obtainAttributes(
|
||||
attr, R.styleable.Keyboard_Include);
|
||||
final TypedArray includeAttr = mResources.obtainAttributes(
|
||||
attr, R.styleable.Keyboard);
|
||||
mParams.mDefaultRowHeight = ResourceUtils.getDimensionOrFraction(includeAttr,
|
||||
R.styleable.Keyboard_rowHeight, mParams.mBaseHeight, mParams.mDefaultRowHeight);
|
||||
|
||||
final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
|
||||
int keyboardLayout = 0;
|
||||
try {
|
||||
XmlParseUtils.checkAttributeExists(
|
||||
keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
|
||||
TAG_INCLUDE, parser);
|
||||
keyboardLayout = keyboardAttr.getResourceId(
|
||||
R.styleable.Keyboard_Include_keyboardLayout, 0);
|
||||
if (row != null) {
|
||||
// Override current x coordinate.
|
||||
row.updateXPos(keyAttr);
|
||||
// Push current Row attributes and update with new attributes.
|
||||
row.pushRowAttributes(keyAttr);
|
||||
}
|
||||
} finally {
|
||||
keyboardAttr.recycle();
|
||||
keyAttr.recycle();
|
||||
includeAttr.recycle();
|
||||
}
|
||||
|
||||
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
|
||||
if (DEBUG) {
|
||||
startEndTag("<%s keyboardLayout=%s />", TAG_INCLUDE,
|
||||
mResources.getResourceEntryName(keyboardLayout));
|
||||
}
|
||||
final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
|
||||
try {
|
||||
parseMerge(parserForInclude, row, skip);
|
||||
} finally {
|
||||
if (row != null) {
|
||||
// Restore Row attributes.
|
||||
row.popRowAttributes();
|
||||
}
|
||||
parserForInclude.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
if (DEBUG) startTag("<%s>", TAG_MERGE);
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
final int event = parser.next();
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_MERGE.equals(tag)) {
|
||||
if (row == null) {
|
||||
parseKeyboardContent(parser, skip);
|
||||
} else {
|
||||
parseRowContent(parser, row, skip);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new XmlParseUtils.ParseException(
|
||||
"Included keyboard layout must have <merge> root element", parser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseSwitchKeyboard(final XmlPullParser parser, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
parseSwitchInternal(parser, true, null, skip);
|
||||
}
|
||||
|
||||
private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
parseSwitchInternal(parser, false, null, skip);
|
||||
}
|
||||
|
||||
private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
|
||||
final boolean skip) throws XmlPullParserException, IOException {
|
||||
parseSwitchInternal(parser, false, row, skip);
|
||||
}
|
||||
|
||||
private void parseSwitchInternal(final XmlPullParser parser, final boolean parseKeyboard,
|
||||
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
|
||||
if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
|
||||
boolean selected = false;
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
final int event = parser.next();
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_CASE.equals(tag)) {
|
||||
selected |= parseCase(parser, parseKeyboard, row, selected || skip);
|
||||
} else if (TAG_DEFAULT.equals(tag)) {
|
||||
selected |= parseDefault(parser, parseKeyboard, row, selected || skip);
|
||||
} else {
|
||||
throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_SWITCH);
|
||||
}
|
||||
} else if (event == XmlPullParser.END_TAG) {
|
||||
final String tag = parser.getName();
|
||||
if (TAG_SWITCH.equals(tag)) {
|
||||
if (DEBUG) endTag("</%s>", TAG_SWITCH);
|
||||
return;
|
||||
}
|
||||
throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseCase(final XmlPullParser parser, final boolean parseKeyboard,
|
||||
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
|
||||
final boolean selected = parseCaseCondition(parser);
|
||||
if (parseKeyboard) {
|
||||
// Processing Keyboard root.
|
||||
parseKeyboard(parser, !selected || skip);
|
||||
} else if (row == null) {
|
||||
// Processing Rows.
|
||||
parseKeyboardContent(parser, !selected || skip);
|
||||
} else {
|
||||
// Processing Keys.
|
||||
parseRowContent(parser, row, !selected || skip);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private boolean parseCaseCondition(final XmlPullParser parser) {
|
||||
final KeyboardId id = mParams.mId;
|
||||
if (id == null) {
|
||||
return true;
|
||||
}
|
||||
final AttributeSet attr = Xml.asAttributeSet(parser);
|
||||
final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
|
||||
if (DEBUG) startTag("<%s>", TAG_CASE);
|
||||
try {
|
||||
final boolean keyboardLayoutSetMatched = matchString(caseAttr,
|
||||
R.styleable.Keyboard_Case_keyboardLayoutSet,
|
||||
id.mSubtype.getKeyboardLayoutSet());
|
||||
final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
|
||||
R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
|
||||
KeyboardId.elementIdToName(id.mElementId));
|
||||
final boolean keyboardThemeMatched = matchTypedValue(caseAttr,
|
||||
R.styleable.Keyboard_Case_keyboardTheme, id.mThemeId,
|
||||
KeyboardTheme.getKeyboardThemeName(id.mThemeId));
|
||||
final boolean modeMatched = matchTypedValue(caseAttr,
|
||||
R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
|
||||
final boolean navigateNextMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
|
||||
final boolean navigatePreviousMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
|
||||
final boolean passwordInputMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
|
||||
final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
|
||||
id.mLanguageSwitchKeyEnabled);
|
||||
final boolean isMultiLineMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
|
||||
final boolean imeActionMatched = matchInteger(caseAttr,
|
||||
R.styleable.Keyboard_Case_imeAction, id.imeAction());
|
||||
final boolean isIconDefinedMatched = isIconDefined(caseAttr,
|
||||
R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet);
|
||||
final Locale locale = id.getLocale();
|
||||
final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locale);
|
||||
final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locale);
|
||||
final boolean countryCodeMatched = matchCountryCodes(caseAttr, locale);
|
||||
final boolean showMoreKeysMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_showExtraChars, id.mShowMoreKeys);
|
||||
final boolean showNumberRowMatched = matchBoolean(caseAttr,
|
||||
R.styleable.Keyboard_Case_showNumberRow, id.mShowNumberRow);
|
||||
final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
|
||||
&& keyboardThemeMatched && modeMatched && navigateNextMatched
|
||||
&& navigatePreviousMatched && passwordInputMatched
|
||||
&& languageSwitchKeyEnabledMatched
|
||||
&& isMultiLineMatched && imeActionMatched && isIconDefinedMatched
|
||||
&& localeCodeMatched && languageCodeMatched && countryCodeMatched
|
||||
&& showMoreKeysMatched && showNumberRowMatched;
|
||||
|
||||
return selected;
|
||||
} finally {
|
||||
caseAttr.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matchLocaleCodes(TypedArray caseAttr, final Locale locale) {
|
||||
return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locale.toString());
|
||||
}
|
||||
|
||||
private static boolean matchLanguageCodes(TypedArray caseAttr, Locale locale) {
|
||||
return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, locale.getLanguage());
|
||||
}
|
||||
|
||||
private static boolean matchCountryCodes(TypedArray caseAttr, Locale locale) {
|
||||
return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, locale.getCountry());
|
||||
}
|
||||
|
||||
private static boolean matchInteger(final TypedArray a, final int index, final int value) {
|
||||
// If <case> does not have "index" attribute, that means this <case> is wild-card for
|
||||
// the attribute.
|
||||
return !a.hasValue(index) || a.getInt(index, 0) == value;
|
||||
}
|
||||
|
||||
private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
|
||||
// If <case> does not have "index" attribute, that means this <case> is wild-card for
|
||||
// the attribute.
|
||||
return !a.hasValue(index) || a.getBoolean(index, false) == value;
|
||||
}
|
||||
|
||||
private static boolean matchString(final TypedArray a, final int index, final String value) {
|
||||
// If <case> does not have "index" attribute, that means this <case> is wild-card for
|
||||
// the attribute.
|
||||
return !a.hasValue(index)
|
||||
|| StringUtils.containsInArray(value, a.getString(index).split("\\|"));
|
||||
}
|
||||
|
||||
private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
|
||||
final String strValue) {
|
||||
// If <case> does not have "index" attribute, that means this <case> is wild-card for
|
||||
// the attribute.
|
||||
final TypedValue v = a.peekValue(index);
|
||||
if (v == null) {
|
||||
return true;
|
||||
}
|
||||
if (ResourceUtils.isIntegerValue(v)) {
|
||||
return intValue == a.getInt(index, 0);
|
||||
}
|
||||
if (ResourceUtils.isStringValue(v)) {
|
||||
return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isIconDefined(final TypedArray a, final int index,
|
||||
final KeyboardIconsSet iconsSet) {
|
||||
if (!a.hasValue(index)) {
|
||||
return true;
|
||||
}
|
||||
final String iconName = a.getString(index);
|
||||
final int iconId = KeyboardIconsSet.getIconId(iconName);
|
||||
return iconsSet.getIconDrawable(iconId) != null;
|
||||
}
|
||||
|
||||
private boolean parseDefault(final XmlPullParser parser, final boolean parseKeyboard,
|
||||
final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException {
|
||||
if (DEBUG) startTag("<%s>", TAG_DEFAULT);
|
||||
if (parseKeyboard) {
|
||||
parseKeyboard(parser, skip);
|
||||
} else if (row == null) {
|
||||
parseKeyboardContent(parser, skip);
|
||||
} else {
|
||||
parseRowContent(parser, row, skip);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
|
||||
throws XmlPullParserException, IOException {
|
||||
final AttributeSet attr = Xml.asAttributeSet(parser);
|
||||
final TypedArray keyStyleAttr = mResources.obtainAttributes(
|
||||
attr, R.styleable.Keyboard_KeyStyle);
|
||||
final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
|
||||
try {
|
||||
if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
|
||||
throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
|
||||
+ "/> needs styleName attribute", parser);
|
||||
}
|
||||
if (DEBUG) {
|
||||
startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
|
||||
keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
|
||||
skip ? " skipped" : "");
|
||||
}
|
||||
if (!skip) {
|
||||
mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
|
||||
}
|
||||
} finally {
|
||||
keyStyleAttr.recycle();
|
||||
keyAttrs.recycle();
|
||||
}
|
||||
XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
|
||||
}
|
||||
|
||||
private void startKeyboard() {
|
||||
|
||||
}
|
||||
|
||||
private void startRow(final KeyboardRow row) {
|
||||
mCurrentRow = row;
|
||||
mPreviousKeyInRow = null;
|
||||
}
|
||||
|
||||
private void endRow(final KeyboardRow row) {
|
||||
if (mCurrentRow == null) {
|
||||
throw new RuntimeException("orphan end row tag");
|
||||
}
|
||||
if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
|
||||
setKeyHitboxRightEdge(mPreviousKeyInRow, mParams.mOccupiedWidth);
|
||||
mPreviousKeyInRow = null;
|
||||
}
|
||||
mCurrentY += row.getRowHeight();
|
||||
mCurrentRow = null;
|
||||
}
|
||||
|
||||
private void endKey(final Key key, final KeyboardRow row) {
|
||||
mParams.onAddKey(key);
|
||||
if (mPreviousKeyInRow != null && !mPreviousKeyInRow.isSpacer()) {
|
||||
// Make the last key span the gap so there isn't un-clickable space. The current key's
|
||||
// hitbox left edge is based on the previous key, so this will make the gap between
|
||||
// them split evenly.
|
||||
setKeyHitboxRightEdge(mPreviousKeyInRow, row.getKeyX() - row.getKeyLeftPadding());
|
||||
}
|
||||
mPreviousKeyInRow = key;
|
||||
}
|
||||
|
||||
private void setKeyHitboxRightEdge(final Key key, final float xPos) {
|
||||
final int keyRight = key.getX() + key.getWidth();
|
||||
final float padding = xPos - keyRight;
|
||||
key.setHitboxRightEdge(Math.round(padding) + keyRight);
|
||||
}
|
||||
|
||||
private void endKeyboard() {
|
||||
mParams.removeRedundantMoreKeys();
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class KeyboardCodesSet {
|
||||
public static final String PREFIX_CODE = "!code/";
|
||||
|
||||
private static final HashMap<String, Integer> sNameToIdMap = new HashMap<>();
|
||||
|
||||
private KeyboardCodesSet() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
public static int getCode(final String name) {
|
||||
Integer id = sNameToIdMap.get(name);
|
||||
if (id == null) throw new RuntimeException("Unknown key code: " + name);
|
||||
return DEFAULT[id];
|
||||
}
|
||||
|
||||
private static final String[] ID_TO_NAME = {
|
||||
"key_tab",
|
||||
"key_enter",
|
||||
"key_space",
|
||||
"key_shift",
|
||||
"key_capslock",
|
||||
"key_switch_alpha_symbol",
|
||||
"key_output_text",
|
||||
"key_delete",
|
||||
"key_settings",
|
||||
"key_action_next",
|
||||
"key_action_previous",
|
||||
"key_shift_enter",
|
||||
"key_language_switch",
|
||||
"key_left",
|
||||
"key_right",
|
||||
"key_unspecified",
|
||||
};
|
||||
|
||||
private static final int[] DEFAULT = {
|
||||
Constants.CODE_TAB,
|
||||
Constants.CODE_ENTER,
|
||||
Constants.CODE_SPACE,
|
||||
Constants.CODE_SHIFT,
|
||||
Constants.CODE_CAPSLOCK,
|
||||
Constants.CODE_SWITCH_ALPHA_SYMBOL,
|
||||
Constants.CODE_OUTPUT_TEXT,
|
||||
Constants.CODE_DELETE,
|
||||
Constants.CODE_SETTINGS,
|
||||
Constants.CODE_ACTION_NEXT,
|
||||
Constants.CODE_ACTION_PREVIOUS,
|
||||
Constants.CODE_SHIFT_ENTER,
|
||||
Constants.CODE_LANGUAGE_SWITCH,
|
||||
Constants.CODE_UNSPECIFIED,
|
||||
};
|
||||
|
||||
static {
|
||||
for (int i = 0; i < ID_TO_NAME.length; i++) {
|
||||
sNameToIdMap.put(ID_TO_NAME[i], i);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class KeyboardIconsSet {
|
||||
private static final String TAG = KeyboardIconsSet.class.getSimpleName();
|
||||
|
||||
public static final String PREFIX_ICON = "!icon/";
|
||||
public static final int ICON_UNDEFINED = 0;
|
||||
private static final int ATTR_UNDEFINED = 0;
|
||||
|
||||
private static final String NAME_UNDEFINED = "undefined";
|
||||
public static final String NAME_SHIFT_KEY = "shift_key";
|
||||
public static final String NAME_SHIFT_KEY_SHIFTED = "shift_key_shifted";
|
||||
public static final String NAME_DELETE_KEY = "delete_key";
|
||||
public static final String NAME_SETTINGS_KEY = "settings_key";
|
||||
public static final String NAME_SPACE_KEY = "space_key";
|
||||
public static final String NAME_SPACE_KEY_FOR_NUMBER_LAYOUT = "space_key_for_number_layout";
|
||||
public static final String NAME_ENTER_KEY = "enter_key";
|
||||
public static final String NAME_GO_KEY = "go_key";
|
||||
public static final String NAME_SEARCH_KEY = "search_key";
|
||||
public static final String NAME_SEND_KEY = "send_key";
|
||||
public static final String NAME_NEXT_KEY = "next_key";
|
||||
public static final String NAME_DONE_KEY = "done_key";
|
||||
public static final String NAME_PREVIOUS_KEY = "previous_key";
|
||||
public static final String NAME_TAB_KEY = "tab_key";
|
||||
public static final String NAME_LANGUAGE_SWITCH_KEY = "language_switch_key";
|
||||
public static final String NAME_ZWNJ_KEY = "zwnj_key";
|
||||
public static final String NAME_ZWJ_KEY = "zwj_key";
|
||||
|
||||
private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray();
|
||||
|
||||
// Icon name to icon id map.
|
||||
private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<>();
|
||||
|
||||
private static final Object[] NAMES_AND_ATTR_IDS = {
|
||||
NAME_UNDEFINED, ATTR_UNDEFINED,
|
||||
NAME_SHIFT_KEY, R.styleable.Keyboard_iconShiftKey,
|
||||
NAME_DELETE_KEY, R.styleable.Keyboard_iconDeleteKey,
|
||||
NAME_SETTINGS_KEY, R.styleable.Keyboard_iconSettingsKey,
|
||||
NAME_SPACE_KEY, R.styleable.Keyboard_iconSpaceKey,
|
||||
NAME_ENTER_KEY, R.styleable.Keyboard_iconEnterKey,
|
||||
NAME_GO_KEY, R.styleable.Keyboard_iconGoKey,
|
||||
NAME_SEARCH_KEY, R.styleable.Keyboard_iconSearchKey,
|
||||
NAME_SEND_KEY, R.styleable.Keyboard_iconSendKey,
|
||||
NAME_NEXT_KEY, R.styleable.Keyboard_iconNextKey,
|
||||
NAME_DONE_KEY, R.styleable.Keyboard_iconDoneKey,
|
||||
NAME_PREVIOUS_KEY, R.styleable.Keyboard_iconPreviousKey,
|
||||
NAME_TAB_KEY, R.styleable.Keyboard_iconTabKey,
|
||||
NAME_SPACE_KEY_FOR_NUMBER_LAYOUT, R.styleable.Keyboard_iconSpaceKeyForNumberLayout,
|
||||
NAME_SHIFT_KEY_SHIFTED, R.styleable.Keyboard_iconShiftKeyShifted,
|
||||
NAME_LANGUAGE_SWITCH_KEY, R.styleable.Keyboard_iconLanguageSwitchKey,
|
||||
NAME_ZWNJ_KEY, R.styleable.Keyboard_iconZwnjKey,
|
||||
NAME_ZWJ_KEY, R.styleable.Keyboard_iconZwjKey,
|
||||
};
|
||||
|
||||
private static final int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2;
|
||||
private static final String[] ICON_NAMES = new String[NUM_ICONS];
|
||||
private final Drawable[] mIcons = new Drawable[NUM_ICONS];
|
||||
private final int[] mIconResourceIds = new int[NUM_ICONS];
|
||||
|
||||
static {
|
||||
int iconId = ICON_UNDEFINED;
|
||||
for (int i = 0; i < NAMES_AND_ATTR_IDS.length; i += 2) {
|
||||
final String name = (String) NAMES_AND_ATTR_IDS[i];
|
||||
final Integer attrId = (Integer) NAMES_AND_ATTR_IDS[i + 1];
|
||||
if (attrId != ATTR_UNDEFINED) {
|
||||
ATTR_ID_TO_ICON_ID.put(attrId, iconId);
|
||||
}
|
||||
sNameToIdsMap.put(name, iconId);
|
||||
ICON_NAMES[iconId] = name;
|
||||
iconId++;
|
||||
}
|
||||
}
|
||||
|
||||
public void loadIcons(final TypedArray keyboardAttrs) {
|
||||
final int size = ATTR_ID_TO_ICON_ID.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
final int attrId = ATTR_ID_TO_ICON_ID.keyAt(index);
|
||||
try {
|
||||
final Drawable icon = keyboardAttrs.getDrawable(attrId);
|
||||
setDefaultBounds(icon);
|
||||
final Integer iconId = ATTR_ID_TO_ICON_ID.get(attrId);
|
||||
mIcons[iconId] = icon;
|
||||
mIconResourceIds[iconId] = keyboardAttrs.getResourceId(attrId, 0);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
Log.w(TAG, "Drawable resource for icon #"
|
||||
+ keyboardAttrs.getResources().getResourceEntryName(attrId)
|
||||
+ " not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidIconId(final int iconId) {
|
||||
return iconId >= 0 && iconId < ICON_NAMES.length;
|
||||
}
|
||||
|
||||
public static String getIconName(final int iconId) {
|
||||
return isValidIconId(iconId) ? ICON_NAMES[iconId] : "unknown<" + iconId + ">";
|
||||
}
|
||||
|
||||
public static int getIconId(final String name) {
|
||||
Integer iconId = sNameToIdsMap.get(name);
|
||||
if (iconId != null) {
|
||||
return iconId;
|
||||
}
|
||||
throw new RuntimeException("unknown icon name: " + name);
|
||||
}
|
||||
|
||||
public Drawable getIconDrawable(final int iconId) {
|
||||
if (isValidIconId(iconId)) {
|
||||
return mIcons[iconId];
|
||||
}
|
||||
throw new RuntimeException("unknown icon id: " + getIconName(iconId));
|
||||
}
|
||||
|
||||
private static void setDefaultBounds(final Drawable icon) {
|
||||
if (icon != null) {
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardId;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class KeyboardParams {
|
||||
public KeyboardId mId;
|
||||
|
||||
/**
|
||||
* Total height and width of the keyboard, including the paddings and keys
|
||||
*/
|
||||
public int mOccupiedHeight;
|
||||
public int mOccupiedWidth;
|
||||
|
||||
/**
|
||||
* Base height and width of the keyboard used to calculate rows' or keys' heights and
|
||||
* widths
|
||||
*/
|
||||
public float mBaseHeight;
|
||||
public float mBaseWidth;
|
||||
|
||||
public float mTopPadding;
|
||||
public float mBottomPadding;
|
||||
public float mLeftPadding;
|
||||
public float mRightPadding;
|
||||
|
||||
public KeyVisualAttributes mKeyVisualAttributes;
|
||||
|
||||
public float mDefaultRowHeight;
|
||||
public float mDefaultKeyPaddedWidth;
|
||||
public float mHorizontalGap;
|
||||
public float mVerticalGap;
|
||||
|
||||
public int mMoreKeysTemplate;
|
||||
public int mMaxMoreKeysKeyboardColumn;
|
||||
|
||||
public int mGridWidth;
|
||||
public int mGridHeight;
|
||||
|
||||
// Keys are sorted from top-left to bottom-right order.
|
||||
public final SortedSet<Key> mSortedKeys = new TreeSet<>(ROW_COLUMN_COMPARATOR);
|
||||
public final ArrayList<Key> mShiftKeys = new ArrayList<>();
|
||||
public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<>();
|
||||
public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
|
||||
public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
|
||||
public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
|
||||
|
||||
private final UniqueKeysCache mUniqueKeysCache;
|
||||
public boolean mAllowRedundantMoreKeys;
|
||||
|
||||
public int mMostCommonKeyHeight = 0;
|
||||
public int mMostCommonKeyWidth = 0;
|
||||
|
||||
// Comparator to sort {@link Key}s from top-left to bottom-right order.
|
||||
private static final Comparator<Key> ROW_COLUMN_COMPARATOR = new Comparator<Key>() {
|
||||
@Override
|
||||
public int compare(final Key lhs, final Key rhs) {
|
||||
if (lhs.getY() < rhs.getY()) return -1;
|
||||
if (lhs.getY() > rhs.getY()) return 1;
|
||||
if (lhs.getX() < rhs.getX()) return -1;
|
||||
if (lhs.getX() > rhs.getX()) return 1;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
public KeyboardParams() {
|
||||
this(UniqueKeysCache.NO_CACHE);
|
||||
}
|
||||
|
||||
public KeyboardParams(final UniqueKeysCache keysCache) {
|
||||
mUniqueKeysCache = keysCache;
|
||||
}
|
||||
|
||||
public void onAddKey(final Key newKey) {
|
||||
final Key key = mUniqueKeysCache.getUniqueKey(newKey);
|
||||
final boolean isSpacer = key.isSpacer();
|
||||
if (isSpacer && key.getWidth() == 0) {
|
||||
// Ignore zero width {@link Spacer}.
|
||||
return;
|
||||
}
|
||||
mSortedKeys.add(key);
|
||||
if (isSpacer) {
|
||||
return;
|
||||
}
|
||||
updateHistogram(key);
|
||||
if (key.getCode() == Constants.CODE_SHIFT) {
|
||||
mShiftKeys.add(key);
|
||||
}
|
||||
if (key.altCodeWhileTyping()) {
|
||||
mAltCodeKeysWhileTyping.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeRedundantMoreKeys() {
|
||||
if (mAllowRedundantMoreKeys) {
|
||||
return;
|
||||
}
|
||||
final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout =
|
||||
new MoreKeySpec.LettersOnBaseLayout();
|
||||
for (final Key key : mSortedKeys) {
|
||||
lettersOnBaseLayout.addLetter(key);
|
||||
}
|
||||
final ArrayList<Key> allKeys = new ArrayList<>(mSortedKeys);
|
||||
mSortedKeys.clear();
|
||||
for (final Key key : allKeys) {
|
||||
final Key filteredKey = Key.removeRedundantMoreKeys(key, lettersOnBaseLayout);
|
||||
mSortedKeys.add(mUniqueKeysCache.getUniqueKey(filteredKey));
|
||||
}
|
||||
}
|
||||
|
||||
private int mMaxHeightCount = 0;
|
||||
private int mMaxWidthCount = 0;
|
||||
private final SparseIntArray mHeightHistogram = new SparseIntArray();
|
||||
private final SparseIntArray mWidthHistogram = new SparseIntArray();
|
||||
|
||||
private static int updateHistogramCounter(final SparseIntArray histogram, final int key) {
|
||||
final int index = histogram.indexOfKey(key);
|
||||
final int count = (index >= 0 ? histogram.get(key) : 0) + 1;
|
||||
histogram.put(key, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
private void updateHistogram(final Key key) {
|
||||
final int height = Math.round(key.getDefinedHeight());
|
||||
final int heightCount = updateHistogramCounter(mHeightHistogram, height);
|
||||
if (heightCount > mMaxHeightCount) {
|
||||
mMaxHeightCount = heightCount;
|
||||
mMostCommonKeyHeight = height;
|
||||
}
|
||||
|
||||
final int width = Math.round(key.getDefinedWidth());
|
||||
final int widthCount = updateHistogramCounter(mWidthHistogram, width);
|
||||
if (widthCount > mMaxWidthCount) {
|
||||
mMaxWidthCount = widthCount;
|
||||
mMostCommonKeyWidth = width;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,362 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Keyboard;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
/**
|
||||
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
|
||||
* Some of the key size defaults can be overridden per row from what the {@link Keyboard}
|
||||
* defines.
|
||||
*/
|
||||
public final class KeyboardRow {
|
||||
private static final String TAG = KeyboardRow.class.getSimpleName();
|
||||
private static final float FLOAT_THRESHOLD = 0.0001f;
|
||||
|
||||
// keyWidth enum constants
|
||||
private static final int KEYWIDTH_NOT_ENUM = 0;
|
||||
private static final int KEYWIDTH_FILL_RIGHT = -1;
|
||||
|
||||
private final KeyboardParams mParams;
|
||||
|
||||
/**
|
||||
* The y coordinate of the top edge of all keys in the row, excluding the top padding.
|
||||
*/
|
||||
private final float mY;
|
||||
/**
|
||||
* The height of this row and all keys in it, including the top and bottom padding.
|
||||
*/
|
||||
private final float mRowHeight;
|
||||
/**
|
||||
* The top padding of all of the keys in the row.
|
||||
*/
|
||||
private final float mKeyTopPadding;
|
||||
/**
|
||||
* The bottom padding of all of the keys in the row.
|
||||
*/
|
||||
private final float mKeyBottomPadding;
|
||||
|
||||
/**
|
||||
* A tracker for where the next key should start, excluding padding.
|
||||
*/
|
||||
private float mNextKeyXPos;
|
||||
|
||||
/**
|
||||
* The x coordinate of the left edge of the current key, excluding the left padding.
|
||||
*/
|
||||
private float mCurrentX;
|
||||
/**
|
||||
* The width of the current key excluding the left and right padding.
|
||||
*/
|
||||
private float mCurrentKeyWidth;
|
||||
/**
|
||||
* The left padding of the current key.
|
||||
*/
|
||||
private float mCurrentKeyLeftPadding;
|
||||
/**
|
||||
* The right padding of the current key.
|
||||
*/
|
||||
private float mCurrentKeyRightPadding;
|
||||
|
||||
/**
|
||||
* Flag indicating whether the previous key in the row was a spacer.
|
||||
*/
|
||||
private boolean mLastKeyWasSpacer = false;
|
||||
/**
|
||||
* The x coordinate of the right edge of the previous key, excluding the right padding.
|
||||
*/
|
||||
private float mLastKeyRightEdge = 0;
|
||||
|
||||
private final ArrayDeque<RowAttributes> mRowAttributesStack = new ArrayDeque<>();
|
||||
|
||||
// TODO: Add keyActionFlags.
|
||||
private static class RowAttributes {
|
||||
/**
|
||||
* Default padded width of a key in this row.
|
||||
*/
|
||||
public final float mDefaultKeyPaddedWidth;
|
||||
/**
|
||||
* Default keyLabelFlags in this row.
|
||||
*/
|
||||
public final int mDefaultKeyLabelFlags;
|
||||
/**
|
||||
* Default backgroundType for this row
|
||||
*/
|
||||
public final int mDefaultBackgroundType;
|
||||
|
||||
/**
|
||||
* Parse and create key attributes. This constructor is used to parse Row tag.
|
||||
*
|
||||
* @param keyAttr an attributes array of Row tag.
|
||||
* @param defaultKeyPaddedWidth a default padded key width.
|
||||
* @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
|
||||
*/
|
||||
public RowAttributes(final TypedArray keyAttr, final float defaultKeyPaddedWidth,
|
||||
final float keyboardWidth) {
|
||||
mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyWidth, keyboardWidth, defaultKeyPaddedWidth);
|
||||
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
|
||||
mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
|
||||
Key.BACKGROUND_TYPE_NORMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and update key attributes using default attributes. This constructor is used
|
||||
* to parse include tag.
|
||||
*
|
||||
* @param keyAttr an attributes array of include tag.
|
||||
* @param defaultRowAttr default Row attributes.
|
||||
* @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
|
||||
*/
|
||||
public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr,
|
||||
final float keyboardWidth) {
|
||||
mDefaultKeyPaddedWidth = ResourceUtils.getFraction(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyWidth, keyboardWidth,
|
||||
defaultRowAttr.mDefaultKeyPaddedWidth);
|
||||
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0)
|
||||
| defaultRowAttr.mDefaultKeyLabelFlags;
|
||||
mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
|
||||
defaultRowAttr.mDefaultBackgroundType);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyboardRow(final Resources res, final KeyboardParams params,
|
||||
final XmlPullParser parser, final float y) {
|
||||
mParams = params;
|
||||
final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||
R.styleable.Keyboard);
|
||||
if (y < FLOAT_THRESHOLD) {
|
||||
// The top row should use the keyboard's top padding instead of the vertical gap
|
||||
mKeyTopPadding = params.mTopPadding;
|
||||
} else {
|
||||
// All of the vertical gap will be used as bottom padding rather than split between the
|
||||
// top and bottom because it is probably more likely for users to click below a key
|
||||
mKeyTopPadding = 0;
|
||||
}
|
||||
final float baseRowHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
|
||||
R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
|
||||
float keyHeight = baseRowHeight - params.mVerticalGap;
|
||||
final float rowEndY = y + mKeyTopPadding + keyHeight + params.mVerticalGap;
|
||||
final float keyboardBottomEdge = params.mOccupiedHeight - params.mBottomPadding;
|
||||
if (rowEndY > keyboardBottomEdge - FLOAT_THRESHOLD) {
|
||||
// The bottom row's padding should go to the bottom of the keyboard (this might be
|
||||
// slightly more than the keyboard's bottom padding if the rows don't add up to 100%).
|
||||
// We'll consider it the bottom row as long as the row's normal bottom padding overlaps
|
||||
// with the keyboard's bottom padding any amount.
|
||||
final float keyEndY = y + mKeyTopPadding + keyHeight;
|
||||
final float keyOverflow = keyEndY - keyboardBottomEdge;
|
||||
if (keyOverflow > FLOAT_THRESHOLD) {
|
||||
if (Math.round(keyOverflow) > 0) {
|
||||
// Only bother logging an error when expected rounding wouldn't resolve this
|
||||
Log.e(TAG, "The row is too tall to fit in the keyboard (" + keyOverflow
|
||||
+ " px). The height was reduced to fit.");
|
||||
}
|
||||
keyHeight = Math.max(keyboardBottomEdge - y - mKeyTopPadding, 0);
|
||||
}
|
||||
mKeyBottomPadding = Math.max(params.mOccupiedHeight - keyEndY, 0);
|
||||
} else {
|
||||
mKeyBottomPadding = params.mVerticalGap;
|
||||
}
|
||||
mRowHeight = mKeyTopPadding + keyHeight + mKeyBottomPadding;
|
||||
keyboardAttr.recycle();
|
||||
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||
R.styleable.Keyboard_Key);
|
||||
mRowAttributesStack.push(new RowAttributes(
|
||||
keyAttr, params.mDefaultKeyPaddedWidth, params.mBaseWidth));
|
||||
keyAttr.recycle();
|
||||
|
||||
mY = y + mKeyTopPadding;
|
||||
mLastKeyRightEdge = 0;
|
||||
mNextKeyXPos = params.mLeftPadding;
|
||||
}
|
||||
|
||||
public void pushRowAttributes(final TypedArray keyAttr) {
|
||||
final RowAttributes newAttributes = new RowAttributes(
|
||||
keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth);
|
||||
mRowAttributesStack.push(newAttributes);
|
||||
}
|
||||
|
||||
public void popRowAttributes() {
|
||||
mRowAttributesStack.pop();
|
||||
}
|
||||
|
||||
private float getDefaultKeyPaddedWidth() {
|
||||
return mRowAttributesStack.peek().mDefaultKeyPaddedWidth;
|
||||
}
|
||||
|
||||
public int getDefaultKeyLabelFlags() {
|
||||
return mRowAttributesStack.peek().mDefaultKeyLabelFlags;
|
||||
}
|
||||
|
||||
public int getDefaultBackgroundType() {
|
||||
return mRowAttributesStack.peek().mDefaultBackgroundType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the x position for the next key based on what is set in the keyXPos attribute.
|
||||
*
|
||||
* @param keyAttr the Key XML attributes array.
|
||||
*/
|
||||
public void updateXPos(final TypedArray keyAttr) {
|
||||
if (keyAttr == null || !keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keyXPos is based on the base width, but we need to add in the keyboard padding to
|
||||
// determine the actual position in the keyboard.
|
||||
final float keyXPos = ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyXPos,
|
||||
mParams.mBaseWidth, 0) + mParams.mLeftPadding;
|
||||
// keyXPos shouldn't be less than mLastKeyRightEdge or this key will overlap the adjacent
|
||||
// key on its left hand side.
|
||||
if (keyXPos + FLOAT_THRESHOLD < mLastKeyRightEdge) {
|
||||
Log.e(TAG, "The specified keyXPos (" + keyXPos
|
||||
+ ") is smaller than the next available x position (" + mLastKeyRightEdge
|
||||
+ "). The x position was increased to avoid overlapping keys.");
|
||||
mNextKeyXPos = mLastKeyRightEdge;
|
||||
} else {
|
||||
mNextKeyXPos = keyXPos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the next key's dimensions so they can be retrieved using {@link #getKeyX()},
|
||||
* {@link #getKeyWidth()}, etc.
|
||||
*
|
||||
* @param keyAttr the Key XML attributes array.
|
||||
* @param isSpacer flag indicating if the key is a spacer.
|
||||
*/
|
||||
public void setCurrentKey(final TypedArray keyAttr, final boolean isSpacer) {
|
||||
// Split gap on both sides of key
|
||||
final float defaultGap = mParams.mHorizontalGap / 2;
|
||||
|
||||
updateXPos(keyAttr);
|
||||
final float keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
|
||||
float keyWidth;
|
||||
if (isSpacer) {
|
||||
final float leftGap = Math.min(mNextKeyXPos - mLastKeyRightEdge - defaultGap,
|
||||
defaultGap);
|
||||
// Spacers don't have horizontal gaps but should include that space in its width
|
||||
mCurrentX = mNextKeyXPos - leftGap;
|
||||
keyWidth = getKeyWidth(keyAttr) + leftGap;
|
||||
if (mCurrentX + keyWidth + FLOAT_THRESHOLD < keyboardRightEdge) {
|
||||
// Add what is normally the default right gap for non-edge spacers
|
||||
keyWidth += defaultGap;
|
||||
}
|
||||
mCurrentKeyLeftPadding = 0;
|
||||
mCurrentKeyRightPadding = 0;
|
||||
} else {
|
||||
mCurrentX = mNextKeyXPos;
|
||||
if (mLastKeyRightEdge < FLOAT_THRESHOLD || mLastKeyWasSpacer) {
|
||||
// The first key in the row and a key next to a spacer should have a left padding
|
||||
// that spans the available distance
|
||||
mCurrentKeyLeftPadding = mCurrentX - mLastKeyRightEdge;
|
||||
} else {
|
||||
// Split the gap between the adjacent keys
|
||||
mCurrentKeyLeftPadding = (mCurrentX - mLastKeyRightEdge) / 2;
|
||||
}
|
||||
keyWidth = getKeyWidth(keyAttr);
|
||||
// We can't know this before seeing the next key, so just use the default. The key can
|
||||
// be updated later.
|
||||
mCurrentKeyRightPadding = defaultGap;
|
||||
}
|
||||
final float keyOverflow = mCurrentX + keyWidth - keyboardRightEdge;
|
||||
if (keyOverflow > FLOAT_THRESHOLD) {
|
||||
if (Math.round(keyOverflow) > 0) {
|
||||
// Only bother logging an error when expected rounding wouldn't resolve this
|
||||
Log.e(TAG, "The " + (isSpacer ? "spacer" : "key")
|
||||
+ " is too wide to fit in the keyboard (" + keyOverflow
|
||||
+ " px). The width was reduced to fit.");
|
||||
}
|
||||
keyWidth = Math.max(keyboardRightEdge - mCurrentX, 0);
|
||||
}
|
||||
|
||||
mCurrentKeyWidth = keyWidth;
|
||||
|
||||
// Calculations for the current key are done. Prep for the next key.
|
||||
mLastKeyRightEdge = mCurrentX + keyWidth;
|
||||
mLastKeyWasSpacer = isSpacer;
|
||||
// Set the next key's default position. Spacers only add half because their width includes
|
||||
// what is normally the horizontal gap.
|
||||
mNextKeyXPos = mLastKeyRightEdge + (isSpacer ? defaultGap : mParams.mHorizontalGap);
|
||||
}
|
||||
|
||||
private float getKeyWidth(final TypedArray keyAttr) {
|
||||
if (keyAttr == null) {
|
||||
return getDefaultKeyPaddedWidth() - mParams.mHorizontalGap;
|
||||
}
|
||||
final int widthType = ResourceUtils.getEnumValue(keyAttr,
|
||||
R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
|
||||
switch (widthType) {
|
||||
case KEYWIDTH_FILL_RIGHT:
|
||||
// If keyWidth is fillRight, the actual key width will be determined to fill
|
||||
// out the area up to the right edge of the keyboard.
|
||||
final float keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
|
||||
return keyboardRightEdge - mCurrentX;
|
||||
default: // KEYWIDTH_NOT_ENUM
|
||||
return ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyWidth,
|
||||
mParams.mBaseWidth, getDefaultKeyPaddedWidth()) - mParams.mHorizontalGap;
|
||||
}
|
||||
}
|
||||
|
||||
public float getRowHeight() {
|
||||
return mRowHeight;
|
||||
}
|
||||
|
||||
public float getKeyY() {
|
||||
return mY;
|
||||
}
|
||||
|
||||
public float getKeyX() {
|
||||
return mCurrentX;
|
||||
}
|
||||
|
||||
public float getKeyWidth() {
|
||||
return mCurrentKeyWidth;
|
||||
}
|
||||
|
||||
public float getKeyHeight() {
|
||||
return mRowHeight - mKeyTopPadding - mKeyBottomPadding;
|
||||
}
|
||||
|
||||
public float getKeyTopPadding() {
|
||||
return mKeyTopPadding;
|
||||
}
|
||||
|
||||
public float getKeyBottomPadding() {
|
||||
return mKeyBottomPadding;
|
||||
}
|
||||
|
||||
public float getKeyLeftPadding() {
|
||||
return mCurrentKeyLeftPadding;
|
||||
}
|
||||
|
||||
public float getKeyRightPadding() {
|
||||
return mCurrentKeyRightPadding;
|
||||
}
|
||||
}
|
@ -0,0 +1,678 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.event.Event;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.CapsModeUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.RecapitalizeStatus;
|
||||
|
||||
/**
|
||||
* Keyboard state machine.
|
||||
* <p>
|
||||
* This class contains all keyboard state transition logic.
|
||||
* <p>
|
||||
* The input events are {@link #onLoadKeyboard(int, int)}, {@link #onSaveKeyboardState()},
|
||||
* {@link #onPressKey(int, boolean, int, int)}, {@link #onReleaseKey(int, boolean, int, int)},
|
||||
* {@link #onEvent(Event, int, int)}, {@link #onFinishSlidingInput(int, int)},
|
||||
* {@link #onUpdateShiftState(int, int)}, {@link #onResetKeyboardStateToAlphabet(int, int)}.
|
||||
* <p>
|
||||
* The actions are {@link SwitchActions}'s methods.
|
||||
*/
|
||||
public final class KeyboardState {
|
||||
private static final String TAG = KeyboardState.class.getSimpleName();
|
||||
private static final boolean DEBUG_EVENT = false;
|
||||
private static final boolean DEBUG_INTERNAL_ACTION = false;
|
||||
|
||||
public interface SwitchActions {
|
||||
boolean DEBUG_ACTION = false;
|
||||
|
||||
void setAlphabetKeyboard();
|
||||
|
||||
void setAlphabetManualShiftedKeyboard();
|
||||
|
||||
void setAlphabetAutomaticShiftedKeyboard();
|
||||
|
||||
void setAlphabetShiftLockedKeyboard();
|
||||
|
||||
void setSymbolsKeyboard();
|
||||
|
||||
void setSymbolsShiftedKeyboard();
|
||||
|
||||
/**
|
||||
* Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}.
|
||||
*/
|
||||
void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode);
|
||||
|
||||
boolean DEBUG_TIMER_ACTION = false;
|
||||
|
||||
void startDoubleTapShiftKeyTimer();
|
||||
|
||||
boolean isInDoubleTapShiftKeyTimeout();
|
||||
|
||||
void cancelDoubleTapShiftKeyTimer();
|
||||
}
|
||||
|
||||
private final SwitchActions mSwitchActions;
|
||||
|
||||
private final ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
|
||||
private final ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
|
||||
|
||||
// TODO: Merge {@link #mSwitchState}, {@link #mIsAlphabetMode}, {@link #mAlphabetShiftState},
|
||||
// {@link #mIsSymbolShifted}, {@link #mPrevMainKeyboardWasShiftLocked}, and
|
||||
// {@link #mPrevSymbolsKeyboardWasShifted} into single state variable.
|
||||
private static final int SWITCH_STATE_ALPHA = 0;
|
||||
private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
|
||||
private static final int SWITCH_STATE_SYMBOL = 2;
|
||||
private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
|
||||
private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
|
||||
private int mSwitchState = SWITCH_STATE_ALPHA;
|
||||
|
||||
private boolean mIsAlphabetMode;
|
||||
private final AlphabetShiftState mAlphabetShiftState = new AlphabetShiftState();
|
||||
private boolean mIsSymbolShifted;
|
||||
private boolean mPrevMainKeyboardWasShiftLocked;
|
||||
private boolean mPrevSymbolsKeyboardWasShifted;
|
||||
private int mRecapitalizeMode;
|
||||
|
||||
// For handling double tap.
|
||||
private boolean mIsInAlphabetUnshiftedFromShifted;
|
||||
private boolean mIsInDoubleTapShiftKey;
|
||||
|
||||
private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
|
||||
|
||||
static final class SavedKeyboardState {
|
||||
public boolean mIsValid;
|
||||
public boolean mIsAlphabetMode;
|
||||
public boolean mIsAlphabetShiftLocked;
|
||||
public int mShiftMode;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!mIsValid) {
|
||||
return "INVALID";
|
||||
}
|
||||
if (mIsAlphabetMode) {
|
||||
return mIsAlphabetShiftLocked ? "ALPHABET_SHIFT_LOCKED"
|
||||
: "ALPHABET_" + shiftModeToString(mShiftMode);
|
||||
}
|
||||
return "SYMBOLS_" + shiftModeToString(mShiftMode);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyboardState(final SwitchActions switchActions) {
|
||||
mSwitchActions = switchActions;
|
||||
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
|
||||
}
|
||||
|
||||
public void onLoadKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onLoadKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
// Reset alphabet shift state.
|
||||
mAlphabetShiftState.setShiftLocked(false);
|
||||
mPrevMainKeyboardWasShiftLocked = false;
|
||||
mPrevSymbolsKeyboardWasShifted = false;
|
||||
mShiftKeyState.onRelease();
|
||||
mSymbolKeyState.onRelease();
|
||||
if (mSavedKeyboardState.mIsValid) {
|
||||
onRestoreKeyboardState(autoCapsFlags, recapitalizeMode);
|
||||
mSavedKeyboardState.mIsValid = false;
|
||||
} else {
|
||||
// Reset keyboard to alphabet mode.
|
||||
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for {@link SavedKeyboardState#mShiftMode} and {@link #setShifted(int)}.
|
||||
private static final int UNSHIFT = 0;
|
||||
private static final int MANUAL_SHIFT = 1;
|
||||
private static final int AUTOMATIC_SHIFT = 2;
|
||||
private static final int SHIFT_LOCK_SHIFTED = 3;
|
||||
|
||||
public void onSaveKeyboardState() {
|
||||
final SavedKeyboardState state = mSavedKeyboardState;
|
||||
state.mIsAlphabetMode = mIsAlphabetMode;
|
||||
if (mIsAlphabetMode) {
|
||||
state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
|
||||
state.mShiftMode = mAlphabetShiftState.isAutomaticShifted() ? AUTOMATIC_SHIFT
|
||||
: (mAlphabetShiftState.isShiftedOrShiftLocked() ? MANUAL_SHIFT : UNSHIFT);
|
||||
} else {
|
||||
state.mIsAlphabetShiftLocked = mPrevMainKeyboardWasShiftLocked;
|
||||
state.mShiftMode = mIsSymbolShifted ? MANUAL_SHIFT : UNSHIFT;
|
||||
}
|
||||
state.mIsValid = true;
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onSaveKeyboardState: saved=" + state + " " + this);
|
||||
}
|
||||
}
|
||||
|
||||
private void onRestoreKeyboardState(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
final SavedKeyboardState state = mSavedKeyboardState;
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onRestoreKeyboardState: saved=" + state
|
||||
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
mPrevMainKeyboardWasShiftLocked = state.mIsAlphabetShiftLocked;
|
||||
if (state.mIsAlphabetMode) {
|
||||
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
|
||||
setShiftLocked(state.mIsAlphabetShiftLocked);
|
||||
if (!state.mIsAlphabetShiftLocked) {
|
||||
setShifted(state.mShiftMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Symbol mode
|
||||
if (state.mShiftMode == MANUAL_SHIFT) {
|
||||
setSymbolsShiftedKeyboard();
|
||||
} else {
|
||||
setSymbolsKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
private void setShifted(final int shiftMode) {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
|
||||
}
|
||||
if (!mIsAlphabetMode) return;
|
||||
final int prevShiftMode;
|
||||
if (mAlphabetShiftState.isAutomaticShifted()) {
|
||||
prevShiftMode = AUTOMATIC_SHIFT;
|
||||
} else if (mAlphabetShiftState.isManualShifted()) {
|
||||
prevShiftMode = MANUAL_SHIFT;
|
||||
} else {
|
||||
prevShiftMode = UNSHIFT;
|
||||
}
|
||||
switch (shiftMode) {
|
||||
case AUTOMATIC_SHIFT:
|
||||
mAlphabetShiftState.setAutomaticShifted();
|
||||
if (shiftMode != prevShiftMode) {
|
||||
mSwitchActions.setAlphabetAutomaticShiftedKeyboard();
|
||||
}
|
||||
break;
|
||||
case MANUAL_SHIFT:
|
||||
mAlphabetShiftState.setShifted(true);
|
||||
if (shiftMode != prevShiftMode) {
|
||||
mSwitchActions.setAlphabetManualShiftedKeyboard();
|
||||
}
|
||||
break;
|
||||
case UNSHIFT:
|
||||
mAlphabetShiftState.setShifted(false);
|
||||
if (shiftMode != prevShiftMode) {
|
||||
mSwitchActions.setAlphabetKeyboard();
|
||||
}
|
||||
break;
|
||||
case SHIFT_LOCK_SHIFTED:
|
||||
mAlphabetShiftState.setShifted(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setShiftLocked(final boolean shiftLocked) {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked + " " + this);
|
||||
}
|
||||
if (!mIsAlphabetMode) return;
|
||||
if (shiftLocked && (!mAlphabetShiftState.isShiftLocked()
|
||||
|| mAlphabetShiftState.isShiftLockShifted())) {
|
||||
mSwitchActions.setAlphabetShiftLockedKeyboard();
|
||||
}
|
||||
if (!shiftLocked && mAlphabetShiftState.isShiftLocked()) {
|
||||
mSwitchActions.setAlphabetKeyboard();
|
||||
}
|
||||
mAlphabetShiftState.setShiftLocked(shiftLocked);
|
||||
}
|
||||
|
||||
private void toggleAlphabetAndSymbols(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "toggleAlphabetAndSymbols: "
|
||||
+ stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
if (mIsAlphabetMode) {
|
||||
mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
|
||||
if (mPrevSymbolsKeyboardWasShifted) {
|
||||
setSymbolsShiftedKeyboard();
|
||||
} else {
|
||||
setSymbolsKeyboard();
|
||||
}
|
||||
mPrevSymbolsKeyboardWasShifted = false;
|
||||
} else {
|
||||
mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
|
||||
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
|
||||
if (mPrevMainKeyboardWasShiftLocked) {
|
||||
setShiftLocked(true);
|
||||
}
|
||||
mPrevMainKeyboardWasShiftLocked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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().
|
||||
private void resetKeyboardStateToAlphabet(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "resetKeyboardStateToAlphabet: "
|
||||
+ stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
if (mIsAlphabetMode) return;
|
||||
|
||||
mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
|
||||
setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
|
||||
if (mPrevMainKeyboardWasShiftLocked) {
|
||||
setShiftLocked(true);
|
||||
}
|
||||
mPrevMainKeyboardWasShiftLocked = false;
|
||||
}
|
||||
|
||||
private void toggleShiftInSymbols() {
|
||||
if (mIsSymbolShifted) {
|
||||
setSymbolsKeyboard();
|
||||
} else {
|
||||
setSymbolsShiftedKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
private void setAlphabetKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "setAlphabetKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
|
||||
mSwitchActions.setAlphabetKeyboard();
|
||||
mIsAlphabetMode = true;
|
||||
mIsSymbolShifted = false;
|
||||
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
|
||||
mSwitchState = SWITCH_STATE_ALPHA;
|
||||
mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
|
||||
private void setSymbolsKeyboard() {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "setSymbolsKeyboard");
|
||||
}
|
||||
mSwitchActions.setSymbolsKeyboard();
|
||||
mIsAlphabetMode = false;
|
||||
mIsSymbolShifted = false;
|
||||
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
|
||||
// Reset alphabet shift state.
|
||||
mAlphabetShiftState.setShiftLocked(false);
|
||||
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
|
||||
}
|
||||
|
||||
private void setSymbolsShiftedKeyboard() {
|
||||
if (DEBUG_INTERNAL_ACTION) {
|
||||
Log.d(TAG, "setSymbolsShiftedKeyboard");
|
||||
}
|
||||
mSwitchActions.setSymbolsShiftedKeyboard();
|
||||
mIsAlphabetMode = false;
|
||||
mIsSymbolShifted = true;
|
||||
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
|
||||
// Reset alphabet shift state.
|
||||
mAlphabetShiftState.setShiftLocked(false);
|
||||
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
|
||||
}
|
||||
|
||||
public void onPressKey(final int code, final boolean isSinglePointer, final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code)
|
||||
+ " single=" + isSinglePointer
|
||||
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
if (code != Constants.CODE_SHIFT) {
|
||||
// Because the double tap shift key timer is to detect two consecutive shift key press,
|
||||
// it should be canceled when a non-shift key is pressed.
|
||||
mSwitchActions.cancelDoubleTapShiftKeyTimer();
|
||||
}
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
onPressShift();
|
||||
} else if (code == Constants.CODE_CAPSLOCK) {
|
||||
// Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
|
||||
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||
onPressSymbol(autoCapsFlags, recapitalizeMode);
|
||||
} else {
|
||||
mShiftKeyState.onOtherKeyPressed();
|
||||
mSymbolKeyState.onOtherKeyPressed();
|
||||
// It is required to reset the auto caps state when all of the following conditions
|
||||
// are met:
|
||||
// 1) two or more fingers are in action
|
||||
// 2) in alphabet layout
|
||||
// 3) not in all characters caps mode
|
||||
// As for #3, please note that it's required to check even when the auto caps mode is
|
||||
// off because, for example, we may be in the #1 state within the manual temporary
|
||||
// shifted mode.
|
||||
if (!isSinglePointer && mIsAlphabetMode
|
||||
&& autoCapsFlags != TextUtils.CAP_MODE_CHARACTERS) {
|
||||
final boolean needsToResetAutoCaps =
|
||||
(mAlphabetShiftState.isAutomaticShifted() && !mShiftKeyState.isChording())
|
||||
|| (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing());
|
||||
if (needsToResetAutoCaps) {
|
||||
mSwitchActions.setAlphabetKeyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onReleaseKey(final int code, final boolean withSliding, final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onReleaseKey: code=" + Constants.printableCode(code)
|
||||
+ " sliding=" + withSliding
|
||||
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
|
||||
} else if (code == Constants.CODE_CAPSLOCK) {
|
||||
setShiftLocked(!mAlphabetShiftState.isShiftLocked());
|
||||
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||
onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPressSymbol(final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
|
||||
mSymbolKeyState.onPress();
|
||||
mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
|
||||
}
|
||||
|
||||
private void onReleaseSymbol(final boolean withSliding, final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
if (mSymbolKeyState.isChording()) {
|
||||
// Switch back to the previous keyboard mode if the user chords the mode change key and
|
||||
// another key, then releases the mode change key.
|
||||
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
|
||||
} else if (!withSliding) {
|
||||
// If the mode change key is being released without sliding, we should forget the
|
||||
// previous symbols keyboard shift state and simply switch back to symbols layout
|
||||
// (never symbols shifted) next time the mode gets changed to symbols layout.
|
||||
mPrevSymbolsKeyboardWasShifted = false;
|
||||
}
|
||||
mSymbolKeyState.onRelease();
|
||||
}
|
||||
|
||||
public void onUpdateShiftState(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onUpdateShiftState: " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
mRecapitalizeMode = recapitalizeMode;
|
||||
updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
|
||||
// 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 onResetKeyboardStateToAlphabet(final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onResetKeyboardStateToAlphabet: "
|
||||
+ stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
resetKeyboardStateToAlphabet(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
|
||||
private void updateShiftStateForRecapitalize(final int recapitalizeMode) {
|
||||
switch (recapitalizeMode) {
|
||||
case RecapitalizeStatus.CAPS_MODE_ALL_UPPER:
|
||||
setShifted(SHIFT_LOCK_SHIFTED);
|
||||
break;
|
||||
case RecapitalizeStatus.CAPS_MODE_FIRST_WORD_UPPER:
|
||||
setShifted(AUTOMATIC_SHIFT);
|
||||
break;
|
||||
case RecapitalizeStatus.CAPS_MODE_ALL_LOWER:
|
||||
case RecapitalizeStatus.CAPS_MODE_ORIGINAL_MIXED_CASE:
|
||||
default:
|
||||
setShifted(UNSHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAlphabetShiftState(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (!mIsAlphabetMode) return;
|
||||
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != recapitalizeMode) {
|
||||
// We are recapitalizing. Match the keyboard to the current recapitalize state.
|
||||
updateShiftStateForRecapitalize(recapitalizeMode);
|
||||
return;
|
||||
}
|
||||
if (!mShiftKeyState.isReleasing()) {
|
||||
// Ignore update shift state event while the shift key is being pressed (including
|
||||
// chording).
|
||||
return;
|
||||
}
|
||||
if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
|
||||
if (mShiftKeyState.isReleasing() && autoCapsFlags != Constants.TextUtils.CAP_MODE_OFF) {
|
||||
// Only when shift key is releasing, automatic temporary upper case will be set.
|
||||
setShifted(AUTOMATIC_SHIFT);
|
||||
} else {
|
||||
setShifted(mShiftKeyState.isChording() ? MANUAL_SHIFT : UNSHIFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPressShift() {
|
||||
// If we are recapitalizing, we don't do any of the normal processing, including
|
||||
// importantly the double tap timer.
|
||||
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
|
||||
return;
|
||||
}
|
||||
if (mIsAlphabetMode) {
|
||||
mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapShiftKeyTimeout();
|
||||
if (!mIsInDoubleTapShiftKey) {
|
||||
// This is first tap.
|
||||
mSwitchActions.startDoubleTapShiftKeyTimer();
|
||||
}
|
||||
if (mIsInDoubleTapShiftKey) {
|
||||
if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
|
||||
// Shift key has been double tapped while in manual shifted or automatic
|
||||
// shifted state.
|
||||
setShiftLocked(true);
|
||||
} else {
|
||||
// Shift key has been double tapped while in normal state. This is the second
|
||||
// tap to disable shift locked state, so just ignore this.
|
||||
}
|
||||
} else {
|
||||
if (mAlphabetShiftState.isShiftLocked()) {
|
||||
// Shift key is pressed while shift locked state, we will treat this state as
|
||||
// shift lock shifted state and mark as if shift key pressed while normal
|
||||
// state.
|
||||
setShifted(SHIFT_LOCK_SHIFTED);
|
||||
mShiftKeyState.onPress();
|
||||
} else if (mAlphabetShiftState.isAutomaticShifted()) {
|
||||
// Shift key pressed while automatic shifted isn't considered a manual shift
|
||||
// since it doesn't change the keyboard into a shifted state.
|
||||
mShiftKeyState.onPress();
|
||||
} else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
|
||||
// In manual shifted state, we just record shift key has been pressing while
|
||||
// shifted state.
|
||||
mShiftKeyState.onPressOnShifted();
|
||||
} else {
|
||||
// In base layout, chording or manual shifted mode is started.
|
||||
setShifted(MANUAL_SHIFT);
|
||||
mShiftKeyState.onPress();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In symbol mode, just toggle symbol and symbol more keyboard.
|
||||
toggleShiftInSymbols();
|
||||
mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
|
||||
mShiftKeyState.onPress();
|
||||
}
|
||||
}
|
||||
|
||||
private void onReleaseShift(final boolean withSliding, final int autoCapsFlags,
|
||||
final int recapitalizeMode) {
|
||||
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
|
||||
// We are recapitalizing. We should match the keyboard state to the recapitalize
|
||||
// state in priority.
|
||||
updateShiftStateForRecapitalize(mRecapitalizeMode);
|
||||
} else if (mIsAlphabetMode) {
|
||||
final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
|
||||
mIsInAlphabetUnshiftedFromShifted = false;
|
||||
if (mIsInDoubleTapShiftKey) {
|
||||
// Double tap shift key has been handled in {@link #onPressShift}, so that just
|
||||
// ignore this release shift key here.
|
||||
mIsInDoubleTapShiftKey = false;
|
||||
} else if (mShiftKeyState.isChording()) {
|
||||
if (mAlphabetShiftState.isShiftLockShifted()) {
|
||||
// After chording input while shift locked state.
|
||||
setShiftLocked(true);
|
||||
} else {
|
||||
// After chording input while normal state.
|
||||
setShifted(UNSHIFT);
|
||||
}
|
||||
// After chording input, automatic shift state may have been changed depending on
|
||||
// what characters were input.
|
||||
mShiftKeyState.onRelease();
|
||||
mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
|
||||
return;
|
||||
} else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
|
||||
&& (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
|
||||
&& !withSliding) {
|
||||
// Shift has been long pressed, ignore this release.
|
||||
} else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
|
||||
// Shift has been pressed without chording while shift locked state.
|
||||
setShiftLocked(false);
|
||||
} else if (mAlphabetShiftState.isShiftedOrShiftLocked()
|
||||
&& mShiftKeyState.isPressingOnShifted() && !withSliding) {
|
||||
// Shift has been pressed without chording while shifted state.
|
||||
setShifted(UNSHIFT);
|
||||
mIsInAlphabetUnshiftedFromShifted = true;
|
||||
} else if (mAlphabetShiftState.isAutomaticShifted() && mShiftKeyState.isPressing()
|
||||
&& !withSliding) {
|
||||
// Shift has been pressed without chording while automatic shifted
|
||||
setShifted(UNSHIFT);
|
||||
mIsInAlphabetUnshiftedFromShifted = true;
|
||||
}
|
||||
} else {
|
||||
// In symbol mode, switch back to the previous keyboard mode if the user chords the
|
||||
// shift key and another key, then releases the shift key.
|
||||
if (mShiftKeyState.isChording()) {
|
||||
toggleShiftInSymbols();
|
||||
}
|
||||
}
|
||||
mShiftKeyState.onRelease();
|
||||
}
|
||||
|
||||
public void onFinishSlidingInput(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onFinishSlidingInput: " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
// Switch back to the previous keyboard mode if the user cancels sliding input.
|
||||
switch (mSwitchState) {
|
||||
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
|
||||
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
|
||||
break;
|
||||
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
|
||||
toggleShiftInSymbols();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSpaceOrEnter(final int c) {
|
||||
return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
|
||||
}
|
||||
|
||||
public void onEvent(final Event event, final int autoCapsFlags, final int recapitalizeMode) {
|
||||
final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint;
|
||||
if (DEBUG_EVENT) {
|
||||
Log.d(TAG, "onEvent: code=" + Constants.printableCode(code)
|
||||
+ " " + stateToString(autoCapsFlags, recapitalizeMode));
|
||||
}
|
||||
|
||||
switch (mSwitchState) {
|
||||
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
|
||||
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||
// Detected only the mode change key has been pressed, and then released.
|
||||
if (mIsAlphabetMode) {
|
||||
mSwitchState = SWITCH_STATE_ALPHA;
|
||||
} else {
|
||||
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
// Detected only the shift key has been pressed on symbol layout, and then
|
||||
// released.
|
||||
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
|
||||
}
|
||||
break;
|
||||
case SWITCH_STATE_SYMBOL_BEGIN:
|
||||
if (!isSpaceOrEnter(code) && (Constants.isLetterCode(code)
|
||||
|| code == Constants.CODE_OUTPUT_TEXT)) {
|
||||
mSwitchState = SWITCH_STATE_SYMBOL;
|
||||
}
|
||||
break;
|
||||
case SWITCH_STATE_SYMBOL:
|
||||
// Switch back to alpha keyboard mode if user types one or more non-space/enter
|
||||
// characters followed by a space/enter.
|
||||
if (isSpaceOrEnter(code)) {
|
||||
toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
|
||||
mPrevSymbolsKeyboardWasShifted = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the code is a letter, update keyboard shift state.
|
||||
if (Constants.isLetterCode(code)) {
|
||||
updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
|
||||
}
|
||||
}
|
||||
|
||||
static String shiftModeToString(final int shiftMode) {
|
||||
switch (shiftMode) {
|
||||
case UNSHIFT:
|
||||
return "UNSHIFT";
|
||||
case MANUAL_SHIFT:
|
||||
return "MANUAL";
|
||||
case AUTOMATIC_SHIFT:
|
||||
return "AUTOMATIC";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String switchStateToString(final int switchState) {
|
||||
switch (switchState) {
|
||||
case SWITCH_STATE_ALPHA:
|
||||
return "ALPHA";
|
||||
case SWITCH_STATE_SYMBOL_BEGIN:
|
||||
return "SYMBOL-BEGIN";
|
||||
case SWITCH_STATE_SYMBOL:
|
||||
return "SYMBOL";
|
||||
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
|
||||
return "MOMENTARY-ALPHA-SYMBOL";
|
||||
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
|
||||
return "MOMENTARY-SYMBOL-MORE";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
|
||||
: (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
|
||||
+ " shift=" + mShiftKeyState
|
||||
+ " symbol=" + mSymbolKeyState
|
||||
+ " switch=" + switchStateToString(mSwitchState) + "]";
|
||||
}
|
||||
|
||||
private String stateToString(final int autoCapsFlags, final int recapitalizeMode) {
|
||||
return this + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
|
||||
+ " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode);
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
// TODO: Make this an immutable class.
|
||||
public final class KeyboardTextsSet {
|
||||
public static final String PREFIX_TEXT = "!text/";
|
||||
private static final String PREFIX_RESOURCE = "!string/";
|
||||
|
||||
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
|
||||
private static final int MAX_REFERENCE_INDIRECTION = 10;
|
||||
|
||||
private Resources mResources;
|
||||
private String mResourcePackageName;
|
||||
private String[] mTextsTable;
|
||||
|
||||
public void setLocale(final Locale locale, final Context context) {
|
||||
final Resources res = context.getResources();
|
||||
// Null means the current system locale.
|
||||
final String resourcePackageName = res.getResourcePackageName(
|
||||
context.getApplicationInfo().labelRes);
|
||||
setLocale(locale, res, resourcePackageName);
|
||||
}
|
||||
|
||||
public void setLocale(final Locale locale, final Resources res,
|
||||
final String resourcePackageName) {
|
||||
mResources = res;
|
||||
// Null means the current system locale.
|
||||
mResourcePackageName = resourcePackageName;
|
||||
mTextsTable = KeyboardTextsTable.getTextsTable(locale);
|
||||
}
|
||||
|
||||
public String getText(final String name) {
|
||||
return KeyboardTextsTable.getText(name, mTextsTable);
|
||||
}
|
||||
|
||||
private static int searchTextNameEnd(final String text, final int start) {
|
||||
final int size = text.length();
|
||||
for (int pos = start; pos < size; pos++) {
|
||||
final char c = text.charAt(pos);
|
||||
// Label name should be consisted of [a-zA-Z_0-9].
|
||||
if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
|
||||
continue;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// TODO: Resolve text reference when creating {@link KeyboardTextsTable} class.
|
||||
public String resolveTextReference(final String rawText) {
|
||||
if (TextUtils.isEmpty(rawText)) {
|
||||
return null;
|
||||
}
|
||||
int level = 0;
|
||||
String text = rawText;
|
||||
StringBuilder sb;
|
||||
do {
|
||||
level++;
|
||||
if (level >= MAX_REFERENCE_INDIRECTION) {
|
||||
throw new RuntimeException("Too many " + PREFIX_TEXT + " or " + PREFIX_RESOURCE +
|
||||
" reference indirection: " + text);
|
||||
}
|
||||
|
||||
final int prefixLength = PREFIX_TEXT.length();
|
||||
final int size = text.length();
|
||||
if (size < prefixLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
sb = null;
|
||||
for (int pos = 0; pos < size; pos++) {
|
||||
final char c = text.charAt(pos);
|
||||
if (text.startsWith(PREFIX_TEXT, pos)) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(text.substring(0, pos));
|
||||
}
|
||||
pos = expandReference(text, pos, PREFIX_TEXT, sb);
|
||||
} else if (text.startsWith(PREFIX_RESOURCE, pos)) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(text.substring(0, pos));
|
||||
}
|
||||
pos = expandReference(text, pos, PREFIX_RESOURCE, sb);
|
||||
} else if (c == BACKSLASH) {
|
||||
if (sb != null) {
|
||||
// Append both escape character and escaped character.
|
||||
sb.append(text.substring(pos, Math.min(pos + 2, size)));
|
||||
}
|
||||
pos++;
|
||||
} else if (sb != null) {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sb != null) {
|
||||
text = sb.toString();
|
||||
}
|
||||
} while (sb != null);
|
||||
return TextUtils.isEmpty(text) ? null : text;
|
||||
}
|
||||
|
||||
private int expandReference(final String text, final int pos, final String prefix,
|
||||
final StringBuilder sb) {
|
||||
final int prefixLength = prefix.length();
|
||||
final int end = searchTextNameEnd(text, pos + prefixLength);
|
||||
final String name = text.substring(pos + prefixLength, end);
|
||||
if (prefix.equals(PREFIX_TEXT)) {
|
||||
sb.append(getText(name));
|
||||
} else { // PREFIX_RESOURCE
|
||||
final int resId = mResources.getIdentifier(name, "string", mResourcePackageName);
|
||||
sb.append(mResources.getString(resId));
|
||||
}
|
||||
return end - 1;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/* package */ class ModifierKeyState {
|
||||
protected static final String TAG = ModifierKeyState.class.getSimpleName();
|
||||
protected static final boolean DEBUG = false;
|
||||
|
||||
protected static final int RELEASING = 0;
|
||||
protected static final int PRESSING = 1;
|
||||
protected static final int CHORDING = 2;
|
||||
|
||||
protected final String mName;
|
||||
protected int mState = RELEASING;
|
||||
|
||||
public ModifierKeyState(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public void onPress() {
|
||||
mState = PRESSING;
|
||||
}
|
||||
|
||||
public void onRelease() {
|
||||
mState = RELEASING;
|
||||
}
|
||||
|
||||
public void onOtherKeyPressed() {
|
||||
final int oldState = mState;
|
||||
if (oldState == PRESSING)
|
||||
mState = CHORDING;
|
||||
if (DEBUG)
|
||||
Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
|
||||
}
|
||||
|
||||
public boolean isPressing() {
|
||||
return mState == PRESSING;
|
||||
}
|
||||
|
||||
public boolean isReleasing() {
|
||||
return mState == RELEASING;
|
||||
}
|
||||
|
||||
public boolean isChording() {
|
||||
return mState == CHORDING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(mState);
|
||||
}
|
||||
|
||||
protected String toString(int state) {
|
||||
switch (state) {
|
||||
case RELEASING:
|
||||
return "RELEASING";
|
||||
case PRESSING:
|
||||
return "PRESSING";
|
||||
case CHORDING:
|
||||
return "CHORDING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,341 @@
|
||||
/*
|
||||
* 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.keyboard.internal;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CollectionUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The more key specification object. The more keys are an array of {@link MoreKeySpec}.
|
||||
* <p>
|
||||
* The more keys specification is comma separated "key specification" each of which represents one
|
||||
* "more key".
|
||||
* The key specification might have label or string resource reference in it. These references are
|
||||
* expanded before parsing comma.
|
||||
* Special character, comma ',' backslash '\' can be escaped by '\' character.
|
||||
* Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
|
||||
* as well.
|
||||
*/
|
||||
// TODO: Should extend the key specification object.
|
||||
public final class MoreKeySpec {
|
||||
public final int mCode;
|
||||
public final String mLabel;
|
||||
public final String mOutputText;
|
||||
public final int mIconId;
|
||||
|
||||
public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase,
|
||||
final Locale locale) {
|
||||
if (moreKeySpec.isEmpty()) {
|
||||
throw new KeySpecParser.KeySpecParserError("Empty more key spec");
|
||||
}
|
||||
final String label = KeySpecParser.getLabel(moreKeySpec);
|
||||
mLabel = needsToUpperCase ? StringUtils.toTitleCaseOfKeyLabel(label, locale) : label;
|
||||
final int codeInSpec = KeySpecParser.getCode(moreKeySpec);
|
||||
final int code = needsToUpperCase ? StringUtils.toTitleCaseOfKeyCode(codeInSpec, locale)
|
||||
: codeInSpec;
|
||||
if (code == Constants.CODE_UNSPECIFIED) {
|
||||
// Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
|
||||
// upper case representation ("SS").
|
||||
mCode = Constants.CODE_OUTPUT_TEXT;
|
||||
mOutputText = mLabel;
|
||||
} else {
|
||||
mCode = code;
|
||||
final String outputText = KeySpecParser.getOutputText(moreKeySpec);
|
||||
mOutputText = needsToUpperCase
|
||||
? StringUtils.toTitleCaseOfKeyLabel(outputText, locale) : outputText;
|
||||
}
|
||||
mIconId = KeySpecParser.getIconId(moreKeySpec);
|
||||
}
|
||||
|
||||
public Key buildKey(final float x, final float y, final float width, final float height,
|
||||
final float leftPadding, final float rightPadding, final float topPadding,
|
||||
final float bottomPadding, final int labelFlags) {
|
||||
return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
|
||||
Key.BACKGROUND_TYPE_NORMAL, x, y, width, height, leftPadding, rightPadding,
|
||||
topPadding, bottomPadding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = 31 + mCode;
|
||||
hashCode = hashCode * 31 + mIconId;
|
||||
final String label = mLabel;
|
||||
hashCode = hashCode * 31 + (label == null ? 0 : label.hashCode());
|
||||
final String outputText = mOutputText;
|
||||
hashCode = hashCode * 31 + (outputText == null ? 0 : outputText.hashCode());
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o instanceof MoreKeySpec) {
|
||||
final MoreKeySpec other = (MoreKeySpec) o;
|
||||
return mCode == other.mCode
|
||||
&& mIconId == other.mIconId
|
||||
&& TextUtils.equals(mLabel, other.mLabel)
|
||||
&& TextUtils.equals(mOutputText, other.mOutputText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
|
||||
: KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
|
||||
final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
|
||||
: Constants.printableCode(mCode));
|
||||
if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
|
||||
return output;
|
||||
}
|
||||
return label + "|" + output;
|
||||
}
|
||||
|
||||
public static class LettersOnBaseLayout {
|
||||
private final SparseIntArray mCodes = new SparseIntArray();
|
||||
private final HashSet<String> mTexts = new HashSet<>();
|
||||
|
||||
public void addLetter(final Key key) {
|
||||
final int code = key.getCode();
|
||||
if (Character.isAlphabetic(code)) {
|
||||
mCodes.put(code, 0);
|
||||
} else if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
mTexts.add(key.getOutputText());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(final MoreKeySpec moreKey) {
|
||||
final int code = moreKey.mCode;
|
||||
if (Character.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
|
||||
return true;
|
||||
} else return code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText);
|
||||
}
|
||||
}
|
||||
|
||||
public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
|
||||
final LettersOnBaseLayout lettersOnBaseLayout) {
|
||||
if (moreKeys == null) {
|
||||
return null;
|
||||
}
|
||||
final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
|
||||
for (final MoreKeySpec moreKey : moreKeys) {
|
||||
if (!lettersOnBaseLayout.contains(moreKey)) {
|
||||
filteredMoreKeys.add(moreKey);
|
||||
}
|
||||
}
|
||||
final int size = filteredMoreKeys.size();
|
||||
if (size == moreKeys.length) {
|
||||
return moreKeys;
|
||||
}
|
||||
if (size == 0) {
|
||||
return null;
|
||||
}
|
||||
return filteredMoreKeys.toArray(new MoreKeySpec[size]);
|
||||
}
|
||||
|
||||
// Constants for parsing.
|
||||
private static final char COMMA = Constants.CODE_COMMA;
|
||||
private static final char BACKSLASH = Constants.CODE_BACKSLASH;
|
||||
private static final String ADDITIONAL_MORE_KEY_MARKER =
|
||||
StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
|
||||
|
||||
/**
|
||||
* Split the text containing multiple key specifications separated by commas into an array of
|
||||
* key specifications.
|
||||
* A key specification can contain a character escaped by the backslash character, including a
|
||||
* comma character.
|
||||
* Note that an empty key specification will be eliminated from the result array.
|
||||
*
|
||||
* @param text the text containing multiple key specifications.
|
||||
* @return an array of key specification text. Null if the specified <code>text</code> is empty
|
||||
* or has no key specifications.
|
||||
*/
|
||||
public static String[] splitKeySpecs(final String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
final int size = text.length();
|
||||
// Optimization for one-letter key specification.
|
||||
if (size == 1) {
|
||||
return text.charAt(0) == COMMA ? null : new String[]{text};
|
||||
}
|
||||
|
||||
ArrayList<String> list = null;
|
||||
int start = 0;
|
||||
// The characters in question in this loop are COMMA and BACKSLASH. These characters never
|
||||
// match any high or low surrogate character. So it is OK to iterate through with char
|
||||
// index.
|
||||
for (int pos = 0; pos < size; pos++) {
|
||||
final char c = text.charAt(pos);
|
||||
if (c == COMMA) {
|
||||
// Skip empty entry.
|
||||
if (pos - start > 0) {
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
list.add(text.substring(start, pos));
|
||||
}
|
||||
// Skip comma
|
||||
start = pos + 1;
|
||||
} else if (c == BACKSLASH) {
|
||||
// Skip escape character and escaped character.
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
final String remain = (size - start > 0) ? text.substring(start) : null;
|
||||
if (list == null) {
|
||||
return remain != null ? new String[]{remain} : null;
|
||||
}
|
||||
if (remain != null) {
|
||||
list.add(remain);
|
||||
}
|
||||
return list.toArray(new String[list.size()]);
|
||||
}
|
||||
|
||||
private static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
|
||||
private static String[] filterOutEmptyString(final String[] array) {
|
||||
if (array == null) {
|
||||
return EMPTY_STRING_ARRAY;
|
||||
}
|
||||
ArrayList<String> out = null;
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
final String entry = array[i];
|
||||
if (TextUtils.isEmpty(entry)) {
|
||||
if (out == null) {
|
||||
out = CollectionUtils.arrayAsList(array, 0, i);
|
||||
}
|
||||
} else if (out != null) {
|
||||
out.add(entry);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
return array;
|
||||
}
|
||||
return out.toArray(new String[out.size()]);
|
||||
}
|
||||
|
||||
public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
|
||||
final String[] additionalMoreKeySpecs) {
|
||||
final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
|
||||
final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
|
||||
final int moreKeysCount = moreKeys.length;
|
||||
final int additionalCount = additionalMoreKeys.length;
|
||||
ArrayList<String> out = null;
|
||||
int additionalIndex = 0;
|
||||
for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
|
||||
final String moreKeySpec = moreKeys[moreKeyIndex];
|
||||
if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
|
||||
if (additionalIndex < additionalCount) {
|
||||
// Replace '%' marker with additional more key specification.
|
||||
final String additionalMoreKey = additionalMoreKeys[additionalIndex];
|
||||
if (out != null) {
|
||||
out.add(additionalMoreKey);
|
||||
} else {
|
||||
moreKeys[moreKeyIndex] = additionalMoreKey;
|
||||
}
|
||||
additionalIndex++;
|
||||
} else {
|
||||
// Filter out excessive '%' marker.
|
||||
if (out == null) {
|
||||
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (out != null) {
|
||||
out.add(moreKeySpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (additionalCount > 0 && additionalIndex == 0) {
|
||||
// No '%' marker is found in more keys.
|
||||
// Insert all additional more keys to the head of more keys.
|
||||
out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
|
||||
for (int i = 0; i < moreKeysCount; i++) {
|
||||
out.add(moreKeys[i]);
|
||||
}
|
||||
} else if (additionalIndex < additionalCount) {
|
||||
// The number of '%' markers are less than additional more keys.
|
||||
// Append remained additional more keys to the tail of more keys.
|
||||
out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
|
||||
for (int i = additionalIndex; i < additionalCount; i++) {
|
||||
out.add(additionalMoreKeys[additionalIndex]);
|
||||
}
|
||||
}
|
||||
if (out == null && moreKeysCount > 0) {
|
||||
return moreKeys;
|
||||
} else if (out != null && out.size() > 0) {
|
||||
return out.toArray(new String[out.size()]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getIntValue(final String[] moreKeys, final String key,
|
||||
final int defaultValue) {
|
||||
if (moreKeys == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
final int keyLen = key.length();
|
||||
boolean foundValue = false;
|
||||
int value = defaultValue;
|
||||
for (int i = 0; i < moreKeys.length; i++) {
|
||||
final String moreKeySpec = moreKeys[i];
|
||||
if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
|
||||
continue;
|
||||
}
|
||||
moreKeys[i] = null;
|
||||
try {
|
||||
if (!foundValue) {
|
||||
value = Integer.parseInt(moreKeySpec.substring(keyLen));
|
||||
foundValue = true;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new RuntimeException(
|
||||
"integer should follow after " + key + ": " + moreKeySpec);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static boolean getBooleanValue(final String[] moreKeys, final String key) {
|
||||
if (moreKeys == null) {
|
||||
return false;
|
||||
}
|
||||
boolean value = false;
|
||||
for (int i = 0; i < moreKeys.length; i++) {
|
||||
final String moreKeySpec = moreKeys[i];
|
||||
if (moreKeySpec == null || !moreKeySpec.equals(key)) {
|
||||
continue;
|
||||
}
|
||||
moreKeys[i] = null;
|
||||
value = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.internal;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyDetector;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.PointerTracker;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.CoordinateUtils;
|
||||
|
||||
public final class NonDistinctMultitouchHelper {
|
||||
private static final String TAG = NonDistinctMultitouchHelper.class.getSimpleName();
|
||||
|
||||
private static final int MAIN_POINTER_TRACKER_ID = 0;
|
||||
private int mOldPointerCount = 1;
|
||||
private Key mOldKey;
|
||||
private final int[] mLastCoords = CoordinateUtils.newInstance();
|
||||
|
||||
public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) {
|
||||
final int pointerCount = me.getPointerCount();
|
||||
final int oldPointerCount = mOldPointerCount;
|
||||
mOldPointerCount = pointerCount;
|
||||
// Ignore continuous multi-touch events because we can't trust the coordinates
|
||||
// in multi-touch events.
|
||||
if (pointerCount > 1 && oldPointerCount > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use only main pointer tracker.
|
||||
final PointerTracker mainTracker = PointerTracker.getPointerTracker(
|
||||
MAIN_POINTER_TRACKER_ID);
|
||||
final int action = me.getActionMasked();
|
||||
final int index = me.getActionIndex();
|
||||
final long eventTime = me.getEventTime();
|
||||
final long downTime = me.getDownTime();
|
||||
|
||||
// In single-touch.
|
||||
if (oldPointerCount == 1 && pointerCount == 1) {
|
||||
if (me.getPointerId(index) == mainTracker.mPointerId) {
|
||||
mainTracker.processMotionEvent(me, keyDetector);
|
||||
return;
|
||||
}
|
||||
// Inject a copied event.
|
||||
injectMotionEvent(action, me.getX(index), me.getY(index), downTime, eventTime,
|
||||
mainTracker, keyDetector);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single-touch to multi-touch transition.
|
||||
if (oldPointerCount == 1 && pointerCount == 2) {
|
||||
// Send an up event for the last pointer, be cause we can't trust the coordinates of
|
||||
// this multi-touch event.
|
||||
mainTracker.getLastCoordinates(mLastCoords);
|
||||
final int x = CoordinateUtils.x(mLastCoords);
|
||||
final int y = CoordinateUtils.y(mLastCoords);
|
||||
mOldKey = mainTracker.getKeyOn(x, y);
|
||||
// Inject an artifact up event for the old key.
|
||||
injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime,
|
||||
mainTracker, keyDetector);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-touch to single-touch transition.
|
||||
if (oldPointerCount == 2 && pointerCount == 1) {
|
||||
// Send a down event for the latest pointer if the key is different from the previous
|
||||
// key.
|
||||
final int x = (int) me.getX(index);
|
||||
final int y = (int) me.getY(index);
|
||||
final Key newKey = mainTracker.getKeyOn(x, y);
|
||||
if (mOldKey != newKey) {
|
||||
// Inject an artifact down event for the new key.
|
||||
// An artifact up event for the new key will usually be injected as a single-touch.
|
||||
injectMotionEvent(MotionEvent.ACTION_DOWN, x, y, downTime, eventTime,
|
||||
mainTracker, keyDetector);
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
// Inject an artifact up event for the new key also.
|
||||
injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime,
|
||||
mainTracker, keyDetector);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(TAG, "Unknown touch panel behavior: pointer count is "
|
||||
+ pointerCount + " (previously " + oldPointerCount + ")");
|
||||
}
|
||||
|
||||
private static void injectMotionEvent(final int action, final float x, final float y,
|
||||
final long downTime, final long eventTime, final PointerTracker tracker,
|
||||
final KeyDetector keyDetector) {
|
||||
final MotionEvent me = MotionEvent.obtain(
|
||||
downTime, eventTime, action, x, y, 0 /* metaState */);
|
||||
try {
|
||||
tracker.processMotionEvent(me, keyDetector);
|
||||
} finally {
|
||||
me.recycle();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class PointerTrackerQueue {
|
||||
private static final String TAG = PointerTrackerQueue.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
public interface Element {
|
||||
boolean isModifier();
|
||||
|
||||
boolean isInDraggingFinger();
|
||||
|
||||
void onPhantomUpEvent(long eventTime);
|
||||
|
||||
void cancelTrackingForAction();
|
||||
}
|
||||
|
||||
private static final int INITIAL_CAPACITY = 10;
|
||||
// Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by
|
||||
// {@link #mExpandableArrayOfActivePointers}
|
||||
private final ArrayList<Element> mExpandableArrayOfActivePointers =
|
||||
new ArrayList<>(INITIAL_CAPACITY);
|
||||
private int mArraySize = 0;
|
||||
|
||||
public int size() {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
return mArraySize;
|
||||
}
|
||||
}
|
||||
|
||||
public void add(final Element pointer) {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "add: " + pointer + " " + this);
|
||||
}
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
if (arraySize < expandableArray.size()) {
|
||||
expandableArray.set(arraySize, pointer);
|
||||
} else {
|
||||
expandableArray.add(pointer);
|
||||
}
|
||||
mArraySize = arraySize + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(final Element pointer) {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "remove: " + pointer + " " + this);
|
||||
}
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
int newIndex = 0;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element == pointer) {
|
||||
if (newIndex != index) {
|
||||
Log.w(TAG, "Found duplicated element in remove: " + pointer);
|
||||
}
|
||||
continue; // Remove this element from the expandableArray.
|
||||
}
|
||||
if (newIndex != index) {
|
||||
// Shift this element toward the beginning of the expandableArray.
|
||||
expandableArray.set(newIndex, element);
|
||||
}
|
||||
newIndex++;
|
||||
}
|
||||
mArraySize = newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "releaseAllPointerOlderThan: " + pointer + " " + this);
|
||||
}
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
int newIndex, index;
|
||||
for (newIndex = index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element == pointer) {
|
||||
break; // Stop releasing elements.
|
||||
}
|
||||
if (!element.isModifier()) {
|
||||
element.onPhantomUpEvent(eventTime);
|
||||
continue; // Remove this element from the expandableArray.
|
||||
}
|
||||
if (newIndex != index) {
|
||||
// Shift this element toward the beginning of the expandableArray.
|
||||
expandableArray.set(newIndex, element);
|
||||
}
|
||||
newIndex++;
|
||||
}
|
||||
// Shift rest of the expandableArray.
|
||||
int count = 0;
|
||||
for (; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element == pointer) {
|
||||
count++;
|
||||
if (count > 1) {
|
||||
Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
|
||||
+ pointer);
|
||||
}
|
||||
}
|
||||
if (newIndex != index) {
|
||||
// Shift this element toward the beginning of the expandableArray.
|
||||
expandableArray.set(newIndex, expandableArray.get(index));
|
||||
}
|
||||
newIndex++;
|
||||
}
|
||||
mArraySize = newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseAllPointers(final long eventTime) {
|
||||
releaseAllPointersExcept(null, eventTime);
|
||||
}
|
||||
|
||||
public void releaseAllPointersExcept(final Element pointer, final long eventTime) {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
if (DEBUG) {
|
||||
if (pointer == null) {
|
||||
Log.d(TAG, "releaseAllPointers: " + this);
|
||||
} else {
|
||||
Log.d(TAG, "releaseAllPointerExcept: " + pointer + " " + this);
|
||||
}
|
||||
}
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
int newIndex = 0, count = 0;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element == pointer) {
|
||||
count++;
|
||||
if (count > 1) {
|
||||
Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: "
|
||||
+ pointer);
|
||||
}
|
||||
} else {
|
||||
element.onPhantomUpEvent(eventTime);
|
||||
continue; // Remove this element from the expandableArray.
|
||||
}
|
||||
if (newIndex != index) {
|
||||
// Shift this element toward the beginning of the expandableArray.
|
||||
expandableArray.set(newIndex, element);
|
||||
}
|
||||
newIndex++;
|
||||
}
|
||||
mArraySize = newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasModifierKeyOlderThan(final Element pointer) {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element == pointer) {
|
||||
return false; // Stop searching modifier key.
|
||||
}
|
||||
if (element.isModifier()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAnyInDraggingFinger() {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (element.isInDraggingFinger()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelAllPointerTrackers() {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "cancelAllPointerTracker: " + this);
|
||||
}
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
element.cancelTrackingForAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
synchronized (mExpandableArrayOfActivePointers) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
|
||||
final int arraySize = mArraySize;
|
||||
for (int index = 0; index < arraySize; index++) {
|
||||
final Element element = expandableArray.get(index);
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(element.toString());
|
||||
}
|
||||
return "[" + sb + "]";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/* package */ final class ShiftKeyState extends ModifierKeyState {
|
||||
private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked
|
||||
private static final int IGNORING = 4;
|
||||
|
||||
public ShiftKeyState(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOtherKeyPressed() {
|
||||
int oldState = mState;
|
||||
if (oldState == PRESSING) {
|
||||
mState = CHORDING;
|
||||
} else if (oldState == PRESSING_ON_SHIFTED) {
|
||||
mState = IGNORING;
|
||||
}
|
||||
if (DEBUG)
|
||||
Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
|
||||
}
|
||||
|
||||
public void onPressOnShifted() {
|
||||
mState = PRESSING_ON_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isPressingOnShifted() {
|
||||
return mState == PRESSING_ON_SHIFTED;
|
||||
}
|
||||
|
||||
public boolean isIgnoring() {
|
||||
return mState == IGNORING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(mState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toString(int state) {
|
||||
switch (state) {
|
||||
case PRESSING_ON_SHIFTED:
|
||||
return "PRESSING_ON_SHIFTED";
|
||||
case IGNORING:
|
||||
return "IGNORING";
|
||||
default:
|
||||
return super.toString(state);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.internal;
|
||||
|
||||
import android.os.Message;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.PointerTracker;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LeakGuardHandlerWrapper;
|
||||
|
||||
public final class TimerHandler extends LeakGuardHandlerWrapper<DrawingProxy>
|
||||
implements TimerProxy {
|
||||
private static final int MSG_TYPING_STATE_EXPIRED = 0;
|
||||
private static final int MSG_REPEAT_KEY = 1;
|
||||
private static final int MSG_LONGPRESS_KEY = 2;
|
||||
private static final int MSG_LONGPRESS_SHIFT_KEY = 3;
|
||||
private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 4;
|
||||
private static final int MSG_UPDATE_BATCH_INPUT = 5;
|
||||
private static final int MSG_DISMISS_KEY_PREVIEW = 6;
|
||||
private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 7;
|
||||
|
||||
private final int mIgnoreAltCodeKeyTimeout;
|
||||
|
||||
public TimerHandler(final DrawingProxy ownerInstance, final int ignoreAltCodeKeyTimeout) {
|
||||
super(ownerInstance);
|
||||
mIgnoreAltCodeKeyTimeout = ignoreAltCodeKeyTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final Message msg) {
|
||||
final DrawingProxy drawingProxy = getOwnerInstance();
|
||||
if (drawingProxy == null) {
|
||||
return;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MSG_REPEAT_KEY:
|
||||
final PointerTracker tracker1 = (PointerTracker) msg.obj;
|
||||
tracker1.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
|
||||
break;
|
||||
case MSG_LONGPRESS_KEY:
|
||||
case MSG_LONGPRESS_SHIFT_KEY:
|
||||
cancelLongPressTimers();
|
||||
final PointerTracker tracker2 = (PointerTracker) msg.obj;
|
||||
tracker2.onLongPressed();
|
||||
break;
|
||||
case MSG_DISMISS_KEY_PREVIEW:
|
||||
drawingProxy.onKeyReleased((Key) msg.obj, false /* withAnimation */);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startKeyRepeatTimerOf(final PointerTracker tracker, final int repeatCount,
|
||||
final int delay) {
|
||||
final Key key = tracker.getKey();
|
||||
if (key == null || delay == 0) {
|
||||
return;
|
||||
}
|
||||
sendMessageDelayed(
|
||||
obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay);
|
||||
}
|
||||
|
||||
private void cancelKeyRepeatTimerOf(final PointerTracker tracker) {
|
||||
removeMessages(MSG_REPEAT_KEY, tracker);
|
||||
}
|
||||
|
||||
public void cancelKeyRepeatTimers() {
|
||||
removeMessages(MSG_REPEAT_KEY);
|
||||
}
|
||||
|
||||
// TODO: Suppress layout changes in key repeat mode
|
||||
public boolean isInKeyRepeat() {
|
||||
return hasMessages(MSG_REPEAT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLongPressTimerOf(final PointerTracker tracker, final int delay) {
|
||||
final Key key = tracker.getKey();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
// Use a separate message id for long pressing shift key, because long press shift key
|
||||
// timers should be canceled when other key is pressed.
|
||||
final int messageId = (key.getCode() == Constants.CODE_SHIFT)
|
||||
? MSG_LONGPRESS_SHIFT_KEY : MSG_LONGPRESS_KEY;
|
||||
sendMessageDelayed(obtainMessage(messageId, tracker), delay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelLongPressTimersOf(final PointerTracker tracker) {
|
||||
removeMessages(MSG_LONGPRESS_KEY, tracker);
|
||||
removeMessages(MSG_LONGPRESS_SHIFT_KEY, tracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelLongPressShiftKeyTimer() {
|
||||
removeMessages(MSG_LONGPRESS_SHIFT_KEY);
|
||||
}
|
||||
|
||||
public void cancelLongPressTimers() {
|
||||
removeMessages(MSG_LONGPRESS_KEY);
|
||||
removeMessages(MSG_LONGPRESS_SHIFT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startTypingStateTimer(final Key typedKey) {
|
||||
if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isTyping = isTypingState();
|
||||
removeMessages(MSG_TYPING_STATE_EXPIRED);
|
||||
final DrawingProxy drawingProxy = getOwnerInstance();
|
||||
if (drawingProxy == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When user hits the space or the enter key, just cancel the while-typing timer.
|
||||
final int typedCode = typedKey.getCode();
|
||||
if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
|
||||
if (isTyping) {
|
||||
drawingProxy.startWhileTypingAnimation(DrawingProxy.FADE_IN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessageDelayed(
|
||||
obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
|
||||
if (isTyping) {
|
||||
return;
|
||||
}
|
||||
drawingProxy.startWhileTypingAnimation(DrawingProxy.FADE_OUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTypingState() {
|
||||
return hasMessages(MSG_TYPING_STATE_EXPIRED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDoubleTapShiftKeyTimer() {
|
||||
sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
|
||||
ViewConfiguration.getDoubleTapTimeout());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDoubleTapShiftKeyTimer() {
|
||||
removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInDoubleTapShiftKeyTimeout() {
|
||||
return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelKeyTimersOf(final PointerTracker tracker) {
|
||||
cancelKeyRepeatTimerOf(tracker);
|
||||
cancelLongPressTimersOf(tracker);
|
||||
}
|
||||
|
||||
public void cancelAllKeyTimers() {
|
||||
cancelKeyRepeatTimers();
|
||||
cancelLongPressTimers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
|
||||
removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAllUpdateBatchInputTimers() {
|
||||
removeMessages(MSG_UPDATE_BATCH_INPUT);
|
||||
}
|
||||
|
||||
public void postDismissKeyPreview(final Key key, final long delay) {
|
||||
sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, key), delay);
|
||||
}
|
||||
|
||||
public void cancelAllMessages() {
|
||||
cancelAllKeyTimers();
|
||||
cancelAllUpdateBatchInputTimers();
|
||||
removeMessages(MSG_DISMISS_KEY_PREVIEW);
|
||||
removeMessages(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.PointerTracker;
|
||||
|
||||
public interface TimerProxy {
|
||||
/**
|
||||
* Start a timer to detect if a user is typing keys.
|
||||
*
|
||||
* @param typedKey the key that is typed.
|
||||
*/
|
||||
void startTypingStateTimer(Key typedKey);
|
||||
|
||||
/**
|
||||
* Check if a user is key typing.
|
||||
*
|
||||
* @return true if a user is in typing.
|
||||
*/
|
||||
boolean isTypingState();
|
||||
|
||||
/**
|
||||
* Start a timer to simulate repeated key presses while a user keep pressing a key.
|
||||
*
|
||||
* @param tracker the {@link PointerTracker} that points the key to be repeated.
|
||||
* @param repeatCount the number of times that the key is repeating. Starting from 1.
|
||||
* @param delay the interval delay to the next key repeat, in millisecond.
|
||||
*/
|
||||
void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay);
|
||||
|
||||
/**
|
||||
* Start a timer to detect a long pressed key.
|
||||
* If a key pointed by <code>tracker</code> is a shift key, start another timer to detect
|
||||
* long pressed shift key.
|
||||
*
|
||||
* @param tracker the {@link PointerTracker} that starts long pressing.
|
||||
* @param delay the delay to fire the long press timer, in millisecond.
|
||||
*/
|
||||
void startLongPressTimerOf(PointerTracker tracker, int delay);
|
||||
|
||||
/**
|
||||
* Cancel timers for detecting a long pressed key and a long press shift key.
|
||||
*
|
||||
* @param tracker cancel long press timers of this {@link PointerTracker}.
|
||||
*/
|
||||
void cancelLongPressTimersOf(PointerTracker tracker);
|
||||
|
||||
/**
|
||||
* Cancel a timer for detecting a long pressed shift key.
|
||||
*/
|
||||
void cancelLongPressShiftKeyTimer();
|
||||
|
||||
/**
|
||||
* Cancel timers for detecting repeated key press, long pressed key, and long pressed shift key.
|
||||
*
|
||||
* @param tracker the {@link PointerTracker} that starts timers to be canceled.
|
||||
*/
|
||||
void cancelKeyTimersOf(PointerTracker tracker);
|
||||
|
||||
/**
|
||||
* Start a timer to detect double tapped shift key.
|
||||
*/
|
||||
void startDoubleTapShiftKeyTimer();
|
||||
|
||||
/**
|
||||
* Cancel a timer of detecting double tapped shift key.
|
||||
*/
|
||||
void cancelDoubleTapShiftKeyTimer();
|
||||
|
||||
/**
|
||||
* Check if a timer of detecting double tapped shift key is running.
|
||||
*
|
||||
* @return true if detecting double tapped shift key is on going.
|
||||
*/
|
||||
boolean isInDoubleTapShiftKeyTimeout();
|
||||
|
||||
/**
|
||||
* Cancel a timer of firing updating batch input.
|
||||
*
|
||||
* @param tracker the {@link PointerTracker} that resumes moving or ends gesture input.
|
||||
*/
|
||||
void cancelUpdateBatchInputTimer(PointerTracker tracker);
|
||||
|
||||
/**
|
||||
* Cancel all timers of firing updating batch input.
|
||||
*/
|
||||
void cancelAllUpdateBatchInputTimers();
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.internal;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.Key;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public abstract class UniqueKeysCache {
|
||||
public abstract void setEnabled(boolean enabled);
|
||||
|
||||
public abstract void clear();
|
||||
|
||||
public abstract Key getUniqueKey(Key key);
|
||||
|
||||
public static final UniqueKeysCache NO_CACHE = new UniqueKeysCache() {
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getUniqueKey(Key key) {
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
public static UniqueKeysCache newInstance() {
|
||||
return new UniqueKeysCacheImpl();
|
||||
}
|
||||
|
||||
private static final class UniqueKeysCacheImpl extends UniqueKeysCache {
|
||||
private final HashMap<Key, Key> mCache;
|
||||
|
||||
private boolean mEnabled;
|
||||
|
||||
UniqueKeysCacheImpl() {
|
||||
mCache = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
mCache.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getUniqueKey(final Key key) {
|
||||
if (!mEnabled) {
|
||||
return key;
|
||||
}
|
||||
final Key existingKey = mCache.get(key);
|
||||
if (existingKey != null) {
|
||||
// Reuse the existing object that equals to "key" without adding "key" to
|
||||
// the cache.
|
||||
return existingKey;
|
||||
}
|
||||
mCache.put(key, key);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Vibrator;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.View;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
|
||||
|
||||
/**
|
||||
* This class gathers audio feedback and haptic feedback functions.
|
||||
* <p>
|
||||
* It offers a consistent and simple interface that allows LatinIME to forget about the
|
||||
* complexity of settings and the like.
|
||||
*/
|
||||
public final class AudioAndHapticFeedbackManager {
|
||||
private AudioManager mAudioManager;
|
||||
private Vibrator mVibrator;
|
||||
|
||||
private SettingsValues mSettingsValues;
|
||||
private boolean mSoundOn;
|
||||
|
||||
private static final AudioAndHapticFeedbackManager sInstance =
|
||||
new AudioAndHapticFeedbackManager();
|
||||
|
||||
public static AudioAndHapticFeedbackManager getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private AudioAndHapticFeedbackManager() {
|
||||
// Intentional empty constructor for singleton.
|
||||
}
|
||||
|
||||
public static void init(final Context context) {
|
||||
sInstance.initInternal(context);
|
||||
}
|
||||
|
||||
private void initInternal(final Context context) {
|
||||
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
public boolean hasVibrator() {
|
||||
return mVibrator != null && mVibrator.hasVibrator();
|
||||
}
|
||||
|
||||
public void vibrate(final long milliseconds) {
|
||||
if (mVibrator == null) {
|
||||
return;
|
||||
}
|
||||
mVibrator.vibrate(milliseconds);
|
||||
}
|
||||
|
||||
private boolean reevaluateIfSoundIsOn() {
|
||||
if (mSettingsValues == null || !mSettingsValues.mSoundOn || mAudioManager == null) {
|
||||
return false;
|
||||
}
|
||||
return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
|
||||
}
|
||||
|
||||
public void performAudioFeedback(final int code) {
|
||||
// if mAudioManager is null, we can't play a sound anyway, so return
|
||||
if (mAudioManager == null) {
|
||||
return;
|
||||
}
|
||||
if (!mSoundOn) {
|
||||
return;
|
||||
}
|
||||
final int sound;
|
||||
switch (code) {
|
||||
case Constants.CODE_DELETE:
|
||||
sound = AudioManager.FX_KEYPRESS_DELETE;
|
||||
break;
|
||||
case Constants.CODE_ENTER:
|
||||
sound = AudioManager.FX_KEYPRESS_RETURN;
|
||||
break;
|
||||
case Constants.CODE_SPACE:
|
||||
sound = AudioManager.FX_KEYPRESS_SPACEBAR;
|
||||
break;
|
||||
default:
|
||||
sound = AudioManager.FX_KEYPRESS_STANDARD;
|
||||
break;
|
||||
}
|
||||
mAudioManager.playSoundEffect(sound, mSettingsValues.mKeypressSoundVolume);
|
||||
}
|
||||
|
||||
public void performHapticFeedback(final View viewToPerformHapticFeedbackOn) {
|
||||
if (!mSettingsValues.mVibrateOn) {
|
||||
return;
|
||||
}
|
||||
if (mSettingsValues.mKeypressVibrationDuration >= 0) {
|
||||
vibrate(mSettingsValues.mKeypressVibrationDuration);
|
||||
return;
|
||||
}
|
||||
// Go ahead with the system default
|
||||
if (viewToPerformHapticFeedbackOn != null) {
|
||||
viewToPerformHapticFeedbackOn.performHapticFeedback(
|
||||
HapticFeedbackConstants.KEYBOARD_TAP,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSettingsChanged(final SettingsValues settingsValues) {
|
||||
mSettingsValues = settingsValues;
|
||||
mSoundOn = reevaluateIfSoundIsOn();
|
||||
}
|
||||
|
||||
public void onRingerModeChanged() {
|
||||
mSoundOn = reevaluateIfSoundIsOn();
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.KeyListener;
|
||||
import android.text.style.SuggestionSpan;
|
||||
import android.util.Log;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Source: https://stackoverflow.com/a/39460124
|
||||
*/
|
||||
public class E2EEInputConnection extends BaseInputConnection {
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = E2EEInputConnection.class.getSimpleName();
|
||||
|
||||
private final TextView mTextView;
|
||||
|
||||
// Keeps track of nested begin/end batch edit to ensure this connection always has a
|
||||
// balanced impact on its associated TextView.
|
||||
// A negative value means that this connection has been finished by the InputMethodManager.
|
||||
private int mBatchEditNesting;
|
||||
|
||||
public E2EEInputConnection(TextView textview) {
|
||||
super(textview, true);
|
||||
mTextView = textview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
TextView tv = mTextView;
|
||||
if (tv != null) {
|
||||
return tv.getEditableText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginBatchEdit() {
|
||||
synchronized (this) {
|
||||
if (mBatchEditNesting >= 0) {
|
||||
mTextView.beginBatchEdit();
|
||||
mBatchEditNesting++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endBatchEdit() {
|
||||
synchronized (this) {
|
||||
if (mBatchEditNesting > 0) {
|
||||
// When the connection is reset by the InputMethodManager and reportFinish
|
||||
// is called, some endBatchEdit calls may still be asynchronously received from the
|
||||
// IME. Do not take these into account, thus ensuring that this IC's final
|
||||
// contribution to mTextView's nested batch edit count is zero.
|
||||
mTextView.endBatchEdit();
|
||||
mBatchEditNesting--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearMetaKeyStates(int states) {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) return false;
|
||||
KeyListener kl = mTextView.getKeyListener();
|
||||
if (kl != null) {
|
||||
try {
|
||||
kl.clearMetaKeyState(mTextView, content, states);
|
||||
} catch (AbstractMethodError e) {
|
||||
// This is an old listener that doesn't implement the
|
||||
// new method.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitCompletion(CompletionInfo text) {
|
||||
if (DEBUG) Log.v(TAG, "commitCompletion " + text);
|
||||
mTextView.beginBatchEdit();
|
||||
mTextView.onCommitCompletion(text);
|
||||
mTextView.endBatchEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
|
||||
*/
|
||||
@Override
|
||||
public boolean commitCorrection(CorrectionInfo correctionInfo) {
|
||||
if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
|
||||
mTextView.beginBatchEdit();
|
||||
mTextView.onCommitCorrection(correctionInfo);
|
||||
mTextView.endBatchEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performEditorAction(int actionCode) {
|
||||
if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
|
||||
mTextView.onEditorAction(actionCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performContextMenuAction(int id) {
|
||||
if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
|
||||
mTextView.beginBatchEdit();
|
||||
mTextView.onTextContextMenuItem(id);
|
||||
mTextView.endBatchEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
||||
if (mTextView != null) {
|
||||
ExtractedText et = new ExtractedText();
|
||||
if (mTextView.extractText(request, et)) {
|
||||
if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) {
|
||||
// mTextView.setExtracting(request);
|
||||
}
|
||||
return et;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performPrivateCommand(String action, Bundle data) {
|
||||
mTextView.onPrivateIMECommand(action, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (mTextView == null) {
|
||||
return super.commitText(text, newCursorPosition);
|
||||
}
|
||||
if (text instanceof Spanned) {
|
||||
Spanned spanned = ((Spanned) text);
|
||||
SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
|
||||
// mIMM.registerSuggestionSpansForNotification(spans);
|
||||
}
|
||||
|
||||
// mTextView.resetErrorChangedFlag();
|
||||
boolean success = super.commitText(text, newCursorPosition);
|
||||
// mTextView.hideErrorIfUnchanged();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||
if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
|
||||
|
||||
// It is possible that any other bit is used as a valid flag in a future release.
|
||||
// We should reject the entire request in such a case.
|
||||
final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR;
|
||||
final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
|
||||
if (unknownFlags != 0) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." + " cursorUpdateMode=" + cursorUpdateMode + " unknownFlags=" + unknownFlags);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
|
||||
|
||||
/**
|
||||
* Class to hold attributes of the input field.
|
||||
*/
|
||||
public final class InputAttributes {
|
||||
private final String TAG = InputAttributes.class.getSimpleName();
|
||||
|
||||
final public String mTargetApplicationPackageName;
|
||||
final public boolean mInputTypeNoAutoCorrect;
|
||||
final public boolean mIsPasswordField;
|
||||
final public boolean mShouldShowSuggestions;
|
||||
final public boolean mApplicationSpecifiedCompletionOn;
|
||||
final public boolean mShouldInsertSpacesAutomatically;
|
||||
/**
|
||||
* Whether the floating gesture preview should be disabled. If true, this should override the
|
||||
* corresponding keyboard settings preference, always suppressing the floating preview text.
|
||||
*/
|
||||
final private int mInputType;
|
||||
|
||||
public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
|
||||
mTargetApplicationPackageName = null != editorInfo ? editorInfo.packageName : null;
|
||||
final int inputType = null != editorInfo ? editorInfo.inputType : 0;
|
||||
final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
|
||||
mInputType = inputType;
|
||||
mIsPasswordField = InputTypeUtils.isPasswordInputType(inputType)
|
||||
|| InputTypeUtils.isVisiblePasswordInputType(inputType);
|
||||
if (inputClass != InputType.TYPE_CLASS_TEXT) {
|
||||
// If we are not looking at a TYPE_CLASS_TEXT field, the following strange
|
||||
// cases may arise, so we do a couple sanity checks for them. If it's a
|
||||
// TYPE_CLASS_TEXT field, these special cases cannot happen, by construction
|
||||
// of the flags.
|
||||
if (null == editorInfo) {
|
||||
Log.w(TAG, "No editor info for this field. Bug?");
|
||||
} else if (InputType.TYPE_NULL == inputType) {
|
||||
// TODO: We should honor TYPE_NULL specification.
|
||||
Log.i(TAG, "InputType.TYPE_NULL is specified");
|
||||
} else if (inputClass == 0) {
|
||||
// TODO: is this check still necessary?
|
||||
Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x"
|
||||
+ " imeOptions=0x%08x", inputType, editorInfo.imeOptions));
|
||||
}
|
||||
mShouldShowSuggestions = false;
|
||||
mInputTypeNoAutoCorrect = false;
|
||||
mApplicationSpecifiedCompletionOn = false;
|
||||
mShouldInsertSpacesAutomatically = false;
|
||||
return;
|
||||
}
|
||||
// inputClass == InputType.TYPE_CLASS_TEXT
|
||||
final int variation = inputType & InputType.TYPE_MASK_VARIATION;
|
||||
final boolean flagNoSuggestions =
|
||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
final boolean flagMultiLine =
|
||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
final boolean flagAutoCorrect =
|
||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
|
||||
final boolean flagAutoComplete =
|
||||
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
|
||||
|
||||
// TODO: Have a helper method in InputTypeUtils
|
||||
// Make sure that passwords are not displayed in {@link SuggestionStripView}.
|
||||
final boolean shouldSuppressSuggestions = mIsPasswordField
|
||||
|| InputTypeUtils.isEmailVariation(variation)
|
||||
|| InputType.TYPE_TEXT_VARIATION_URI == variation
|
||||
|| InputType.TYPE_TEXT_VARIATION_FILTER == variation
|
||||
|| flagNoSuggestions
|
||||
|| flagAutoComplete;
|
||||
mShouldShowSuggestions = !shouldSuppressSuggestions;
|
||||
|
||||
mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType);
|
||||
|
||||
// If it's a browser edit field and auto correct is not ON explicitly, then
|
||||
// disable auto correction, but keep suggestions on.
|
||||
// If NO_SUGGESTIONS is set, don't do prediction.
|
||||
// If it's not multiline and the autoCorrect flag is not set, then don't correct
|
||||
mInputTypeNoAutoCorrect =
|
||||
(variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT && !flagAutoCorrect)
|
||||
|| flagNoSuggestions
|
||||
|| (!flagAutoCorrect && !flagMultiLine);
|
||||
|
||||
mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode;
|
||||
}
|
||||
|
||||
public boolean isTypeNull() {
|
||||
return InputType.TYPE_NULL == mInputType;
|
||||
}
|
||||
|
||||
public boolean isSameInputType(final EditorInfo editorInfo) {
|
||||
return editorInfo.inputType == mInputType;
|
||||
}
|
||||
|
||||
// Pretty print
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s: inputType=0x%08x%s%s%s%s%s targetApp=%s\n", getClass().getSimpleName(),
|
||||
mInputType,
|
||||
(mInputTypeNoAutoCorrect ? " noAutoCorrect" : ""),
|
||||
(mIsPasswordField ? " password" : ""),
|
||||
(mShouldShowSuggestions ? " shouldShowSuggestions" : ""),
|
||||
(mApplicationSpecifiedCompletionOn ? " appSpecified" : ""),
|
||||
(mShouldInsertSpacesAutomatically ? " insertSpaces" : ""),
|
||||
mTargetApplicationPackageName);
|
||||
}
|
||||
|
||||
public static boolean inPrivateImeOptions(final String packageName, final String key,
|
||||
final EditorInfo editorInfo) {
|
||||
if (editorInfo == null) return false;
|
||||
final String findingKey = (packageName != null) ? packageName + "." + key : key;
|
||||
return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions);
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.MainKeyboardView;
|
||||
import com.amnesica.kryptey.inputmethod.latin.e2ee.E2EEStripView;
|
||||
|
||||
public final class InputView extends FrameLayout {
|
||||
private MainKeyboardView mMainKeyboardView;
|
||||
private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
|
||||
|
||||
public InputView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
final E2EEStripView e2eeStripView = findViewById(R.id.e2ee_strip_view);
|
||||
mMainKeyboardView = findViewById(R.id.keyboard_view);
|
||||
mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder(
|
||||
mMainKeyboardView, e2eeStripView);
|
||||
|
||||
super.onFinishInflate();
|
||||
}
|
||||
|
||||
public void setKeyboardTopPadding(final int keyboardTopPadding) {
|
||||
mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class forwards {@link android.view.MotionEvent}s happened in the top padding of
|
||||
* {@link MainKeyboardView} to {@link com.amnesica.kryptey.inputmethod.latin.e2ee.E2EEStripView}.
|
||||
*/
|
||||
private static class KeyboardTopPaddingForwarder
|
||||
extends MotionEventForwarder<MainKeyboardView, E2EEStripView> {
|
||||
private int mKeyboardTopPadding;
|
||||
|
||||
public KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView,
|
||||
final E2EEStripView e2eeStripView) {
|
||||
super(mainKeyboardView, e2eeStripView);
|
||||
}
|
||||
|
||||
public void setKeyboardTopPadding(final int keyboardTopPadding) {
|
||||
mKeyboardTopPadding = keyboardTopPadding;
|
||||
}
|
||||
|
||||
private boolean isInKeyboardTopPadding(final int y) {
|
||||
return y < mEventSendingRect.top + mKeyboardTopPadding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needsToForward(final int x, final int y) {
|
||||
// Forwarding an event only when {@link MainKeyboardView} is visible.
|
||||
// Because the visibility of {@link MainKeyboardView} is controlled by its parent
|
||||
// view in {@link KeyboardSwitcher#setMainKeyboardFrame()}, we should check the
|
||||
// visibility of the parent view.
|
||||
final View mainKeyboardFrame = (View) mSenderView.getParent();
|
||||
return mainKeyboardFrame.getVisibility() == View.VISIBLE && isInKeyboardTopPadding(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int translateY(final int y) {
|
||||
final int translatedY = super.translateY(y);
|
||||
if (isInKeyboardTopPadding(y)) {
|
||||
// The forwarded event should have coordinates that are inside of the target.
|
||||
return Math.min(translatedY, mEventReceivingRect.height() - 1);
|
||||
}
|
||||
return translatedY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class forwards series of {@link android.view.MotionEvent}s from <code>SenderView</code> to
|
||||
* <code>ReceiverView</code>.
|
||||
*
|
||||
* @param <SenderView> a {@link View} that may send a {@link android.view.MotionEvent} to <ReceiverView>.
|
||||
* @param <ReceiverView> a {@link View} that receives forwarded {@link android.view.MotionEvent} from
|
||||
* <SenderView>.
|
||||
*/
|
||||
private static abstract class
|
||||
MotionEventForwarder<SenderView extends View, ReceiverView extends View> {
|
||||
protected final SenderView mSenderView;
|
||||
protected final ReceiverView mReceiverView;
|
||||
|
||||
protected final Rect mEventSendingRect = new Rect();
|
||||
protected final Rect mEventReceivingRect = new Rect();
|
||||
|
||||
public MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView) {
|
||||
mSenderView = senderView;
|
||||
mReceiverView = receiverView;
|
||||
}
|
||||
|
||||
// Return true if a touch event of global coordinate x, y needs to be forwarded.
|
||||
protected abstract boolean needsToForward(final int x, final int y);
|
||||
|
||||
// Translate global x-coordinate to <code>ReceiverView</code> local coordinate.
|
||||
protected int translateX(final int x) {
|
||||
return x - mEventReceivingRect.left;
|
||||
}
|
||||
|
||||
// Translate global y-coordinate to <code>ReceiverView</code> local coordinate.
|
||||
protected int translateY(final int y) {
|
||||
return y - mEventReceivingRect.top;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when a {@link android.view.MotionEvent} is forwarded.
|
||||
*
|
||||
* @param me the motion event to be forwarded.
|
||||
*/
|
||||
protected void onForwardingEvent(final MotionEvent me) {
|
||||
}
|
||||
|
||||
// Returns true if a {@link MotionEvent} is needed to be forwarded to
|
||||
// <code>ReceiverView</code>. Otherwise returns false.
|
||||
public boolean onInterceptTouchEvent(final int x, final int y, final MotionEvent me) {
|
||||
// Forwards a {link MotionEvent} only if both <code>SenderView</code> and
|
||||
// <code>ReceiverView</code> are visible.
|
||||
if (mSenderView.getVisibility() != View.VISIBLE ||
|
||||
mReceiverView.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
mSenderView.getGlobalVisibleRect(mEventSendingRect);
|
||||
if (!mEventSendingRect.contains(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (me.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
// If the down event happens in the forwarding area, successive
|
||||
// {@link MotionEvent}s should be forwarded to <code>ReceiverView</code>.
|
||||
return needsToForward(x, y);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if a {@link MotionEvent} is forwarded to <code>ReceiverView</code>.
|
||||
// Otherwise returns false.
|
||||
public boolean onTouchEvent(final int x, final int y, final MotionEvent me) {
|
||||
mReceiverView.getGlobalVisibleRect(mEventReceivingRect);
|
||||
// Translate global coordinates to <code>ReceiverView</code> local coordinates.
|
||||
me.setLocation(translateX(x), translateY(y));
|
||||
mReceiverView.dispatchTouchEvent(me);
|
||||
onForwardingEvent(me);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,610 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.SystemClock;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.UnicodeSurrogate;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.SpacingAndPunctuations;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.CapsModeUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.DebugLogUtils;
|
||||
|
||||
/**
|
||||
* Enrichment class for InputConnection to simplify interaction and add functionality.
|
||||
* <p>
|
||||
* This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
|
||||
* InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
|
||||
* all the time to find out what text is in the buffer, when we need it to determine caps mode
|
||||
* for example.
|
||||
*/
|
||||
public final class RichInputConnection {
|
||||
private static final String TAG = "RichInputConnection";
|
||||
private static final boolean DBG = false;
|
||||
private static final boolean DEBUG_PREVIOUS_TEXT = false;
|
||||
private static final boolean DEBUG_BATCH_NESTING = false;
|
||||
private static final int INVALID_CURSOR_POSITION = -1;
|
||||
|
||||
/**
|
||||
* The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter
|
||||
*/
|
||||
private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000;
|
||||
/**
|
||||
* The amount of time a {@link #getTextBeforeCursor} call needs
|
||||
*/
|
||||
private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200;
|
||||
|
||||
private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0;
|
||||
private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1;
|
||||
private static final int OPERATION_RELOAD_TEXT_CACHE = 3;
|
||||
private static final String[] OPERATION_NAMES = new String[]{
|
||||
"GET_TEXT_BEFORE_CURSOR",
|
||||
"GET_TEXT_AFTER_CURSOR",
|
||||
"GET_WORD_RANGE_AT_CURSOR",
|
||||
"RELOAD_TEXT_CACHE"};
|
||||
|
||||
/**
|
||||
* This variable contains an expected value for the selection start position. This is where the
|
||||
* cursor or selection start may end up after all the keyboard-triggered updates have passed. We
|
||||
* keep this to compare it to the actual selection start to guess whether the move was caused by
|
||||
* a keyboard command or not.
|
||||
* It's not really the selection start position: the selection start may not be there yet, and
|
||||
* in some cases, it may never arrive there.
|
||||
*/
|
||||
private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points
|
||||
/**
|
||||
* The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is
|
||||
* expected. The same caveats as mExpectedSelStart apply.
|
||||
*/
|
||||
private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points
|
||||
/**
|
||||
* This contains the committed text immediately preceding the cursor and the composing
|
||||
* text, if any. It is refreshed when the cursor moves by calling upon the TextView.
|
||||
*/
|
||||
private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
|
||||
/**
|
||||
* This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
|
||||
*/
|
||||
private final StringBuilder mComposingText = new StringBuilder();
|
||||
|
||||
/**
|
||||
* This variable is a temporary object used in { #commitText(CharSequence, int)}
|
||||
* to avoid object creation.
|
||||
*/
|
||||
private final SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
|
||||
|
||||
private final InputMethodService mParent;
|
||||
private InputConnection mIC;
|
||||
private E2EEInputConnection mOtherIC;
|
||||
private int mNestLevel;
|
||||
|
||||
private boolean shouldUseOtherIC;
|
||||
|
||||
public RichInputConnection(final InputMethodService parent) {
|
||||
mParent = parent;
|
||||
mIC = null;
|
||||
mNestLevel = 0;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return getIC() != null;
|
||||
}
|
||||
|
||||
private void checkConsistencyForDebug() {
|
||||
final ExtractedTextRequest r = new ExtractedTextRequest();
|
||||
r.hintMaxChars = 0;
|
||||
r.hintMaxLines = 0;
|
||||
r.token = 1;
|
||||
r.flags = 0;
|
||||
final ExtractedText et = getIC().getExtractedText(r, 0);
|
||||
final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
|
||||
0);
|
||||
final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText)
|
||||
.append(mComposingText);
|
||||
if (null == et || null == beforeCursor) return;
|
||||
final int actualLength = Math.min(beforeCursor.length(), internal.length());
|
||||
if (internal.length() > actualLength) {
|
||||
internal.delete(0, internal.length() - actualLength);
|
||||
}
|
||||
final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
|
||||
: beforeCursor.subSequence(beforeCursor.length() - actualLength,
|
||||
beforeCursor.length()).toString();
|
||||
if (et.selectionStart != mExpectedSelStart
|
||||
|| !(reference.equals(internal.toString()))) {
|
||||
final String context = "Expected selection start = " + mExpectedSelStart
|
||||
+ "\nActual selection start = " + et.selectionStart
|
||||
+ "\nExpected text = " + internal.length() + " " + internal
|
||||
+ "\nActual text = " + reference.length() + " " + reference;
|
||||
((LatinIME) mParent).debugDumpStateAndCrashWithException(context);
|
||||
} else {
|
||||
Log.e(TAG, DebugLogUtils.getStackTrace(2));
|
||||
Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart);
|
||||
}
|
||||
}
|
||||
|
||||
public void beginBatchEdit() {
|
||||
if (++mNestLevel == 1) {
|
||||
if (isConnected()) {
|
||||
getIC().beginBatchEdit();
|
||||
}
|
||||
} else {
|
||||
if (DBG) {
|
||||
throw new RuntimeException("Nest level too deep");
|
||||
}
|
||||
Log.e(TAG, "Nest level too deep : " + mNestLevel);
|
||||
}
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
}
|
||||
|
||||
public void endBatchEdit() {
|
||||
if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
|
||||
if (--mNestLevel == 0 && isConnected()) {
|
||||
getIC().endBatchEdit();
|
||||
}
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cached text and retrieve it again from the editor.
|
||||
* <p>
|
||||
* This should be called when the cursor moved. It's possible that we can't connect to
|
||||
* the application when doing this; notably, this happens sometimes during rotation, probably
|
||||
* because of a race condition in the framework. In this case, we just can't retrieve the
|
||||
* data, so we empty the cache and note that we don't know the new cursor position, and we
|
||||
* return false so that the caller knows about this and can retry later.
|
||||
*
|
||||
* @param newSelStart the new position of the selection start, as received from the system.
|
||||
* @param newSelEnd the new position of the selection end, as received from the system.
|
||||
* @return true if we were able to connect to the editor successfully, false otherwise. When
|
||||
* this method returns false, the caches could not be correctly refreshed so they were only
|
||||
* reset: the caller should try again later to return to normal operation.
|
||||
*/
|
||||
public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart,
|
||||
final int newSelEnd) {
|
||||
mExpectedSelStart = newSelStart;
|
||||
mExpectedSelEnd = newSelEnd;
|
||||
mComposingText.setLength(0);
|
||||
final boolean didReloadTextSuccessfully = reloadTextCache();
|
||||
if (!didReloadTextSuccessfully) {
|
||||
Log.d(TAG, "Will try to retrieve text later.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the cached text from the InputConnection.
|
||||
*
|
||||
* @return true if successful
|
||||
*/
|
||||
private boolean reloadTextCache() {
|
||||
mCommittedTextBeforeComposingText.setLength(0);
|
||||
// Call upon the inputconnection directly since our own method is using the cache, and
|
||||
// we want to refresh it.
|
||||
final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection(
|
||||
OPERATION_RELOAD_TEXT_CACHE,
|
||||
SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS,
|
||||
Constants.EDITOR_CONTENTS_CACHE_SIZE,
|
||||
0 /* flags */);
|
||||
if (null == textBeforeCursor) {
|
||||
// For some reason the app thinks we are not connected to it. This looks like a
|
||||
// framework bug... Fall back to ground state and return false.
|
||||
mExpectedSelStart = INVALID_CURSOR_POSITION;
|
||||
mExpectedSelEnd = INVALID_CURSOR_POSITION;
|
||||
Log.e(TAG, "Unable to connect to the editor to retrieve text.");
|
||||
return false;
|
||||
}
|
||||
mCommittedTextBeforeComposingText.append(textBeforeCursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkBatchEdit() {
|
||||
if (mNestLevel != 1) {
|
||||
// TODO: exception instead
|
||||
Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
|
||||
Log.e(TAG, DebugLogUtils.getStackTrace(4));
|
||||
}
|
||||
}
|
||||
|
||||
public void finishComposingText() {
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
// TODO: this is not correct! The cursor is not necessarily after the composing text.
|
||||
// In the practice right now this is only called when input ends so it will be reset so
|
||||
// it works, but it's wrong and should be fixed.
|
||||
mCommittedTextBeforeComposingText.append(mComposingText);
|
||||
mComposingText.setLength(0);
|
||||
if (isConnected()) {
|
||||
getIC().finishComposingText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link InputConnection#commitText(CharSequence, int)}.
|
||||
*
|
||||
* @param text The text to commit. This may include styles.
|
||||
* @param newCursorPosition The new cursor position around the text.
|
||||
*/
|
||||
public void commitText(final CharSequence text, final int newCursorPosition) {
|
||||
RichInputMethodManager.getInstance().resetSubtypeCycleOrder();
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
mCommittedTextBeforeComposingText.append(text);
|
||||
// TODO: the following is exceedingly error-prone. Right now when the cursor is in the
|
||||
// middle of the composing word mComposingText only holds the part of the composing text
|
||||
// that is before the cursor, so this actually works, but it's terribly confusing. Fix this.
|
||||
if (hasCursorPosition()) {
|
||||
mExpectedSelStart += text.length() - mComposingText.length();
|
||||
mExpectedSelEnd = mExpectedSelStart;
|
||||
}
|
||||
mComposingText.setLength(0);
|
||||
if (isConnected()) {
|
||||
mTempObjectForCommitText.clear();
|
||||
mTempObjectForCommitText.append(text);
|
||||
final CharacterStyle[] spans = mTempObjectForCommitText.getSpans(
|
||||
0, text.length(), CharacterStyle.class);
|
||||
for (final CharacterStyle span : spans) {
|
||||
final int spanStart = mTempObjectForCommitText.getSpanStart(span);
|
||||
final int spanEnd = mTempObjectForCommitText.getSpanEnd(span);
|
||||
final int spanFlags = mTempObjectForCommitText.getSpanFlags(span);
|
||||
// We have to adjust the end of the span to include an additional character.
|
||||
// This is to avoid splitting a unicode surrogate pair.
|
||||
// See com.amnesica.kryptey.inputmethod.latin.common.Constants.UnicodeSurrogate
|
||||
// See https://b.corp.google.com/issues/19255233
|
||||
if (0 < spanEnd && spanEnd < mTempObjectForCommitText.length()) {
|
||||
final char spanEndChar = mTempObjectForCommitText.charAt(spanEnd - 1);
|
||||
final char nextChar = mTempObjectForCommitText.charAt(spanEnd);
|
||||
if (UnicodeSurrogate.isLowSurrogate(spanEndChar)
|
||||
&& UnicodeSurrogate.isHighSurrogate(nextChar)) {
|
||||
mTempObjectForCommitText.setSpan(span, spanStart, spanEnd + 1, spanFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
getIC().commitText(mTempObjectForCommitText, newCursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getSelectedText(final int flags) {
|
||||
return isConnected() ? getIC().getSelectedText(flags) : null;
|
||||
}
|
||||
|
||||
public boolean canDeleteCharacters() {
|
||||
return mExpectedSelStart > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the caps modes we should be in after this specific string.
|
||||
* <p>
|
||||
* This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
|
||||
* This method also supports faking an additional space after the string passed in argument,
|
||||
* to support cases where a space will be added automatically, like in phantom space
|
||||
* state for example.
|
||||
* Note that for English, we are using American typography rules (which are not specific to
|
||||
* American English, it's just the most common set of rules for English).
|
||||
*
|
||||
* @param inputType a mask of the caps modes to test for.
|
||||
* @param spacingAndPunctuations the values of the settings to use for locale and separators.
|
||||
* @return the caps modes that should be on as a set of bits
|
||||
*/
|
||||
public int getCursorCapsMode(final int inputType, final SpacingAndPunctuations spacingAndPunctuations) {
|
||||
if (!isConnected()) {
|
||||
return Constants.TextUtils.CAP_MODE_OFF;
|
||||
}
|
||||
if (!TextUtils.isEmpty(mComposingText)) {
|
||||
// We have some composing text - we should be in MODE_CHARACTERS only.
|
||||
return TextUtils.CAP_MODE_CHARACTERS & inputType;
|
||||
}
|
||||
// TODO: this will generally work, but there may be cases where the buffer contains SOME
|
||||
// information but not enough to determine the caps mode accurately. This may happen after
|
||||
// heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
|
||||
// getCapsMode should be updated to be able to return a "not enough info" result so that
|
||||
// we can get more context only when needed.
|
||||
if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) {
|
||||
if (!reloadTextCache()) {
|
||||
Log.w(TAG, "Unable to connect to the editor. "
|
||||
+ "Setting caps mode without knowing text.");
|
||||
}
|
||||
}
|
||||
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
|
||||
// never blocks or initiates IPC.
|
||||
// TODO: don't call #toString() here. Instead, all accesses to
|
||||
// mCommittedTextBeforeComposingText should be done on the main thread.
|
||||
return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText.toString(), inputType,
|
||||
spacingAndPunctuations);
|
||||
}
|
||||
|
||||
public int getCodePointBeforeCursor() {
|
||||
final int length = mCommittedTextBeforeComposingText.length();
|
||||
if (length < 1) return Constants.NOT_A_CODE;
|
||||
return Character.codePointBefore(mCommittedTextBeforeComposingText, length);
|
||||
}
|
||||
|
||||
public CharSequence getTextBeforeCursor(final int n, final int flags) {
|
||||
final int cachedLength =
|
||||
mCommittedTextBeforeComposingText.length() + mComposingText.length();
|
||||
// If we have enough characters to satisfy the request, or if we have all characters in
|
||||
// the text field, then we can return the cached version right away.
|
||||
// However, if we don't have an expected cursor position, then we should always
|
||||
// go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to
|
||||
// test for this explicitly)
|
||||
if (INVALID_CURSOR_POSITION != mExpectedSelStart
|
||||
&& (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
|
||||
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
|
||||
// We call #toString() here to create a temporary object.
|
||||
// In some situations, this method is called on a worker thread, and it's possible
|
||||
// the main thread touches the contents of mComposingText while this worker thread
|
||||
// is suspended, because mComposingText is a StringBuilder. This may lead to crashes,
|
||||
// so we call #toString() on it. That will result in the return value being strictly
|
||||
// speaking wrong, but since this is used for basing bigram probability off, and
|
||||
// it's only going to matter for one getSuggestions call, it's fine in the practice.
|
||||
s.append(mComposingText);
|
||||
if (s.length() > n) {
|
||||
s.delete(0, s.length() - n);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
return getTextBeforeCursorAndDetectLaggyConnection(
|
||||
OPERATION_GET_TEXT_BEFORE_CURSOR,
|
||||
SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
|
||||
n, flags);
|
||||
}
|
||||
|
||||
public CharSequence getTextAfterCursor(final int n, final int flags) {
|
||||
return getTextAfterCursorAndDetectLaggyConnection(
|
||||
OPERATION_GET_TEXT_AFTER_CURSOR,
|
||||
SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
|
||||
n, flags);
|
||||
}
|
||||
|
||||
private CharSequence getTextBeforeCursorAndDetectLaggyConnection(
|
||||
final int operation, final long timeout, final int n, final int flags) {
|
||||
if (!isConnected()) {
|
||||
return null;
|
||||
}
|
||||
final long startTime = SystemClock.uptimeMillis();
|
||||
final CharSequence result = getIC().getTextBeforeCursor(n, flags);
|
||||
detectLaggyConnection(operation, timeout, startTime);
|
||||
return result;
|
||||
}
|
||||
|
||||
private CharSequence getTextAfterCursorAndDetectLaggyConnection(
|
||||
final int operation, final long timeout, final int n, final int flags) {
|
||||
if (!isConnected()) {
|
||||
return null;
|
||||
}
|
||||
final long startTime = SystemClock.uptimeMillis();
|
||||
final CharSequence result = getIC().getTextAfterCursor(n, flags);
|
||||
detectLaggyConnection(operation, timeout, startTime);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void detectLaggyConnection(final int operation, final long timeout, final long startTime) {
|
||||
final long duration = SystemClock.uptimeMillis() - startTime;
|
||||
if (duration >= timeout) {
|
||||
final String operationName = OPERATION_NAMES[operation];
|
||||
Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms.");
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteTextBeforeCursor(final int beforeLength) {
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
// TODO: the following is incorrect if the cursor is not immediately after the composition.
|
||||
// Right now we never come here in this case because we reset the composing state before we
|
||||
// come here in this case, but we need to fix this.
|
||||
final int remainingChars = mComposingText.length() - beforeLength;
|
||||
if (remainingChars >= 0) {
|
||||
mComposingText.setLength(remainingChars);
|
||||
} else {
|
||||
mComposingText.setLength(0);
|
||||
// Never cut under 0
|
||||
final int len = Math.max(mCommittedTextBeforeComposingText.length()
|
||||
+ remainingChars, 0);
|
||||
mCommittedTextBeforeComposingText.setLength(len);
|
||||
}
|
||||
if (mExpectedSelStart > beforeLength) {
|
||||
mExpectedSelStart -= beforeLength;
|
||||
mExpectedSelEnd -= beforeLength;
|
||||
} else {
|
||||
// There are fewer characters before the cursor in the buffer than we are being asked to
|
||||
// delete. Only delete what is there, and update the end with the amount deleted.
|
||||
mExpectedSelEnd -= mExpectedSelStart;
|
||||
mExpectedSelStart = 0;
|
||||
}
|
||||
if (isConnected()) {
|
||||
getIC().deleteSurroundingText(beforeLength, 0);
|
||||
}
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
}
|
||||
|
||||
public void performEditorAction(final int actionId) {
|
||||
if (isConnected()) {
|
||||
getIC().performEditorAction(actionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyEvent(final KeyEvent keyEvent) {
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
// This method is only called for enter or backspace when speaking to old applications
|
||||
// (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits.
|
||||
// When talking to new applications we never use this method because it's inherently
|
||||
// racy and has unpredictable results, but for backward compatibility we continue
|
||||
// sending the key events for only Enter and Backspace because some applications
|
||||
// mistakenly catch them to do some stuff.
|
||||
switch (keyEvent.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
mCommittedTextBeforeComposingText.append("\n");
|
||||
if (hasCursorPosition()) {
|
||||
mExpectedSelStart += 1;
|
||||
mExpectedSelEnd = mExpectedSelStart;
|
||||
}
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
if (0 == mComposingText.length()) {
|
||||
if (mCommittedTextBeforeComposingText.length() > 0) {
|
||||
mCommittedTextBeforeComposingText.delete(
|
||||
mCommittedTextBeforeComposingText.length() - 1,
|
||||
mCommittedTextBeforeComposingText.length());
|
||||
}
|
||||
} else {
|
||||
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
|
||||
}
|
||||
|
||||
if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) {
|
||||
// TODO: Handle surrogate pairs.
|
||||
mExpectedSelStart -= 1;
|
||||
}
|
||||
mExpectedSelEnd = mExpectedSelStart;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_UNKNOWN:
|
||||
if (null != keyEvent.getCharacters()) {
|
||||
mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
|
||||
if (hasCursorPosition()) {
|
||||
mExpectedSelStart += keyEvent.getCharacters().length();
|
||||
mExpectedSelEnd = mExpectedSelStart;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar());
|
||||
mCommittedTextBeforeComposingText.append(text);
|
||||
if (hasCursorPosition()) {
|
||||
mExpectedSelStart += text.length();
|
||||
mExpectedSelEnd = mExpectedSelStart;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isConnected()) {
|
||||
getIC().sendKeyEvent(keyEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection of the text editor.
|
||||
* <p>
|
||||
* Calls through to {@link InputConnection#setSelection(int, int)}.
|
||||
*
|
||||
* @param start the character index where the selection should start.
|
||||
* @param end the character index where the selection should end.
|
||||
* @return Returns true on success, false on failure: either the input connection is no longer
|
||||
* valid when setting the selection or when retrieving the text cache at that point, or
|
||||
* invalid arguments were passed.
|
||||
*/
|
||||
public void setSelection(int start, int end) {
|
||||
if (DEBUG_BATCH_NESTING) checkBatchEdit();
|
||||
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
|
||||
if (start < 0 || end < 0) {
|
||||
return;
|
||||
}
|
||||
if (mExpectedSelStart == start && mExpectedSelEnd == end) {
|
||||
return;
|
||||
}
|
||||
|
||||
mExpectedSelStart = start;
|
||||
mExpectedSelEnd = end;
|
||||
if (isConnected()) {
|
||||
final boolean isIcValid = getIC().setSelection(start, end);
|
||||
if (!isIcValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
reloadTextCache();
|
||||
}
|
||||
|
||||
public int getExpectedSelectionStart() {
|
||||
return mExpectedSelStart;
|
||||
}
|
||||
|
||||
public int getExpectedSelectionEnd() {
|
||||
return mExpectedSelEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether there is a selection currently active.
|
||||
*/
|
||||
public boolean hasSelection() {
|
||||
return mExpectedSelEnd != mExpectedSelStart;
|
||||
}
|
||||
|
||||
public boolean hasCursorPosition() {
|
||||
return mExpectedSelStart != INVALID_CURSOR_POSITION && mExpectedSelEnd != INVALID_CURSOR_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some chars, such as emoji consist of 2 chars (surrogate pairs). We should treat them as one character.
|
||||
*/
|
||||
public int getUnicodeSteps(int chars, boolean rightSidePointer) {
|
||||
int steps = 0;
|
||||
if (chars < 0) {
|
||||
CharSequence charsBeforeCursor = rightSidePointer && hasSelection() ?
|
||||
getSelectedText(0) :
|
||||
getTextBeforeCursor(-chars * 2, 0);
|
||||
if (charsBeforeCursor != null) {
|
||||
for (int i = charsBeforeCursor.length() - 1; i >= 0 && chars < 0; i--, chars++, steps--) {
|
||||
if (Character.isSurrogate(charsBeforeCursor.charAt(i))) {
|
||||
steps--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (chars > 0) {
|
||||
CharSequence charsAfterCursor = !rightSidePointer && hasSelection() ?
|
||||
getSelectedText(0) :
|
||||
getTextAfterCursor(chars * 2, 0);
|
||||
if (charsAfterCursor != null) {
|
||||
for (int i = 0; i < charsAfterCursor.length() && chars > 0; i++, chars--, steps++) {
|
||||
if (Character.isSurrogate(charsAfterCursor.charAt(i))) {
|
||||
steps++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
// TODO
|
||||
private InputConnection getIC() {
|
||||
if (!shouldUseOtherIC) mIC = mParent.getCurrentInputConnection();
|
||||
return shouldUseOtherIC ? mOtherIC : mIC;
|
||||
}
|
||||
|
||||
// TODO
|
||||
public void setShouldUseOtherIC(boolean shouldUseOtherIC) {
|
||||
this.shouldUseOtherIC = shouldUseOtherIC;
|
||||
}
|
||||
|
||||
// TODO
|
||||
public void setOtherIC(TextView textView) {
|
||||
if (textView == null) return;
|
||||
mOtherIC = new E2EEInputConnection(textView);
|
||||
}
|
||||
}
|
@ -0,0 +1,773 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.InputMethodSubtype;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.LocaleUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.DialogUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypePreferenceUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Enrichment class for InputMethodManager to simplify interaction and add functionality.
|
||||
*/
|
||||
// non final for easy mocking.
|
||||
public class RichInputMethodManager {
|
||||
private static final String TAG = RichInputMethodManager.class.getSimpleName();
|
||||
|
||||
private RichInputMethodManager() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
private static final RichInputMethodManager sInstance = new RichInputMethodManager();
|
||||
|
||||
private InputMethodManager mImmService;
|
||||
|
||||
private SubtypeList mSubtypeList;
|
||||
|
||||
public static RichInputMethodManager getInstance() {
|
||||
sInstance.checkInitialized();
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public static void init(final Context context) {
|
||||
sInstance.initInternal(context);
|
||||
}
|
||||
|
||||
private boolean isInitialized() {
|
||||
return mImmService != null;
|
||||
}
|
||||
|
||||
private void checkInitialized() {
|
||||
if (!isInitialized()) {
|
||||
throw new RuntimeException(TAG + " is used before initialization");
|
||||
}
|
||||
}
|
||||
|
||||
private void initInternal(final Context context) {
|
||||
if (isInitialized()) {
|
||||
return;
|
||||
}
|
||||
mImmService = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
LocaleResourceUtils.init(context);
|
||||
|
||||
// Initialize the virtual subtypes
|
||||
mSubtypeList = new SubtypeList(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to be called when the virtual subtype changes.
|
||||
*
|
||||
* @param listener the listener to call when the subtype changes.
|
||||
*/
|
||||
public void setSubtypeChangeHandler(final SubtypeChangedListener listener) {
|
||||
mSubtypeList.setSubtypeChangeHandler(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to allow some code to run when the virtual subtype changes.
|
||||
*/
|
||||
public interface SubtypeChangedListener {
|
||||
void onCurrentSubtypeChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager for the list of enabled subtypes that also handles which one is currently in use.
|
||||
* Only one of these should be created to avoid conflicts.
|
||||
*/
|
||||
private static class SubtypeList {
|
||||
/**
|
||||
* The list of enabled subtypes ordered by how they should be cycled through when moving to
|
||||
* the next subtype. When a subtype is actually in use, it should be moved to the beginning
|
||||
* of the list so that the next time the user uses the switch to next subtype button, all
|
||||
* of the subtypes can be iterated through before potentially switching to a different
|
||||
* input method.
|
||||
*/
|
||||
private final List<Subtype> mSubtypes;
|
||||
/**
|
||||
* The index of the currently selected subtype. This is used for tracking the status of
|
||||
* cycling through subtypes. When actually using the keyboard, the subtype should be moved
|
||||
* to the beginning of the list, so this should normally be 0.
|
||||
*/
|
||||
private int mCurrentSubtypeIndex;
|
||||
|
||||
private final SharedPreferences mPrefs;
|
||||
private SubtypeChangedListener mSubtypeChangedListener;
|
||||
|
||||
/**
|
||||
* Create the manager for the virtual subtypes.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
*/
|
||||
public SubtypeList(final Context context) {
|
||||
mPrefs = PreferenceManagerCompat.getDeviceSharedPreferences(context);
|
||||
|
||||
final String prefSubtypes = Settings.readPrefSubtypes(mPrefs);
|
||||
final List<Subtype> subtypes = SubtypePreferenceUtils.createSubtypesFromPref(
|
||||
prefSubtypes, context.getResources());
|
||||
if (subtypes == null || subtypes.size() < 1) {
|
||||
mSubtypes = SubtypeLocaleUtils.getDefaultSubtypes(context.getResources());
|
||||
} else {
|
||||
mSubtypes = subtypes;
|
||||
}
|
||||
mCurrentSubtypeIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to be called when the virtual subtype changes.
|
||||
*
|
||||
* @param listener the listener to call when the subtype changes.
|
||||
*/
|
||||
public void setSubtypeChangeHandler(final SubtypeChangedListener listener) {
|
||||
mSubtypeChangedListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the subtype changed handler to indicate that the virtual subtype has changed.
|
||||
*/
|
||||
public void notifySubtypeChanged() {
|
||||
if (mSubtypeChangedListener != null) {
|
||||
mSubtypeChangedListener.onCurrentSubtypeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled languages.
|
||||
*
|
||||
* @return the enabled languages.
|
||||
*/
|
||||
public synchronized Set<Locale> getAllLocales() {
|
||||
final Set<Locale> locales = new HashSet<>();
|
||||
for (final Subtype subtype : mSubtypes) {
|
||||
locales.add(subtype.getLocaleObject());
|
||||
}
|
||||
return locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled subtypes for language.
|
||||
*
|
||||
* @param locale filter by Locale.
|
||||
* @return the enabled subtypes.
|
||||
*/
|
||||
public synchronized Set<Subtype> getAllForLocale(final String locale) {
|
||||
final Set<Subtype> subtypes = new HashSet<>();
|
||||
for (final Subtype subtype : mSubtypes) {
|
||||
if (subtype.getLocale().equals(locale))
|
||||
subtypes.add(subtype);
|
||||
}
|
||||
return subtypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled subtypes.
|
||||
*
|
||||
* @param sortForDisplay whether the subtypes should be sorted alphabetically by the display
|
||||
* name as opposed to having no particular order.
|
||||
* @return the enabled subtypes.
|
||||
*/
|
||||
public synchronized Set<Subtype> getAll(final boolean sortForDisplay) {
|
||||
final Set<Subtype> subtypes;
|
||||
if (sortForDisplay) {
|
||||
subtypes = new TreeSet<>(new Comparator<Subtype>() {
|
||||
@Override
|
||||
public int compare(Subtype a, Subtype b) {
|
||||
if (a.equals(b)) {
|
||||
// ensure that this is consistent with equals
|
||||
return 0;
|
||||
}
|
||||
final int result = a.getName().compareToIgnoreCase(b.getName());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
// ensure that non-equal objects are distinguished to be consistent with
|
||||
// equals
|
||||
return a.hashCode() > b.hashCode() ? 1 : -1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
subtypes = new HashSet<>();
|
||||
}
|
||||
subtypes.addAll(mSubtypes);
|
||||
return subtypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of enabled subtypes.
|
||||
*
|
||||
* @return the number of enabled subtypes.
|
||||
*/
|
||||
public synchronized int size() {
|
||||
return mSubtypes.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the preference for the list of enabled subtypes.
|
||||
*/
|
||||
private void saveSubtypeListPref() {
|
||||
final String prefSubtypes = SubtypePreferenceUtils.createPrefSubtypes(mSubtypes);
|
||||
Settings.writePrefSubtypes(mPrefs, prefSubtypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subtype to the list.
|
||||
*
|
||||
* @param subtype the subtype to add.
|
||||
* @return whether the subtype was added to the list (or already existed in the list).
|
||||
*/
|
||||
public synchronized boolean addSubtype(final Subtype subtype) {
|
||||
if (mSubtypes.contains(subtype)) {
|
||||
// don't allow duplicates, but since it's already in the list this can be considered
|
||||
// successful
|
||||
return true;
|
||||
}
|
||||
if (!mSubtypes.add(subtype)) {
|
||||
return false;
|
||||
}
|
||||
saveSubtypeListPref();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a subtype from the list.
|
||||
*
|
||||
* @param subtype the subtype to remove.
|
||||
* @return whether the subtype was removed (or wasn't even in the list).
|
||||
*/
|
||||
public synchronized boolean removeSubtype(final Subtype subtype) {
|
||||
if (mSubtypes.size() == 1) {
|
||||
// there needs to be at least one subtype
|
||||
return false;
|
||||
}
|
||||
|
||||
final int index = mSubtypes.indexOf(subtype);
|
||||
if (index < 0) {
|
||||
// nothing to remove
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean subtypeChanged;
|
||||
if (mCurrentSubtypeIndex == index) {
|
||||
mCurrentSubtypeIndex = 0;
|
||||
subtypeChanged = true;
|
||||
} else {
|
||||
if (mCurrentSubtypeIndex > index) {
|
||||
// make sure the current subtype is still pointed to when the other subtype is
|
||||
// removed
|
||||
mCurrentSubtypeIndex--;
|
||||
}
|
||||
subtypeChanged = false;
|
||||
}
|
||||
|
||||
mSubtypes.remove(index);
|
||||
saveSubtypeListPref();
|
||||
if (subtypeChanged) {
|
||||
notifySubtypeChanged();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current subtype to the beginning of the list to allow the rest of the subtypes
|
||||
* to be cycled through before possibly switching to a separate input method. This should be
|
||||
* called whenever the user is done cycling through subtypes (eg: when a subtype is actually
|
||||
* used or the keyboard is closed).
|
||||
*/
|
||||
public synchronized void resetSubtypeCycleOrder() {
|
||||
if (mCurrentSubtypeIndex == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// move the current subtype to the top of the list and shift everything above it down
|
||||
Collections.rotate(mSubtypes.subList(0, mCurrentSubtypeIndex + 1), 1);
|
||||
mCurrentSubtypeIndex = 0;
|
||||
saveSubtypeListPref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current subtype to a specific subtype.
|
||||
*
|
||||
* @param subtype the subtype to set as current.
|
||||
* @return whether the current subtype was set to the requested subtype.
|
||||
*/
|
||||
public synchronized boolean setCurrentSubtype(final Subtype subtype) {
|
||||
if (getCurrentSubtype().equals(subtype)) {
|
||||
// nothing to do
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < mSubtypes.size(); i++) {
|
||||
if (mSubtypes.get(i).equals(subtype)) {
|
||||
setCurrentSubtype(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current subtype to match a specified locale.
|
||||
*
|
||||
* @param locale the locale to use.
|
||||
* @return whether the current subtype was set to the requested locale.
|
||||
*/
|
||||
public synchronized boolean setCurrentSubtype(final Locale locale) {
|
||||
final ArrayList<Locale> enabledLocales = new ArrayList<>(mSubtypes.size());
|
||||
for (final Subtype subtype : mSubtypes) {
|
||||
enabledLocales.add(subtype.getLocaleObject());
|
||||
}
|
||||
final Locale bestLocale = LocaleUtils.findBestLocale(locale, enabledLocales);
|
||||
if (bestLocale != null) {
|
||||
// get the first subtype (most recently used) with a matching locale
|
||||
for (int i = 0; i < mSubtypes.size(); i++) {
|
||||
final Subtype subtype = mSubtypes.get(i);
|
||||
if (bestLocale.equals(subtype.getLocaleObject())) {
|
||||
setCurrentSubtype(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current subtype to a specified index. This should only be used when setting the
|
||||
* subtype to something specific (not when just iterating through the subtypes).
|
||||
*
|
||||
* @param index the index of the subtype to set as current.
|
||||
*/
|
||||
private void setCurrentSubtype(final int index) {
|
||||
if (mCurrentSubtypeIndex == index) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
mCurrentSubtypeIndex = index;
|
||||
if (index != 0) {
|
||||
// since the subtype was selected directly, the cycle should be reset so switching
|
||||
// to the next subtype can iterate through all of the rest of the subtypes
|
||||
resetSubtypeCycleOrder();
|
||||
}
|
||||
notifySubtypeChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the next subtype in the list.
|
||||
*
|
||||
* @param notifyChangeOnCycle whether the subtype changed handler should be notified if the
|
||||
* end of the list is passed and the next subtype would go back to
|
||||
* the first in the list.
|
||||
* @return whether the subtype changed listener was called.
|
||||
*/
|
||||
public synchronized boolean switchToNextSubtype(final boolean notifyChangeOnCycle) {
|
||||
final int nextIndex = mCurrentSubtypeIndex + 1;
|
||||
if (nextIndex >= mSubtypes.size()) {
|
||||
mCurrentSubtypeIndex = 0;
|
||||
if (!notifyChangeOnCycle) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mCurrentSubtypeIndex = nextIndex;
|
||||
}
|
||||
notifySubtypeChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtype that is currently in use (or will be once the keyboard is opened).
|
||||
*
|
||||
* @return the current subtype.
|
||||
*/
|
||||
public synchronized Subtype getCurrentSubtype() {
|
||||
return mSubtypes.get(mCurrentSubtypeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled subtypes.
|
||||
*
|
||||
* @param sortForDisplay whether the subtypes should be sorted alphabetically by the display
|
||||
* name as opposed to having no particular order.
|
||||
* @return the enabled subtypes.
|
||||
*/
|
||||
public Set<Subtype> getEnabledSubtypes(final boolean sortForDisplay) {
|
||||
return mSubtypeList.getAll(sortForDisplay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled languages.
|
||||
*
|
||||
* @return the enabled languages.
|
||||
*/
|
||||
public Set<Locale> getEnabledLocales() {
|
||||
return mSubtypeList.getAllLocales();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the enabled subtypes for language.
|
||||
*
|
||||
* @param locale filter by Locale.
|
||||
* @return the enabled subtypes.
|
||||
*/
|
||||
public Set<Subtype> getEnabledSubtypesForLocale(final String locale) {
|
||||
return mSubtypeList.getAllForLocale(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are multiple enabled subtypes.
|
||||
*
|
||||
* @return whether there are multiple subtypes.
|
||||
*/
|
||||
public boolean hasMultipleEnabledSubtypes() {
|
||||
return mSubtypeList.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a new subtype.
|
||||
*
|
||||
* @param subtype the subtype to add.
|
||||
* @return whether the subtype was added.
|
||||
*/
|
||||
public boolean addSubtype(final Subtype subtype) {
|
||||
return mSubtypeList.addSubtype(subtype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a subtype.
|
||||
*
|
||||
* @param subtype the subtype to remove.
|
||||
* @return whether the subtype was removed.
|
||||
*/
|
||||
public boolean removeSubtype(final Subtype subtype) {
|
||||
return mSubtypeList.removeSubtype(subtype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current subtype to the beginning of the list to allow the rest of the subtypes
|
||||
* to be cycled through before possibly switching to a separate input method.
|
||||
*/
|
||||
public void resetSubtypeCycleOrder() {
|
||||
mSubtypeList.resetSubtypeCycleOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current subtype to a specific subtype.
|
||||
*
|
||||
* @param subtype the subtype to set as current.
|
||||
* @return whether the current subtype was set to the requested subtype.
|
||||
*/
|
||||
public boolean setCurrentSubtype(final Subtype subtype) {
|
||||
return mSubtypeList.setCurrentSubtype(subtype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current subtype to match a specified locale.
|
||||
*
|
||||
* @param locale the locale to use.
|
||||
* @return whether the current subtype was set to the requested locale.
|
||||
*/
|
||||
public boolean setCurrentSubtype(final Locale locale) {
|
||||
return mSubtypeList.setCurrentSubtype(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the next subtype of this IME or optionally to another IME if all of the subtypes of
|
||||
* this IME have already been iterated through.
|
||||
*
|
||||
* @param token supplies the identifying token given to an input method when it was started,
|
||||
* which allows it to perform this operation on itself.
|
||||
* @param onlyCurrentIme whether to only switch virtual subtypes or also switch to other input
|
||||
* methods.
|
||||
* @return whether the switch was successful.
|
||||
*/
|
||||
public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
|
||||
if (onlyCurrentIme) {
|
||||
if (!hasMultipleEnabledSubtypes()) {
|
||||
return false;
|
||||
}
|
||||
return mSubtypeList.switchToNextSubtype(true);
|
||||
}
|
||||
if (mSubtypeList.switchToNextSubtype(false)) {
|
||||
return true;
|
||||
}
|
||||
// switch to a different IME
|
||||
if (mImmService.switchToNextInputMethod(token, false)) {
|
||||
return true;
|
||||
}
|
||||
if (hasMultipleEnabledSubtypes()) {
|
||||
// the virtual subtype should have been reset to the first item to prepare for switching
|
||||
// back to this IME, but we skipped notifying the change because we expected to switch
|
||||
// to a different IME, but since that failed, we just need to notify the listener
|
||||
mSubtypeList.notifySubtypeChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtype that is currently in use.
|
||||
*
|
||||
* @return the current subtype.
|
||||
*/
|
||||
public Subtype getCurrentSubtype() {
|
||||
return mSubtypeList.getCurrentSubtype();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the IME should offer ways to switch to a next input method (eg: a globe key).
|
||||
*
|
||||
* @param binder supplies the identifying token given to an input method when it was started,
|
||||
* which allows it to perform this operation on itself.
|
||||
* @return whether the IME should offer ways to switch to a next input method.
|
||||
*/
|
||||
public boolean shouldOfferSwitchingToOtherInputMethods(final IBinder binder) {
|
||||
// Use the default value instead on Jelly Bean MR2 and previous where
|
||||
// {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} isn't yet available
|
||||
// and on KitKat where the API is still just a stub to return true always.
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||
return false;
|
||||
}
|
||||
return mImmService.shouldOfferSwitchingToNextInputMethod(binder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a popup to pick the current subtype.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
* @param windowToken identifier for the window.
|
||||
* @param inputMethodService the input method service for this IME.
|
||||
* @return the dialog that was created.
|
||||
*/
|
||||
public AlertDialog showSubtypePicker(final Context context, final IBinder windowToken,
|
||||
final InputMethodService inputMethodService) {
|
||||
if (windowToken == null) {
|
||||
return null;
|
||||
}
|
||||
final CharSequence title = context.getString(R.string.change_keyboard);
|
||||
|
||||
final List<SubtypeInfo> subtypeInfoList = getEnabledSubtypeInfoOfAllImes(context);
|
||||
if (subtypeInfoList.size() < 2) {
|
||||
// if there aren't multiple options, there is no reason to show the picker
|
||||
return null;
|
||||
}
|
||||
|
||||
final CharSequence[] items = new CharSequence[subtypeInfoList.size()];
|
||||
final Subtype currentSubtype = getCurrentSubtype();
|
||||
int currentSubtypeIndex = 0;
|
||||
int i = 0;
|
||||
for (final SubtypeInfo subtypeInfo : subtypeInfoList) {
|
||||
if (subtypeInfo.virtualSubtype != null
|
||||
&& subtypeInfo.virtualSubtype.equals(currentSubtype)) {
|
||||
currentSubtypeIndex = i;
|
||||
}
|
||||
|
||||
final SpannableString itemTitle;
|
||||
final SpannableString itemSubtitle;
|
||||
if (!TextUtils.isEmpty(subtypeInfo.subtypeName)) {
|
||||
itemTitle = new SpannableString(subtypeInfo.subtypeName);
|
||||
itemSubtitle = new SpannableString("\n" + subtypeInfo.imeName);
|
||||
} else {
|
||||
itemTitle = new SpannableString(subtypeInfo.imeName);
|
||||
itemSubtitle = new SpannableString("");
|
||||
}
|
||||
itemTitle.setSpan(new RelativeSizeSpan(0.9f), 0, itemTitle.length(),
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
itemSubtitle.setSpan(new RelativeSizeSpan(0.85f), 0, itemSubtitle.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
|
||||
items[i++] = new SpannableStringBuilder().append(itemTitle).append(itemSubtitle);
|
||||
}
|
||||
final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface di, int position) {
|
||||
di.dismiss();
|
||||
int i = 0;
|
||||
for (final SubtypeInfo subtypeInfo : subtypeInfoList) {
|
||||
if (i == position) {
|
||||
if (subtypeInfo.virtualSubtype != null) {
|
||||
setCurrentSubtype(subtypeInfo.virtualSubtype);
|
||||
} else {
|
||||
switchToTargetIme(subtypeInfo.imiId, subtypeInfo.systemSubtype,
|
||||
inputMethodService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
};
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(
|
||||
DialogUtils.getPlatformDialogThemeContext(context));
|
||||
builder.setSingleChoiceItems(items, currentSubtypeIndex, listener).setTitle(title);
|
||||
final AlertDialog dialog = builder.create();
|
||||
dialog.setCancelable(true);
|
||||
dialog.setCanceledOnTouchOutside(true);
|
||||
|
||||
final Window window = dialog.getWindow();
|
||||
final WindowManager.LayoutParams lp = window.getAttributes();
|
||||
lp.token = windowToken;
|
||||
lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
|
||||
window.setAttributes(lp);
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
|
||||
|
||||
dialog.show();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info for all of virtual subtypes of this IME and system subtypes of all other IMEs.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
* @return a list with info for all of the subtypes.
|
||||
*/
|
||||
private List<SubtypeInfo> getEnabledSubtypeInfoOfAllImes(final Context context) {
|
||||
final List<SubtypeInfo> subtypeInfoList = new ArrayList<>();
|
||||
final PackageManager packageManager = context.getPackageManager();
|
||||
|
||||
final Set<InputMethodInfo> imiList = new TreeSet<>(new Comparator<InputMethodInfo>() {
|
||||
@Override
|
||||
public int compare(InputMethodInfo a, InputMethodInfo b) {
|
||||
if (a.equals(b)) {
|
||||
// ensure that this is consistent with equals
|
||||
return 0;
|
||||
}
|
||||
final String labelA = a.loadLabel(packageManager).toString();
|
||||
final String labelB = b.loadLabel(packageManager).toString();
|
||||
final int result = labelA.compareToIgnoreCase(labelB);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
// ensure that non-equal objects are distinguished to be consistent with
|
||||
// equals
|
||||
return a.hashCode() > b.hashCode() ? 1 : -1;
|
||||
}
|
||||
});
|
||||
imiList.addAll(mImmService.getEnabledInputMethodList());
|
||||
|
||||
for (final InputMethodInfo imi : imiList) {
|
||||
final CharSequence imeName = imi.loadLabel(packageManager);
|
||||
final String imiId = imi.getId();
|
||||
final String packageName = imi.getPackageName();
|
||||
|
||||
if (packageName.equals(context.getPackageName())) {
|
||||
for (final Subtype subtype : getEnabledSubtypes(true)) {
|
||||
final SubtypeInfo subtypeInfo = new SubtypeInfo();
|
||||
subtypeInfo.virtualSubtype = subtype;
|
||||
subtypeInfo.subtypeName = subtype.getName();
|
||||
subtypeInfo.imeName = imeName;
|
||||
subtypeInfo.imiId = imiId;
|
||||
subtypeInfoList.add(subtypeInfo);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final List<InputMethodSubtype> subtypes =
|
||||
mImmService.getEnabledInputMethodSubtypeList(imi, true);
|
||||
// IMEs that have no subtypes should still be returned
|
||||
if (subtypes.isEmpty()) {
|
||||
final SubtypeInfo subtypeInfo = new SubtypeInfo();
|
||||
subtypeInfo.imeName = imeName;
|
||||
subtypeInfo.imiId = imiId;
|
||||
subtypeInfoList.add(subtypeInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
final ApplicationInfo applicationInfo = imi.getServiceInfo().applicationInfo;
|
||||
for (final InputMethodSubtype subtype : subtypes) {
|
||||
if (subtype.isAuxiliary()) {
|
||||
continue;
|
||||
}
|
||||
final SubtypeInfo subtypeInfo = new SubtypeInfo();
|
||||
subtypeInfo.systemSubtype = subtype;
|
||||
if (!subtype.overridesImplicitlyEnabledSubtype()) {
|
||||
subtypeInfo.subtypeName = subtype.getDisplayName(context, packageName,
|
||||
applicationInfo);
|
||||
}
|
||||
subtypeInfo.imeName = imeName;
|
||||
subtypeInfo.imiId = imiId;
|
||||
subtypeInfoList.add(subtypeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return subtypeInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Info for a virtual or system subtype.
|
||||
*/
|
||||
private static class SubtypeInfo {
|
||||
public InputMethodSubtype systemSubtype;
|
||||
public Subtype virtualSubtype;
|
||||
public CharSequence subtypeName;
|
||||
public CharSequence imeName;
|
||||
public String imiId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different input method.
|
||||
*
|
||||
* @param imiId the ID for the input method to be switched to.
|
||||
* @param subtype the subtype for the input method to be switched to.
|
||||
* @param context the input method service for this IME.
|
||||
*/
|
||||
private void switchToTargetIme(final String imiId, final InputMethodSubtype subtype,
|
||||
final InputMethodService context) {
|
||||
final IBinder token = context.getWindow().getWindow().getAttributes().token;
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
final InputMethodManager imm = mImmService;
|
||||
Executors.newSingleThreadExecutor().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
imm.setInputMethodAndSubtype(token, imiId, subtype);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.LocaleUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A keyboard layout for a locale.
|
||||
*/
|
||||
public final class Subtype {
|
||||
private static final int NO_RESOURCE = 0;
|
||||
|
||||
private final String mLocale;
|
||||
private final String mLayoutSet;
|
||||
private final int mLayoutNameRes;
|
||||
private final String mLayoutNameStr;
|
||||
private final boolean mShowLayoutInName;
|
||||
private final Resources mResources;
|
||||
|
||||
/**
|
||||
* Create a subtype.
|
||||
*
|
||||
* @param locale the locale for the layout in the format of "ll_cc_variant" where "ll" is a
|
||||
* language code, "cc" is a country code.
|
||||
* @param layoutSet the keyboard layout set name.
|
||||
* @param layoutNameRes the keyboard layout name resource ID to use for display instead of the
|
||||
* name of the language.
|
||||
* @param showLayoutInName flag to indicate if the display name of the keyboard layout should be
|
||||
* used in the main display name of the subtype
|
||||
* (eg: "English (US) (QWERTY)" vs "English (US)").
|
||||
* @param resources the resources to use.
|
||||
*/
|
||||
public Subtype(final String locale, final String layoutSet, final int layoutNameRes,
|
||||
final boolean showLayoutInName, final Resources resources) {
|
||||
mLocale = locale;
|
||||
mLayoutSet = layoutSet;
|
||||
mLayoutNameRes = layoutNameRes;
|
||||
mLayoutNameStr = null;
|
||||
mShowLayoutInName = showLayoutInName;
|
||||
mResources = resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subtype.
|
||||
*
|
||||
* @param locale the locale for the layout in the format of "ll_cc_variant" where "ll" is a
|
||||
* language code, "cc" is a country code.
|
||||
* @param layoutSet the keyboard layout set name.
|
||||
* @param layoutNameStr the keyboard layout name string to use for display instead of the name
|
||||
* of the language.
|
||||
* @param showLayoutInName flag to indicate if the display name of the keyboard layout should be
|
||||
* used in the main display name of the subtype
|
||||
* (eg: "English (US) (QWERTY)" vs "English (US)").
|
||||
* @param resources the resources to use.
|
||||
*/
|
||||
public Subtype(final String locale, final String layoutSet, final String layoutNameStr,
|
||||
final boolean showLayoutInName, final Resources resources) {
|
||||
mLocale = locale;
|
||||
mLayoutSet = layoutSet;
|
||||
mLayoutNameRes = NO_RESOURCE;
|
||||
mLayoutNameStr = layoutNameStr;
|
||||
mShowLayoutInName = showLayoutInName;
|
||||
mResources = resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale string.
|
||||
*
|
||||
* @return the locale string.
|
||||
*/
|
||||
public String getLocale() {
|
||||
return mLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale object.
|
||||
*
|
||||
* @return the locale object.
|
||||
*/
|
||||
public Locale getLocaleObject() {
|
||||
return LocaleUtils.constructLocaleFromString(mLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for the subtype. This should be something like "English (US)" or
|
||||
* "English (US) (QWERTY)".
|
||||
*
|
||||
* @return the display name.
|
||||
*/
|
||||
public String getName() {
|
||||
final String localeDisplayName =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(mLocale);
|
||||
if (mShowLayoutInName) {
|
||||
if (mLayoutNameRes != NO_RESOURCE) {
|
||||
return mResources.getString(R.string.subtype_generic_layout, localeDisplayName,
|
||||
mResources.getString(mLayoutNameRes));
|
||||
}
|
||||
if (mLayoutNameStr != null) {
|
||||
return mResources.getString(R.string.subtype_generic_layout, localeDisplayName,
|
||||
mLayoutNameStr);
|
||||
}
|
||||
}
|
||||
return localeDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keyboard layout set name (internal).
|
||||
*
|
||||
* @return the keyboard layout set name.
|
||||
*/
|
||||
public String getKeyboardLayoutSet() {
|
||||
return mLayoutSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for the keyboard layout. This should be something like "QWERTY".
|
||||
*
|
||||
* @return the display name for the keyboard layout.
|
||||
*/
|
||||
public String getLayoutDisplayName() {
|
||||
final String displayName;
|
||||
if (mLayoutNameRes != NO_RESOURCE) {
|
||||
displayName = mResources.getString(mLayoutNameRes);
|
||||
} else if (mLayoutNameStr != null) {
|
||||
displayName = mLayoutNameStr;
|
||||
} else {
|
||||
displayName = LocaleResourceUtils.getLanguageDisplayNameInSystemLocale(mLocale);
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (!(o instanceof Subtype)) {
|
||||
return false;
|
||||
}
|
||||
final Subtype other = (Subtype) o;
|
||||
return mLocale.equals(other.mLocale) && mLayoutSet.equals(other.mLayoutSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = 31 + mLocale.hashCode();
|
||||
hashCode = hashCode * 31 + mLayoutSet.hashCode();
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "subtype " + mLocale + ":" + mLayoutSet;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.latin;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardLayoutSet;
|
||||
|
||||
/**
|
||||
* When the system locale has been changed, {@link Intent#ACTION_LOCALE_CHANGED} is received by
|
||||
* this receiver and the {@link KeyboardLayoutSet}'s cache is cleared.
|
||||
*/
|
||||
public final class SystemBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = SystemBroadcastReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
final String intentAction = intent.getAction();
|
||||
if (Intent.ACTION_LOCALE_CHANGED.equals(intentAction)) {
|
||||
Log.i(TAG, "System locale changed");
|
||||
KeyboardLayoutSet.onSystemLocaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Utility methods for working with collections.
|
||||
*/
|
||||
public final class CollectionUtils {
|
||||
private CollectionUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a sub-range of the given array to an ArrayList of the appropriate type.
|
||||
*
|
||||
* @param array Array to be converted.
|
||||
* @param start First index inclusive to be converted.
|
||||
* @param end Last index exclusive to be converted.
|
||||
* @throws IllegalArgumentException if start or end are out of range or start > end.
|
||||
*/
|
||||
public static <E> ArrayList<E> arrayAsList(final E[] array, final int start,
|
||||
final int end) {
|
||||
if (start < 0 || start > end || end > array.length) {
|
||||
throw new IllegalArgumentException("Invalid start: " + start + " end: " + end
|
||||
+ " with array.length: " + array.length);
|
||||
}
|
||||
|
||||
final ArrayList<E> list = new ArrayList<>(end - start);
|
||||
for (int i = start; i < end; i++) {
|
||||
list.add(array[i]);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
public static final class Color {
|
||||
/**
|
||||
* The alpha value for fully opaque.
|
||||
*/
|
||||
public final static int ALPHA_OPAQUE = 255;
|
||||
}
|
||||
|
||||
public static final class TextUtils {
|
||||
/**
|
||||
* Capitalization mode for {@link android.text.TextUtils#getCapsMode}: don't capitalize
|
||||
* characters. This value may be used with
|
||||
* {@link android.text.TextUtils#CAP_MODE_CHARACTERS},
|
||||
* {@link android.text.TextUtils#CAP_MODE_WORDS}, and
|
||||
* {@link android.text.TextUtils#CAP_MODE_SENTENCES}.
|
||||
*/
|
||||
// TODO: Straighten this out. It's bizarre to have to use android.text.TextUtils.CAP_MODE_*
|
||||
// except for OFF that is in Constants.TextUtils.
|
||||
public static final int CAP_MODE_OFF = 0;
|
||||
|
||||
private TextUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
}
|
||||
|
||||
public static final int NOT_A_CODE = -1;
|
||||
public static final int NOT_A_CURSOR_POSITION = -1;
|
||||
// TODO: replace the following constants with state in InputTransaction?
|
||||
public static final int NOT_A_COORDINATE = -1;
|
||||
|
||||
// A hint on how many characters to cache from the TextView. A good value of this is given by
|
||||
// how many characters we need to be able to almost always find the caps mode.
|
||||
public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024;
|
||||
// How many characters we accept for the recapitalization functionality. This needs to be
|
||||
// large enough for all reasonable purposes, but avoid purposeful attacks. 100k sounds about
|
||||
// right for this.
|
||||
public static final int MAX_CHARACTERS_FOR_RECAPITALIZATION = 1024 * 100;
|
||||
|
||||
public static boolean isValidCoordinate(final int coordinate) {
|
||||
// Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
|
||||
// and {@link SPELL_CHECKER_COORDINATE}.
|
||||
return coordinate >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom request code used in
|
||||
* {@link com.amnesica.kryptey.inputmethod.keyboard.KeyboardActionListener#onCustomRequest(int)}.
|
||||
*/
|
||||
// The code to show input method picker.
|
||||
public static final int CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER = 1;
|
||||
|
||||
/**
|
||||
* Some common keys code. Must be positive.
|
||||
*/
|
||||
public static final int CODE_ENTER = '\n';
|
||||
public static final int CODE_TAB = '\t';
|
||||
public static final int CODE_SPACE = ' ';
|
||||
public static final int CODE_PERIOD = '.';
|
||||
public static final int CODE_COMMA = ',';
|
||||
public static final int CODE_SINGLE_QUOTE = '\'';
|
||||
public static final int CODE_DOUBLE_QUOTE = '"';
|
||||
public static final int CODE_BACKSLASH = '\\';
|
||||
public static final int CODE_VERTICAL_BAR = '|';
|
||||
public static final int CODE_PERCENT = '%';
|
||||
public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿
|
||||
public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡
|
||||
|
||||
/**
|
||||
* Special keys code. Must be negative.
|
||||
* These should be aligned with constants in
|
||||
* {@link com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardCodesSet}.
|
||||
*/
|
||||
public static final int CODE_SHIFT = -1;
|
||||
public static final int CODE_CAPSLOCK = -2;
|
||||
public static final int CODE_SWITCH_ALPHA_SYMBOL = -3;
|
||||
public static final int CODE_OUTPUT_TEXT = -4;
|
||||
public static final int CODE_DELETE = -5;
|
||||
public static final int CODE_SETTINGS = -6;
|
||||
public static final int CODE_ACTION_NEXT = -8;
|
||||
public static final int CODE_ACTION_PREVIOUS = -9;
|
||||
public static final int CODE_LANGUAGE_SWITCH = -10;
|
||||
public static final int CODE_SHIFT_ENTER = -11;
|
||||
public static final int CODE_SYMBOL_SHIFT = -12;
|
||||
// Code value representing the code is not specified.
|
||||
public static final int CODE_UNSPECIFIED = -13;
|
||||
|
||||
public static boolean isLetterCode(final int code) {
|
||||
return code >= CODE_SPACE;
|
||||
}
|
||||
|
||||
public static String printableCode(final int code) {
|
||||
switch (code) {
|
||||
case CODE_SHIFT:
|
||||
return "shift";
|
||||
case CODE_CAPSLOCK:
|
||||
return "capslock";
|
||||
case CODE_SWITCH_ALPHA_SYMBOL:
|
||||
return "symbol";
|
||||
case CODE_OUTPUT_TEXT:
|
||||
return "text";
|
||||
case CODE_DELETE:
|
||||
return "delete";
|
||||
case CODE_SETTINGS:
|
||||
return "settings";
|
||||
case CODE_ACTION_NEXT:
|
||||
return "actionNext";
|
||||
case CODE_ACTION_PREVIOUS:
|
||||
return "actionPrevious";
|
||||
case CODE_LANGUAGE_SWITCH:
|
||||
return "languageSwitch";
|
||||
case CODE_SHIFT_ENTER:
|
||||
return "shiftEnter";
|
||||
case CODE_UNSPECIFIED:
|
||||
return "unspec";
|
||||
case CODE_TAB:
|
||||
return "tab";
|
||||
case CODE_ENTER:
|
||||
return "enter";
|
||||
case CODE_SPACE:
|
||||
return "space";
|
||||
default:
|
||||
if (code < CODE_SPACE) return String.format("\\u%02X", code);
|
||||
if (code < 0x100) return String.format("%c", code);
|
||||
if (code < 0x10000) return String.format("\\u%04X", code);
|
||||
return String.format("\\U%05X", code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen metrics (a.k.a. Device form factor) constants of
|
||||
* {@link com.amnesica.kryptey.inputmethod.R.integer#config_screen_metrics}.
|
||||
*/
|
||||
public static final int SCREEN_METRICS_LARGE_TABLET = 2;
|
||||
public static final int SCREEN_METRICS_SMALL_TABLET = 3;
|
||||
|
||||
private Constants() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
public final class CoordinateUtils {
|
||||
private static final int INDEX_X = 0;
|
||||
private static final int INDEX_Y = 1;
|
||||
private static final int ELEMENT_SIZE = INDEX_Y + 1;
|
||||
|
||||
private CoordinateUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
public static int[] newInstance() {
|
||||
return new int[ELEMENT_SIZE];
|
||||
}
|
||||
|
||||
public static int x(final int[] coords) {
|
||||
return coords[INDEX_X];
|
||||
}
|
||||
|
||||
public static int y(final int[] coords) {
|
||||
return coords[INDEX_Y];
|
||||
}
|
||||
|
||||
public static void set(final int[] coords, final int x, final int y) {
|
||||
coords[INDEX_X] = x;
|
||||
coords[INDEX_Y] = y;
|
||||
}
|
||||
|
||||
public static void copy(final int[] destination, final int[] source) {
|
||||
destination[INDEX_X] = source[INDEX_X];
|
||||
destination[INDEX_Y] = source[INDEX_Y];
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A class to help with handling Locales in string form.
|
||||
* <p>
|
||||
* This file has the same meaning and features (and shares all of its code) with the one with the
|
||||
* same name in Latin IME. They need to be kept synchronized; for any update/bugfix to
|
||||
* this file, consider also updating/fixing the version in Latin IME.
|
||||
*/
|
||||
public final class LocaleUtils {
|
||||
private LocaleUtils() {
|
||||
// Intentional empty constructor for utility class.
|
||||
}
|
||||
|
||||
// Locale match level constants.
|
||||
// A higher level of match is guaranteed to have a higher numerical value.
|
||||
// Some room is left within constants to add match cases that may arise necessary
|
||||
// in the future, for example differentiating between the case where the countries
|
||||
// are both present and different, and the case where one of the locales does not
|
||||
// specify the countries. This difference is not needed now.
|
||||
|
||||
private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a locale from a string specification.
|
||||
*
|
||||
* @param localeString a string specification of a locale, in a format of "ll_cc_variant" where
|
||||
* "ll" is a language code, "cc" is a country code.
|
||||
*/
|
||||
public static Locale constructLocaleFromString(final String localeString) {
|
||||
synchronized (sLocaleCache) {
|
||||
if (sLocaleCache.containsKey(localeString)) {
|
||||
return sLocaleCache.get(localeString);
|
||||
}
|
||||
final String[] elements = localeString.split("_", 3);
|
||||
final Locale locale;
|
||||
if (elements.length == 1) {
|
||||
locale = new Locale(elements[0] /* language */);
|
||||
} else if (elements.length == 2) {
|
||||
locale = new Locale(elements[0] /* language */, elements[1] /* country */);
|
||||
} else { // localeParams.length == 3
|
||||
locale = new Locale(elements[0] /* language */, elements[1] /* country */,
|
||||
elements[2] /* variant */);
|
||||
}
|
||||
sLocaleCache.put(localeString, locale);
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string specification for a locale.
|
||||
*
|
||||
* @param locale the locale.
|
||||
* @return a string specification of a locale, in a format of "ll_cc_variant" where "ll" is a
|
||||
* language code, "cc" is a country code.
|
||||
*/
|
||||
public static String getLocaleString(final Locale locale) {
|
||||
if (!TextUtils.isEmpty(locale.getVariant())) {
|
||||
return locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant();
|
||||
}
|
||||
if (!TextUtils.isEmpty(locale.getCountry())) {
|
||||
return locale.getLanguage() + "_" + locale.getCountry();
|
||||
}
|
||||
return locale.getLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the closest matching locale. This searches by:
|
||||
* 1. {@link Locale#equals(Object)}
|
||||
* 2. Language, Country, and Variant match
|
||||
* 3. Language and Country match
|
||||
* 4. Language matches
|
||||
*
|
||||
* @param localeToMatch the locale to match.
|
||||
* @param options a collection of locales to find the best match.
|
||||
* @return the locale from the collection that is the best match for the specified locale or
|
||||
* null if nothing matches.
|
||||
*/
|
||||
public static Locale findBestLocale(final Locale localeToMatch,
|
||||
final Collection<Locale> options) {
|
||||
// Find the best subtype based on a straightforward matching algorithm.
|
||||
// TODO: Use LocaleList#getFirstMatch() instead.
|
||||
for (final Locale locale : options) {
|
||||
if (locale.equals(localeToMatch)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
for (final Locale locale : options) {
|
||||
if (locale.getLanguage().equals(localeToMatch.getLanguage()) &&
|
||||
locale.getCountry().equals(localeToMatch.getCountry()) &&
|
||||
locale.getVariant().equals(localeToMatch.getVariant())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
for (final Locale locale : options) {
|
||||
if (locale.getLanguage().equals(localeToMatch.getLanguage()) &&
|
||||
locale.getCountry().equals(localeToMatch.getCountry())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
for (final Locale locale : options) {
|
||||
if (locale.getLanguage().equals(localeToMatch.getLanguage())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of locales enabled in the system.
|
||||
*
|
||||
* @return the list of locales enabled in the system.
|
||||
*/
|
||||
public static List<Locale> getSystemLocales() {
|
||||
ArrayList<Locale> locales = new ArrayList<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleList localeList = Resources.getSystem().getConfiguration().getLocales();
|
||||
for (int i = 0; i < localeList.size(); i++) {
|
||||
locales.add(localeList.get(i));
|
||||
}
|
||||
} else {
|
||||
locales.add(Resources.getSystem().getConfiguration().locale);
|
||||
}
|
||||
return locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for {@link Locale} to order them alphabetically
|
||||
* first.
|
||||
*/
|
||||
public static class LocaleComparator implements Comparator<Locale> {
|
||||
@Override
|
||||
public int compare(Locale a, Locale b) {
|
||||
if (a.equals(b)) {
|
||||
// ensure that this is consistent with equals
|
||||
return 0;
|
||||
}
|
||||
final String aDisplay =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(getLocaleString(a));
|
||||
final String bDisplay =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(getLocaleString(b));
|
||||
final int result = aDisplay.compareToIgnoreCase(bDisplay);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
// ensure that non-equal objects are distinguished to be consistent with equals
|
||||
return a.hashCode() > b.hashCode() ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class StringUtils {
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
private StringUtils() {
|
||||
// This utility class is not publicly instantiable.
|
||||
}
|
||||
|
||||
public static int codePointCount(final CharSequence text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return 0;
|
||||
}
|
||||
return Character.codePointCount(text, 0, text.length());
|
||||
}
|
||||
|
||||
public static String newSingleCodePointString(final int codePoint) {
|
||||
if (Character.charCount(codePoint) == 1) {
|
||||
// Optimization: avoid creating a temporary array for characters that are
|
||||
// represented by a single char value
|
||||
return String.valueOf((char) codePoint);
|
||||
}
|
||||
// For surrogate pair
|
||||
return new String(Character.toChars(codePoint));
|
||||
}
|
||||
|
||||
public static boolean containsInArray(final String text,
|
||||
final String[] array) {
|
||||
for (final String element : array) {
|
||||
if (text.equals(element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
|
||||
* Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
|
||||
* a comma character in it.
|
||||
*/
|
||||
private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
|
||||
|
||||
public static boolean containsInCommaSplittableText(final String text,
|
||||
final String extraValues) {
|
||||
if (TextUtils.isEmpty(extraValues)) {
|
||||
return false;
|
||||
}
|
||||
return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
|
||||
}
|
||||
|
||||
public static String removeFromCommaSplittableTextIfExists(final String text,
|
||||
final String extraValues) {
|
||||
if (TextUtils.isEmpty(extraValues)) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
|
||||
if (!containsInArray(text, elements)) {
|
||||
return extraValues;
|
||||
}
|
||||
final ArrayList<String> result = new ArrayList<>(elements.length - 1);
|
||||
for (final String element : elements) {
|
||||
if (!text.equals(element)) {
|
||||
result.add(element);
|
||||
}
|
||||
}
|
||||
return TextUtils.join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
|
||||
}
|
||||
|
||||
public static String capitalizeFirstCodePoint(final String s,
|
||||
final Locale locale) {
|
||||
if (s.length() <= 1) {
|
||||
return s.toUpperCase(getLocaleUsedForToTitleCase(locale));
|
||||
}
|
||||
// Please refer to the comment below in
|
||||
// {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
|
||||
final int cutoff = s.offsetByCodePoints(0, 1);
|
||||
return s.substring(0, cutoff).toUpperCase(getLocaleUsedForToTitleCase(locale))
|
||||
+ s.substring(cutoff);
|
||||
}
|
||||
|
||||
public static int[] toCodePointArray(final CharSequence charSequence) {
|
||||
return toCodePointArray(charSequence, 0, charSequence.length());
|
||||
}
|
||||
|
||||
private static final int[] EMPTY_CODEPOINTS = {};
|
||||
|
||||
/**
|
||||
* Converts a range of a string to an array of code points.
|
||||
*
|
||||
* @param charSequence the source string.
|
||||
* @param startIndex the start index inside the string in java chars, inclusive.
|
||||
* @param endIndex the end index inside the string in java chars, exclusive.
|
||||
* @return a new array of code points. At most endIndex - startIndex, but possibly less.
|
||||
*/
|
||||
public static int[] toCodePointArray(final CharSequence charSequence,
|
||||
final int startIndex, final int endIndex) {
|
||||
final int length = charSequence.length();
|
||||
if (length <= 0) {
|
||||
return EMPTY_CODEPOINTS;
|
||||
}
|
||||
final int[] codePoints =
|
||||
new int[Character.codePointCount(charSequence, startIndex, endIndex)];
|
||||
copyCodePointsAndReturnCodePointCount(codePoints, charSequence, startIndex, endIndex,
|
||||
false /* downCase */);
|
||||
return codePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the codepoints in a CharSequence to an int array.
|
||||
* <p>
|
||||
* This method assumes there is enough space in the array to store the code points. The size
|
||||
* can be measured with Character#codePointCount(CharSequence, int, int) before passing to this
|
||||
* method. If the int array is too small, an ArrayIndexOutOfBoundsException will be thrown.
|
||||
* Also, this method makes no effort to be thread-safe. Do not modify the CharSequence while
|
||||
* this method is running, or the behavior is undefined.
|
||||
* This method can optionally downcase code points before copying them, but it pays no attention
|
||||
* to locale while doing so.
|
||||
*
|
||||
* @param destination the int array.
|
||||
* @param charSequence the CharSequence.
|
||||
* @param startIndex the start index inside the string in java chars, inclusive.
|
||||
* @param endIndex the end index inside the string in java chars, exclusive.
|
||||
* @param downCase if this is true, code points will be downcased before being copied.
|
||||
* @return the number of copied code points.
|
||||
*/
|
||||
public static int copyCodePointsAndReturnCodePointCount(final int[] destination,
|
||||
final CharSequence charSequence, final int startIndex, final int endIndex,
|
||||
final boolean downCase) {
|
||||
int destIndex = 0;
|
||||
for (int index = startIndex; index < endIndex;
|
||||
index = Character.offsetByCodePoints(charSequence, index, 1)) {
|
||||
final int codePoint = Character.codePointAt(charSequence, index);
|
||||
// TODO: stop using this, as it's not aware of the locale and does not always do
|
||||
// the right thing.
|
||||
destination[destIndex] = downCase ? Character.toLowerCase(codePoint) : codePoint;
|
||||
destIndex++;
|
||||
}
|
||||
return destIndex;
|
||||
}
|
||||
|
||||
public static int[] toSortedCodePointArray(final String string) {
|
||||
final int[] codePoints = toCodePointArray(string);
|
||||
Arrays.sort(codePoints);
|
||||
return codePoints;
|
||||
}
|
||||
|
||||
public static boolean isIdenticalAfterUpcase(final String text) {
|
||||
final int length = text.length();
|
||||
int i = 0;
|
||||
while (i < length) {
|
||||
final int codePoint = text.codePointAt(i);
|
||||
if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
|
||||
return false;
|
||||
}
|
||||
i += Character.charCount(codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isIdenticalAfterDowncase(final String text) {
|
||||
final int length = text.length();
|
||||
int i = 0;
|
||||
while (i < length) {
|
||||
final int codePoint = text.codePointAt(i);
|
||||
if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
|
||||
return false;
|
||||
}
|
||||
i += Character.charCount(codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isIdenticalAfterCapitalizeEachWord(final String text) {
|
||||
boolean needsCapsNext = true;
|
||||
final int len = text.length();
|
||||
for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
|
||||
final int codePoint = text.codePointAt(i);
|
||||
if (Character.isLetter(codePoint)) {
|
||||
if ((needsCapsNext && !Character.isUpperCase(codePoint))
|
||||
|| (!needsCapsNext && !Character.isLowerCase(codePoint))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// We need a capital letter next if this is a whitespace.
|
||||
needsCapsNext = Character.isWhitespace(codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
|
||||
// which should be capitalized together in *some* cases.
|
||||
public static String capitalizeEachWord(final String text, final Locale locale) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
boolean needsCapsNext = true;
|
||||
final int len = text.length();
|
||||
for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
|
||||
final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
|
||||
if (needsCapsNext) {
|
||||
builder.append(nextChar.toUpperCase(locale));
|
||||
} else {
|
||||
builder.append(nextChar.toLowerCase(locale));
|
||||
}
|
||||
// We need a capital letter next if this is a whitespace.
|
||||
needsCapsNext = Character.isWhitespace(nextChar.codePointAt(0));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final String LANGUAGE_GREEK = "el";
|
||||
|
||||
private static Locale getLocaleUsedForToTitleCase(final Locale locale) {
|
||||
// In Greek locale {@link String#toUpperCase(Locale)} eliminates accents from its result.
|
||||
// In order to get accented upper case letter, {@link Locale#ROOT} should be used.
|
||||
if (LANGUAGE_GREEK.equals(locale.getLanguage())) {
|
||||
return Locale.ROOT;
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
public static String toTitleCaseOfKeyLabel(final String label,
|
||||
final Locale locale) {
|
||||
if (label == null) {
|
||||
return label;
|
||||
}
|
||||
return label.toUpperCase(getLocaleUsedForToTitleCase(locale));
|
||||
}
|
||||
|
||||
public static int toTitleCaseOfKeyCode(final int code, final Locale locale) {
|
||||
if (!Constants.isLetterCode(code)) {
|
||||
return code;
|
||||
}
|
||||
final String label = newSingleCodePointString(code);
|
||||
final String titleCaseLabel = toTitleCaseOfKeyLabel(label, locale);
|
||||
return codePointCount(titleCaseLabel) == 1
|
||||
? titleCaseLabel.codePointAt(0) : Constants.CODE_UNSPECIFIED;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.latin.common;
|
||||
|
||||
/**
|
||||
* Emojis are supplementary characters expressed as a low+high pair. For instance,
|
||||
* the emoji U+1F625 is encoded as "\uD83D\uDE25" in UTF-16, where '\uD83D' is in
|
||||
* the range of [0xd800, 0xdbff] and '\uDE25' is in the range of [0xdc00, 0xdfff].
|
||||
* {@see http://docs.oracle.com/javase/6/docs/api/java/lang/Character.html#unicode}
|
||||
*/
|
||||
public final class UnicodeSurrogate {
|
||||
private static final char LOW_SURROGATE_MIN = '\uD800';
|
||||
private static final char LOW_SURROGATE_MAX = '\uDBFF';
|
||||
private static final char HIGH_SURROGATE_MIN = '\uDC00';
|
||||
private static final char HIGH_SURROGATE_MAX = '\uDFFF';
|
||||
|
||||
public static boolean isLowSurrogate(final char c) {
|
||||
return c >= LOW_SURROGATE_MIN && c <= LOW_SURROGATE_MAX;
|
||||
}
|
||||
|
||||
public static boolean isHighSurrogate(final char c) {
|
||||
return c >= HIGH_SURROGATE_MIN && c <= HIGH_SURROGATE_MAX;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.latin.define;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public final class DebugFlags {
|
||||
public static final boolean DEBUG_ENABLED = false;
|
||||
|
||||
private DebugFlags() {
|
||||
// This class is not publicly instantiable.
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static void init(final SharedPreferences prefs) {
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin.e2ee;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.latin.e2ee.util.HTMLHelper;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.MessageEnvelope;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.MessageType;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.SignalProtocolMain;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.Contact;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.StorageMessage;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.encoding.EncodeHelper;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.encoding.Encoder;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.encoding.FairyTaleEncoder;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.encoding.RawEncoder;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.DuplicateContactException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.InvalidContactException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.TooManyCharsException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.UnknownContactException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.UnknownMessageException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.util.JsonUtil;
|
||||
|
||||
import org.signal.libsignal.protocol.DuplicateMessageException;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidKeyIdException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.InvalidVersionException;
|
||||
import org.signal.libsignal.protocol.LegacyMessageException;
|
||||
import org.signal.libsignal.protocol.NoSessionException;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.UntrustedIdentityException;
|
||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class E2EEStrip {
|
||||
private static final String TAG = E2EEStrip.class.getSimpleName();
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final String INFO_CONTACT_ALREADY_EXISTS = "Contact already exists and was not saved";
|
||||
private final String INFO_CONTACT_INVALID = "Contact is invalid and was not saved";
|
||||
private final String INFO_SESSION_CREATION_FAILED = "Session creation failed. If possible delete sender in contact list and ask for a new keybundle";
|
||||
|
||||
private final int CHAR_THRESHOLD_RAW = 500;
|
||||
private final int CHAR_THRESHOLD_FAIRYTALE = 500;
|
||||
|
||||
public E2EEStrip(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
CharSequence encryptMessage(final String unencryptedMessage, final SignalProtocolAddress signalProtocolAddress, Encoder encoder) throws IOException {
|
||||
checkMessageLengthForEncodingMethod(unencryptedMessage, encoder, false);
|
||||
final MessageEnvelope messageEnvelope = SignalProtocolMain.encryptMessage(unencryptedMessage, signalProtocolAddress);
|
||||
String json = JsonUtil.toJson(messageEnvelope);
|
||||
if (json == null) return null;
|
||||
return encode(json, encoder);
|
||||
}
|
||||
|
||||
CharSequence decryptMessage(final MessageEnvelope messageEnvelope, final Contact sender) {
|
||||
CharSequence decryptedMessage = null;
|
||||
try {
|
||||
updateSessionWithNewSignedPreKeyIfNecessary(messageEnvelope, sender);
|
||||
|
||||
decryptedMessage = SignalProtocolMain.decryptMessage(messageEnvelope, sender.getSignalProtocolAddress());
|
||||
} catch (InvalidMessageException | NoSessionException | InvalidContactException |
|
||||
UnknownMessageException |
|
||||
UntrustedIdentityException | DuplicateMessageException | InvalidVersionException |
|
||||
InvalidKeyIdException |
|
||||
LegacyMessageException | InvalidKeyException e) {
|
||||
Log.e(TAG, "Error: Decrypting message failed");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return decryptedMessage;
|
||||
}
|
||||
|
||||
public String encode(final String message, final Encoder encoder) throws IOException {
|
||||
String encodedMessage = null;
|
||||
if (encoder.equals(Encoder.FAIRYTALE))
|
||||
encodedMessage = FairyTaleEncoder.encode(message, mContext);
|
||||
if (encoder.equals(Encoder.RAW)) encodedMessage = RawEncoder.encode(message);
|
||||
return encodedMessage;
|
||||
}
|
||||
|
||||
private void updateSessionWithNewSignedPreKeyIfNecessary(MessageEnvelope messageEnvelope, Contact sender) {
|
||||
if (messageEnvelope.getPreKeyResponse() != null && messageEnvelope.getCiphertextMessage() != null) {
|
||||
SignalProtocolMain.processPreKeyResponseMessage(messageEnvelope, sender.getSignalProtocolAddress());
|
||||
}
|
||||
}
|
||||
|
||||
CharSequence getEncryptedMessageFromClipboard() {
|
||||
ClipboardManager clipboardManager =
|
||||
(ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboardManager != null) {
|
||||
try {
|
||||
// hint: listener for HTML text needed for using app with telegram
|
||||
if (clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
|
||||
clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
|
||||
ClipData.Item item = clipboardManager.getPrimaryClip().getItemAt(0);
|
||||
return HTMLHelper.replaceHtmlCharacters(item.getText().toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error: Getting clipboard message!");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void clearClipboard() {
|
||||
ClipboardManager clipboardManager =
|
||||
(ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboardManager != null) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
clipboardManager.clearPrimaryClip();
|
||||
// debug Toast.makeText(mContext, "Clipboard deleted!", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// support for older devices
|
||||
ClipData clipData = ClipData.newPlainText("", "");
|
||||
clipboardManager.setPrimaryClip(clipData);
|
||||
// debug Toast.makeText(mContext, "Clipboard deleted!", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error: Clearing clipboard message!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<Contact> getContacts() {
|
||||
return SignalProtocolMain.getContactList();
|
||||
}
|
||||
|
||||
public Contact createAndAddContactToContacts(final CharSequence firstName, final CharSequence lastName, final String signalProtocolAddressName, final int deviceId) {
|
||||
Contact contact = null;
|
||||
try {
|
||||
contact = SignalProtocolMain.addContact(firstName, lastName, signalProtocolAddressName, deviceId);
|
||||
} catch (DuplicateContactException e) {
|
||||
Toast.makeText(mContext, INFO_CONTACT_ALREADY_EXISTS, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
} catch (InvalidContactException e) {
|
||||
Toast.makeText(mContext, INFO_CONTACT_INVALID, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
return contact;
|
||||
}
|
||||
|
||||
public boolean createSessionWithContact(Contact chosenContact, MessageEnvelope messageEnvelope, SignalProtocolAddress recipientProtocolAddress) {
|
||||
boolean successful = SignalProtocolMain.processPreKeyResponseMessage(messageEnvelope, recipientProtocolAddress);
|
||||
if (successful) {
|
||||
Toast.makeText(mContext, "Session with " + chosenContact.getFirstName() + " " + chosenContact.getLastName() + " created", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(mContext, INFO_SESSION_CREATION_FAILED, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return successful;
|
||||
}
|
||||
|
||||
public String getPreKeyResponseMessage() {
|
||||
final MessageEnvelope messageEnvelope = SignalProtocolMain.getPreKeyResponseMessage();
|
||||
return JsonUtil.toJson(messageEnvelope);
|
||||
}
|
||||
|
||||
public Object getContactFromEnvelope(MessageEnvelope messageEnvelope) {
|
||||
return SignalProtocolMain.extractContactFromMessageEnvelope(messageEnvelope);
|
||||
}
|
||||
|
||||
public MessageType getMessageType(MessageEnvelope messageEnvelope) {
|
||||
return SignalProtocolMain.getMessageType(messageEnvelope);
|
||||
}
|
||||
|
||||
public void removeContact(Contact contact) {
|
||||
SignalProtocolMain.removeContactFromContactListAndProtocol(contact);
|
||||
}
|
||||
|
||||
public List<StorageMessage> getUnencryptedMessages(Contact contact) throws UnknownContactException {
|
||||
return SignalProtocolMain.getUnencryptedMessagesList(contact);
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return SignalProtocolMain.getNameOfAccount();
|
||||
}
|
||||
|
||||
public Fingerprint getFingerprint(Contact contact) {
|
||||
return SignalProtocolMain.getFingerprint(contact);
|
||||
}
|
||||
|
||||
public void verifyContact(Contact contact) throws UnknownContactException {
|
||||
SignalProtocolMain.verifyContact(contact);
|
||||
}
|
||||
|
||||
public String decodeMessage(String encodedMessage) throws IOException {
|
||||
if (EncodeHelper.encodedTextContainsInvisibleCharacters(encodedMessage)) {
|
||||
return FairyTaleEncoder.decode(encodedMessage);
|
||||
} else {
|
||||
return RawEncoder.decode(encodedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkMessageLengthForEncodingMethod(String message, Encoder encodingMethod, boolean isPreKeyResponse) throws TooManyCharsException {
|
||||
if (message == null || encodingMethod == null) return;
|
||||
final int messageBytes = message.getBytes(StandardCharsets.UTF_8).length;
|
||||
if (isPreKeyResponse && messageBytes > CHAR_THRESHOLD_RAW)
|
||||
throw new TooManyCharsException(String.format("Too many characters for invite or update message (%s characters, only %s characters allowed)", messageBytes, CHAR_THRESHOLD_RAW));
|
||||
if (encodingMethod.equals(Encoder.RAW) && messageBytes > CHAR_THRESHOLD_RAW) {
|
||||
throw new TooManyCharsException(String.format("Too many characters for raw message (%s characters, only %s characters allowed)", messageBytes, CHAR_THRESHOLD_RAW));
|
||||
} else if (encodingMethod.equals(Encoder.FAIRYTALE) && messageBytes > CHAR_THRESHOLD_FAIRYTALE) {
|
||||
throw new TooManyCharsException(String.format("Too many characters for fairytale message (%s characters, only %s characters allowed)", messageBytes, CHAR_THRESHOLD_FAIRYTALE));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,991 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin.e2ee;
|
||||
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.BuildConfig;
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.MainKeyboardView;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputConnection;
|
||||
import com.amnesica.kryptey.inputmethod.latin.e2ee.adapter.ListAdapterContacts;
|
||||
import com.amnesica.kryptey.inputmethod.latin.e2ee.adapter.ListAdapterMessages;
|
||||
import com.amnesica.kryptey.inputmethod.latin.e2ee.util.HTMLHelper;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.MessageEnvelope;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.MessageType;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.Contact;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.StorageMessage;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.encoding.Encoder;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.TooManyCharsException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.exceptions.UnknownContactException;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.util.JsonUtil;
|
||||
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class E2EEStripView extends RelativeLayout implements ListAdapterContacts.ListAdapterContactInterface {
|
||||
|
||||
private static final String TAG = E2EEStripView.class.getSimpleName();
|
||||
|
||||
MainKeyboardView mMainKeyboardView;
|
||||
E2EEStrip mE2EEStrip;
|
||||
Listener mListener;
|
||||
|
||||
private E2EEStripVisibilityGroup mE2EEStripVisibilityGroup;
|
||||
private ViewGroup mE2EEMainStrip;
|
||||
|
||||
private RichInputConnection mRichInputConnection;
|
||||
|
||||
// main view
|
||||
private LinearLayout mLayoutE2EEMainView;
|
||||
private ImageButton mEncryptButton;
|
||||
private ImageButton mDecryptButton;
|
||||
private ImageButton mRecipientButton;
|
||||
private ImageButton mChatLogsButton;
|
||||
private ImageButton mShowHelpButton;
|
||||
private TextView mInfoTextView;
|
||||
private EditText mInputEditText;
|
||||
private ImageButton mClearUserInputButton;
|
||||
private ImageButton mSelectEncodingFairyTaleButton;
|
||||
private ImageButton mSelectEncodingRawButton;
|
||||
|
||||
// add contact view
|
||||
private LinearLayout mLayoutE2EEAddContactView;
|
||||
private TextView mAddContactInfoTextView;
|
||||
private EditText mAddContactFirstNameInputEditText;
|
||||
private EditText mAddContactLastNameInputEditText;
|
||||
private ImageButton mAddContactCancelButton;
|
||||
private ImageButton mAddContactAddButton;
|
||||
|
||||
// contact list view
|
||||
private LinearLayout mLayoutE2EEContactListView;
|
||||
private TextView mContactListInfoTextView;
|
||||
private ListView mContactList;
|
||||
private ImageButton mContactListReturnButton;
|
||||
private ImageButton mContactListInviteButton; // send pre key response message
|
||||
|
||||
// messages view
|
||||
private LinearLayout mLayoutE2EEMessagesListView;
|
||||
private TextView mMessagesListInfoTextView;
|
||||
private ListView mMessagesList;
|
||||
private ImageButton mMessagesListReturnButton;
|
||||
|
||||
// help view
|
||||
private LinearLayout mLayoutE2EEHelpView;
|
||||
private TextView mHelpInfoTextView;
|
||||
private TextView mHelpViewTextView;
|
||||
private ImageButton mHelpViewReturnButton;
|
||||
private TextView mHelpVersionTextView;
|
||||
|
||||
// verify contact view
|
||||
private LinearLayout mLayoutE2EEVerifyContactView;
|
||||
private TextView mVerifyContactInfoTextView;
|
||||
private TableLayout mVerifyContactTableView;
|
||||
private ImageButton mVerifyContactReturnButton;
|
||||
private ImageButton mVerifyContactVerifyButton;
|
||||
private TextView[] mCodes = new TextView[12];
|
||||
|
||||
private Contact chosenContact;
|
||||
|
||||
private Encoder encodingMethod = Encoder.RAW; // raw is default
|
||||
|
||||
// info texts
|
||||
private final String INFO_NO_CONTACT_CHOSEN = "No contact chosen";
|
||||
private final String INFO_PRE_KEY_DETECTED = "Keybundle detected: click on decrypt to save the content";
|
||||
private final String INFO_SIGNAL_MESSAGE_DETECTED = "Encrypted message detected: click on decrypt to view message";
|
||||
private final String INFO_PRE_KEY_AND_SIGNAL_MESSAGE_DETECTED = "Encrypted update message detected: click on decrypt to view message";
|
||||
private final String INFO_ADD_CONTACT = "Add contact to send/receive messages";
|
||||
private final String INFO_CONTACT_LIST = "Choose your chat partner to send/receive messages. If you want to chat with someone new, invite them via the add button";
|
||||
private final String INFO_HELP = "Q&A";
|
||||
private final String INFO_MESSAGES_LIST_DEFAULT = "Choose a contact first to see messages here";
|
||||
private final String INFO_NO_SAVED_MESSAGES = "There are no saved messages for this contact";
|
||||
private final String INFO_VERIFY_CONTACT = "To verify the security of your end-to-end encryption with %s, compare the numbers above with their device";
|
||||
|
||||
private final String INFO_SESSION_CREATION_FAILED = "Session creation failed. If possible delete sender in contact list and ask for a new keybundle";
|
||||
private final String INFO_CONTACT_CREATION_FAILED = "Could not create contact. Abort";
|
||||
private final String INFO_ADD_FIRSTNAME_ADD_CONTACT = "Enter a first name to create contact";
|
||||
private final String INFO_CHOOSE_CONTACT_FIRST = "Please choose a contact first";
|
||||
private final String INFO_NO_MESSAGE_TO_ENCRYPT = "No message to encrypt";
|
||||
private final String INFO_NO_MESSAGE_TO_DECRYPT = "No message to decrypt";
|
||||
private final String INFO_MESSAGE_DECRYPTION_FAILED = "Message could not be decrypted. Possible Reasons: You decrypted a message you already have decrypted once or the session is invalid. In that case delete your contact and tell your contact to delete you and ask for a new invite";
|
||||
private final String INFO_CANNOT_DECRYPT_OWN_MESSAGES = "You can't decrypt your own messages";
|
||||
private final String INFO_SIGNAL_MESSAGE_NO_CONTACT_FOUND = "Please add the contact first";
|
||||
private final String INFO_MESSAGE_ENCRYPTION_FAILED = "Message could not be encrypted";
|
||||
private final String INFO_UPDATE_CONTACT_FAILED = "Could not update contact information";
|
||||
|
||||
private static class E2EEStripVisibilityGroup {
|
||||
private final View mE2EEStripView;
|
||||
private final View mE2EEStrip;
|
||||
|
||||
public E2EEStripVisibilityGroup(final View e2EEStripView, final ViewGroup e2EEStrip) {
|
||||
mE2EEStripView = e2EEStripView;
|
||||
mE2EEStrip = e2EEStrip;
|
||||
showE2EEStrip();
|
||||
}
|
||||
|
||||
public void showE2EEStrip() {
|
||||
mE2EEStrip.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link E2EEStripView} for showing e2ee functionality.
|
||||
*
|
||||
* @param context Context
|
||||
* @param attrs AttributeSet
|
||||
*/
|
||||
public E2EEStripView(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.e2eeStripViewStyle);
|
||||
}
|
||||
|
||||
public E2EEStripView(final Context context, final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
mE2EEStrip = new E2EEStrip(getContext());
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
inflater.inflate(R.layout.ee2e_main_view, this);
|
||||
|
||||
setupMainView();
|
||||
setupAddContactView();
|
||||
setupContactListView();
|
||||
setupMessagesListView();
|
||||
setupHelpView();
|
||||
setupVerifyContactView();
|
||||
|
||||
mE2EEStripVisibilityGroup = new E2EEStripVisibilityGroup(this, mE2EEMainStrip);
|
||||
}
|
||||
|
||||
private void setupVerifyContactView() {
|
||||
mLayoutE2EEVerifyContactView = findViewById(R.id.e2ee_verify_contact_wrapper);
|
||||
mVerifyContactInfoTextView = findViewById(R.id.e2ee_verify_contact_info_text);
|
||||
mVerifyContactTableView = findViewById(R.id.e2ee_verify_contact_number_table);
|
||||
mVerifyContactReturnButton = findViewById(R.id.e2ee_verify_contact_return_button);
|
||||
mVerifyContactVerifyButton = findViewById(R.id.e2ee_verify_contact_verify_button);
|
||||
mCodes[0] = findViewById(R.id.code_first);
|
||||
mCodes[1] = findViewById(R.id.code_second);
|
||||
mCodes[2] = findViewById(R.id.code_third);
|
||||
mCodes[3] = findViewById(R.id.code_fourth);
|
||||
mCodes[4] = findViewById(R.id.code_fifth);
|
||||
mCodes[5] = findViewById(R.id.code_sixth);
|
||||
mCodes[6] = findViewById(R.id.code_seventh);
|
||||
mCodes[7] = findViewById(R.id.code_eighth);
|
||||
mCodes[8] = findViewById(R.id.code_ninth);
|
||||
mCodes[9] = findViewById(R.id.code_tenth);
|
||||
mCodes[10] = findViewById(R.id.code_eleventh);
|
||||
mCodes[11] = findViewById(R.id.code_twelth);
|
||||
|
||||
createVerifyContactReturnButtonClickListener();
|
||||
createVerifyContactVerifyButtonClickListener();
|
||||
loadFingerprintInVerifyContactView();
|
||||
|
||||
if (chosenContact == null) return;
|
||||
setInfoTextViewMessage(mVerifyContactInfoTextView, String.format(INFO_VERIFY_CONTACT, "" + chosenContact.getFirstName() + " " + chosenContact.getLastName()));
|
||||
}
|
||||
|
||||
private void createVerifyContactVerifyButtonClickListener() {
|
||||
if (mVerifyContactVerifyButton == null) return;
|
||||
mVerifyContactVerifyButton.setOnClickListener(v -> {
|
||||
try {
|
||||
mE2EEStrip.verifyContact(chosenContact);
|
||||
loadContactsIntoContactsListView();
|
||||
showOnlyUIView(UIView.CONTACT_LIST_VIEW);
|
||||
} catch (UnknownContactException e) {
|
||||
Toast.makeText(getContext(), INFO_UPDATE_CONTACT_FAILED, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadFingerprintInVerifyContactView() {
|
||||
if (chosenContact == null) return;
|
||||
|
||||
createVerifyContactReturnButtonClickListener();
|
||||
setInfoTextViewMessage(mVerifyContactInfoTextView, String.format(INFO_VERIFY_CONTACT, "" + chosenContact.getFirstName() + " " + chosenContact.getLastName()));
|
||||
|
||||
final Fingerprint fingerprint = mE2EEStrip.getFingerprint(chosenContact);
|
||||
if (fingerprint == null) return;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
}
|
||||
|
||||
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
|
||||
String[] segments = new String[segmentCount];
|
||||
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
|
||||
int partSize = digits.length() / segmentCount;
|
||||
|
||||
for (int i = 0; i < segmentCount; i++) {
|
||||
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
|
||||
String[] segments = getSegments(fingerprint, mCodes.length);
|
||||
|
||||
for (int i = 0; i < mCodes.length; i++) {
|
||||
if (animate) setCodeSegment(mCodes[i], segments[i]);
|
||||
else mCodes[i].setText(segments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCodeSegment(final TextView codeView, String segment) {
|
||||
ValueAnimator valueAnimator = new ValueAnimator();
|
||||
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
|
||||
|
||||
valueAnimator.addUpdateListener(animation -> {
|
||||
int value = (int) animation.getAnimatedValue();
|
||||
codeView.setText(String.format(Locale.getDefault(), "%05d", value));
|
||||
});
|
||||
|
||||
valueAnimator.setEvaluator((TypeEvaluator<Integer>) (fraction, startValue, endValue)
|
||||
-> Math.round(startValue + (endValue - startValue) * fraction));
|
||||
|
||||
valueAnimator.setDuration(1000);
|
||||
valueAnimator.start();
|
||||
}
|
||||
|
||||
private void createVerifyContactReturnButtonClickListener() {
|
||||
if (mVerifyContactReturnButton == null) return;
|
||||
mVerifyContactReturnButton.setOnClickListener(v -> showOnlyUIView(UIView.CONTACT_LIST_VIEW));
|
||||
}
|
||||
|
||||
private void setupHelpView() {
|
||||
mLayoutE2EEHelpView = findViewById(R.id.e2ee_help_view_wrapper);
|
||||
mHelpInfoTextView = findViewById(R.id.e2ee_help_info_text);
|
||||
mHelpViewTextView = findViewById(R.id.e2ee_help_view_text);
|
||||
mHelpViewReturnButton = findViewById(R.id.e2ee_help_list_return_button);
|
||||
mHelpVersionTextView = findViewById(R.id.e2ee_help_view_version_text);
|
||||
|
||||
mHelpViewTextView.setText(Html.fromHtml(getResources().getString(R.string.e2ee_help_view_text), Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING));
|
||||
mHelpViewTextView.setMovementMethod(new ScrollingMovementMethod());
|
||||
setInfoTextViewMessage(mHelpInfoTextView, INFO_HELP);
|
||||
|
||||
mHelpVersionTextView.setText(String.format("%s%s", "v", BuildConfig.VERSION_NAME));
|
||||
|
||||
createHelpReturnButtonClickListener();
|
||||
}
|
||||
|
||||
private void createHelpReturnButtonClickListener() {
|
||||
if (mHelpViewReturnButton == null) return;
|
||||
mHelpViewReturnButton.setOnClickListener(v -> showOnlyUIView(UIView.MAIN_VIEW));
|
||||
}
|
||||
|
||||
private void setupMessagesListView() {
|
||||
mLayoutE2EEMessagesListView = findViewById(R.id.e2ee_messages_list_wrapper);
|
||||
mMessagesListInfoTextView = findViewById(R.id.e2ee_messages_list_info_text);
|
||||
mMessagesList = findViewById(R.id.e2ee_messages_list);
|
||||
mMessagesListReturnButton = findViewById(R.id.e2ee_messages_list_return_button);
|
||||
|
||||
refreshContactInMessageInfoField();
|
||||
createMessagesListReturnButtonClickListener();
|
||||
loadMessagesIntoMessagesListView();
|
||||
}
|
||||
|
||||
private void refreshContactInMessageInfoField() {
|
||||
if (mMessagesListInfoTextView == null) return;
|
||||
if (chosenContact != null) {
|
||||
setInfoTextViewMessage(mMessagesListInfoTextView, "Message log with: " + chosenContact.getFirstName() + " " + chosenContact.getLastName());
|
||||
} else {
|
||||
setInfoTextViewMessage(mMessagesListInfoTextView, INFO_MESSAGES_LIST_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMessagesIntoMessagesListView() {
|
||||
List<StorageMessage> messages = null;
|
||||
String accountName = null;
|
||||
|
||||
if (chosenContact != null) {
|
||||
try {
|
||||
messages = mE2EEStrip.getUnencryptedMessages(chosenContact);
|
||||
accountName = mE2EEStrip.getAccountName();
|
||||
} catch (UnknownContactException e) {
|
||||
Toast.makeText(getContext(), INFO_NO_SAVED_MESSAGES, Toast.LENGTH_SHORT).show();
|
||||
Log.d(TAG, INFO_NO_SAVED_MESSAGES);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (messages == null) {
|
||||
messages = new ArrayList<>();
|
||||
} else {
|
||||
// o1 first, then o2
|
||||
messages.sort(Comparator.comparing(StorageMessage::getTimestamp));
|
||||
}
|
||||
|
||||
final ArrayList<Object> messagesAsObjectsList = new ArrayList<>(messages);
|
||||
final ListAdapterMessages listAdapterMessages = new ListAdapterMessages(this.getContext(), R.layout.e2ee_messages_element_view, messagesAsObjectsList, accountName);
|
||||
mMessagesList.setAdapter(listAdapterMessages);
|
||||
|
||||
changeHeightOfMessageListView(messages);
|
||||
}
|
||||
|
||||
private void changeHeightOfMessageListView(List<StorageMessage> messages) {
|
||||
if (messages == null) return;
|
||||
Log.d(TAG, "Setting layout params...");
|
||||
LinearLayout.LayoutParams params = null;
|
||||
if (messages.size() == 0) {
|
||||
params = (LinearLayout.LayoutParams) mMessagesList.getLayoutParams();
|
||||
params.height = 0;
|
||||
mMessagesList.setLayoutParams(params);
|
||||
} else {
|
||||
params = (LinearLayout.LayoutParams) mMessagesList.getLayoutParams();
|
||||
params.height = 700;
|
||||
mMessagesList.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupContactListView() {
|
||||
mLayoutE2EEContactListView = findViewById(R.id.e2ee_contact_list_wrapper);
|
||||
mContactListInfoTextView = findViewById(R.id.e2ee_contact_list_info_text);
|
||||
mContactList = findViewById(R.id.e2ee_contact_list);
|
||||
mContactListReturnButton = findViewById(R.id.e2ee_contact_list_return_button);
|
||||
mContactListInviteButton = findViewById(R.id.e2ee_contact_list_invite_new_contact_button);
|
||||
|
||||
createContactListReturnButtonClickListener();
|
||||
createContactListInviteButtonClickListener();
|
||||
|
||||
setInfoTextViewMessage(mContactListInfoTextView, INFO_CONTACT_LIST);
|
||||
|
||||
loadContactsIntoContactsListView();
|
||||
}
|
||||
|
||||
private void createMessagesListReturnButtonClickListener() {
|
||||
if (mMessagesListReturnButton == null) return;
|
||||
mMessagesListReturnButton.setOnClickListener(v -> showOnlyUIView(UIView.MAIN_VIEW));
|
||||
}
|
||||
|
||||
private void createContactListReturnButtonClickListener() {
|
||||
if (mContactListReturnButton == null) return;
|
||||
mContactListReturnButton.setOnClickListener(v -> showOnlyUIView(UIView.MAIN_VIEW));
|
||||
}
|
||||
|
||||
private void createContactListInviteButtonClickListener() {
|
||||
if (mContactListInviteButton == null) return;
|
||||
mContactListInviteButton.setOnClickListener(v -> {
|
||||
showOnlyUIView(UIView.MAIN_VIEW);
|
||||
sendPreKeyResponseMessageToApplication();
|
||||
});
|
||||
}
|
||||
|
||||
private void loadContactsIntoContactsListView() {
|
||||
ArrayList<Contact> contacts = mE2EEStrip.getContacts();
|
||||
if (contacts == null) return;
|
||||
final ArrayList<Object> contactsAsObjectsList = new ArrayList<>(contacts);
|
||||
final ListAdapterContacts listAdapterContacts = new ListAdapterContacts(this.getContext(), R.layout.e2ee_contact_list_element_view, contactsAsObjectsList);
|
||||
listAdapterContacts.setListener(this); // to remove and select contacts on click
|
||||
mContactList.setAdapter(listAdapterContacts);
|
||||
}
|
||||
|
||||
private void setupAddContactView() {
|
||||
mLayoutE2EEAddContactView = findViewById(R.id.e2ee_add_contact_wrapper);
|
||||
mAddContactInfoTextView = findViewById(R.id.e2ee_add_contact_info_text);
|
||||
mAddContactFirstNameInputEditText = findViewById(R.id.e2ee_add_contact_first_name_input_field);
|
||||
mAddContactLastNameInputEditText = findViewById(R.id.e2ee_add_contact_last_name_input_field);
|
||||
mAddContactCancelButton = findViewById(R.id.e2ee_add_contact_cancel_button);
|
||||
mAddContactAddButton = findViewById(R.id.e2ee_add_contact_button);
|
||||
|
||||
setupFirstNameInputEditTextField();
|
||||
setupLastNameInputEditTextField();
|
||||
|
||||
mAddContactInfoTextView.setText(INFO_ADD_CONTACT);
|
||||
|
||||
createAddContactCancelClickListener();
|
||||
}
|
||||
|
||||
private void createAddContactAddClickListener(final MessageEnvelope messageEnvelope) {
|
||||
if (mAddContactAddButton == null) return;
|
||||
mAddContactAddButton.setOnClickListener(v -> addContact(messageEnvelope));
|
||||
}
|
||||
|
||||
private void addContact(final MessageEnvelope messageEnvelope) {
|
||||
final CharSequence firstName = mAddContactFirstNameInputEditText.getText();
|
||||
final CharSequence lastName = mAddContactLastNameInputEditText.getText();
|
||||
|
||||
final String signalProtocolAddressName = messageEnvelope.getSignalProtocolAddressName();
|
||||
final int deviceId = messageEnvelope.getDeviceId();
|
||||
final SignalProtocolAddress recipientProtocolAddress = new SignalProtocolAddress(signalProtocolAddressName, deviceId);
|
||||
|
||||
if (!providedContactInformationIsValid(firstName, lastName)) return;
|
||||
chosenContact = mE2EEStrip.createAndAddContactToContacts(firstName, lastName, recipientProtocolAddress.getName(), deviceId);
|
||||
|
||||
if (chosenContact == null) {
|
||||
abortContactAdding();
|
||||
return;
|
||||
} else {
|
||||
Log.d(TAG, "chosenContact = " + chosenContact);
|
||||
}
|
||||
|
||||
resetAddContactInputTextFields();
|
||||
showOnlyUIView(UIView.MAIN_VIEW);
|
||||
|
||||
if (messageEnvelope.getPreKeyResponse() != null) {
|
||||
final boolean successful = mE2EEStrip.createSessionWithContact(chosenContact, messageEnvelope, recipientProtocolAddress);
|
||||
if (successful) {
|
||||
setInfoTextViewMessage(mInfoTextView, "Contact " + chosenContact.getFirstName() + " " + chosenContact.getLastName() + " created. You can send messages now");
|
||||
} else {
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_SESSION_CREATION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageEnvelope.getCiphertextMessage() != null) {
|
||||
decryptMessageAndShowMessageInMainInputField(messageEnvelope, chosenContact, false);
|
||||
changeImageButtonState(mDecryptButton, ButtonState.DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
private void abortContactAdding() {
|
||||
Toast.makeText(getContext(), INFO_CONTACT_CREATION_FAILED, Toast.LENGTH_SHORT).show();
|
||||
Log.d(TAG, INFO_CONTACT_CREATION_FAILED);
|
||||
showOnlyUIView(UIView.MAIN_VIEW);
|
||||
resetChosenContactAndInfoText();
|
||||
}
|
||||
|
||||
private void resetAddContactInputTextFields() {
|
||||
mAddContactFirstNameInputEditText.setText("");
|
||||
mAddContactLastNameInputEditText.setText("");
|
||||
}
|
||||
|
||||
private boolean providedContactInformationIsValid(CharSequence firstName, CharSequence lastName) {
|
||||
if (firstName == null || firstName.length() == 0) {
|
||||
Toast.makeText(getContext(), INFO_ADD_FIRSTNAME_ADD_CONTACT, Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createAddContactCancelClickListener() {
|
||||
if (mAddContactCancelButton != null) {
|
||||
mAddContactCancelButton.setOnClickListener(v -> {
|
||||
showOnlyUIView(UIView.MAIN_VIEW);
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_NO_CONTACT_CHOSEN);
|
||||
mE2EEStrip.clearClipboard();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void changeImageButtonState(ImageButton imageButton, ButtonState state) {
|
||||
if (state.equals(ButtonState.ENABLED)) {
|
||||
imageButton.setEnabled(true);
|
||||
} else if (state.equals(ButtonState.DISABLED)) {
|
||||
imageButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupMainView() {
|
||||
mLayoutE2EEMainView = findViewById(R.id.e2ee_main_wrapper);
|
||||
mE2EEMainStrip = findViewById(R.id.e2ee_main_button_strip);
|
||||
mEncryptButton = findViewById(R.id.e2ee_button_encrypt);
|
||||
mDecryptButton = findViewById(R.id.e2ee_button_decrypt);
|
||||
mRecipientButton = findViewById(R.id.e2ee_button_select_recipient);
|
||||
mChatLogsButton = findViewById(R.id.e2ee_button_chat_logs);
|
||||
mShowHelpButton = findViewById(R.id.e2ee_button_show_help);
|
||||
mInfoTextView = findViewById(R.id.e2ee_info_text);
|
||||
mInputEditText = findViewById(R.id.e2ee_input_field);
|
||||
mClearUserInputButton = findViewById(R.id.e2ee_button_clear_text);
|
||||
mSelectEncodingFairyTaleButton = findViewById(R.id.e2ee_button_select_encoding_fairytale);
|
||||
mSelectEncodingRawButton = findViewById(R.id.e2ee_button_select_encoding_raw);
|
||||
|
||||
setMainInfoTextTextChangeListener();
|
||||
setMainInfoTextClearChosenContactListener();
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_NO_CONTACT_CHOSEN);
|
||||
|
||||
createButtonEncryptClickListener();
|
||||
createButtonDecryptClickListener();
|
||||
createButtonClearUserInputClickListener();
|
||||
createButtonRecipientClickListener();
|
||||
createButtonSelectEncryptionMethodClickListener();
|
||||
createButtonChatLogsClickListener();
|
||||
createButtonShowHelpClickListener();
|
||||
|
||||
setupMessageInputEditTextField();
|
||||
|
||||
initClipboardListenerToChangeStateOfDecryptButton();
|
||||
}
|
||||
|
||||
private void setMainInfoTextClearChosenContactListener() {
|
||||
if (mInfoTextView == null) return;
|
||||
mInfoTextView.setOnClickListener(v -> resetChosenContactAndInfoText());
|
||||
}
|
||||
|
||||
private void initClipboardListenerToChangeStateOfDecryptButton() {
|
||||
final ClipboardManager clipboardManager = (ClipboardManager) this.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.addPrimaryClipChangedListener(() -> {
|
||||
try {
|
||||
String item = null;
|
||||
boolean isHTML = false;
|
||||
// hint: listener for HTML text needed for using app with telegram
|
||||
if (clipboardManager.getPrimaryClipDescription() != null &&
|
||||
(clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
|
||||
clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML))) {
|
||||
isHTML = clipboardManager.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
|
||||
item = String.valueOf(clipboardManager.getPrimaryClip().getItemAt(0).getText());
|
||||
}
|
||||
|
||||
if (item == null || item.isEmpty()) return;
|
||||
if (isHTML) {
|
||||
item = HTMLHelper.replaceHtmlCharacters(item);
|
||||
}
|
||||
|
||||
final String decodedItem = mE2EEStrip.decodeMessage(item);
|
||||
if (decodedItem == null) return;
|
||||
|
||||
if (mE2EEStrip.getMessageType(JsonUtil.fromJson(decodedItem, MessageEnvelope.class))
|
||||
.equals(MessageType.UPDATED_PRE_KEY_RESPONSE_MESSAGE_AND_SIGNAL_MESSAGE)) {
|
||||
changeImageButtonState(mDecryptButton, ButtonState.ENABLED);
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_PRE_KEY_AND_SIGNAL_MESSAGE_DETECTED);
|
||||
} else if (mE2EEStrip.getMessageType(JsonUtil.fromJson(decodedItem, MessageEnvelope.class))
|
||||
.equals(MessageType.PRE_KEY_RESPONSE_MESSAGE)) {
|
||||
changeImageButtonState(mDecryptButton, ButtonState.ENABLED);
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_PRE_KEY_DETECTED);
|
||||
} else if (mE2EEStrip.getMessageType(JsonUtil.fromJson(decodedItem, MessageEnvelope.class))
|
||||
.equals(MessageType.SIGNAL_MESSAGE)) {
|
||||
changeImageButtonState(mEncryptButton, ButtonState.ENABLED);
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_SIGNAL_MESSAGE_DETECTED);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setMainInfoTextTextChangeListener() {
|
||||
if (mInfoTextView == null) return;
|
||||
mInfoTextView.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.toString().equals(INFO_NO_CONTACT_CHOSEN)) {
|
||||
changeImageButtonState(mDecryptButton, ButtonState.DISABLED);
|
||||
changeImageButtonState(mEncryptButton, ButtonState.DISABLED);
|
||||
} else {
|
||||
changeImageButtonState(mDecryptButton, ButtonState.ENABLED);
|
||||
changeImageButtonState(mEncryptButton, ButtonState.ENABLED);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setInfoTextViewMessage(final TextView textView, final String message) {
|
||||
if (textView == null) return;
|
||||
textView.setText(message);
|
||||
}
|
||||
|
||||
private void createButtonEncryptClickListener() {
|
||||
if (mEncryptButton == null) return;
|
||||
mEncryptButton.setOnClickListener(v -> encryptAndSendInputFieldContent());
|
||||
}
|
||||
|
||||
private void encryptAndSendInputFieldContent() {
|
||||
if (chosenContact == null) {
|
||||
Toast.makeText(getContext(), INFO_CHOOSE_CONTACT_FIRST, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mInputEditText != null && mInputEditText.getText().length() > 0) {
|
||||
// call encrypt method and encrypt text
|
||||
final CharSequence encryptedMessage;
|
||||
try {
|
||||
encryptedMessage = mE2EEStrip.encryptMessage(mInputEditText.getText().toString(), chosenContact.getSignalProtocolAddress(), encodingMethod);
|
||||
Log.d(TAG, String.valueOf(encryptedMessage));
|
||||
|
||||
if (encryptedMessage != null) {
|
||||
mInputEditText.setText(encryptedMessage);
|
||||
sendEncryptedMessageToApplication(encryptedMessage);
|
||||
} else {
|
||||
Toast.makeText(getContext(), INFO_MESSAGE_ENCRYPTION_FAILED, Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "Error: Encrypted message is null!");
|
||||
}
|
||||
} catch (TooManyCharsException e) {
|
||||
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(getContext(), INFO_MESSAGE_ENCRYPTION_FAILED, Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "Error: Encrypted message is null!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getContext(), INFO_NO_MESSAGE_TO_ENCRYPT, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
showChosenContactInMainInfoField();
|
||||
}
|
||||
|
||||
private void createButtonDecryptClickListener() {
|
||||
if (mDecryptButton == null) return;
|
||||
mDecryptButton.setOnClickListener(v -> decryptMessageInClipboard());
|
||||
}
|
||||
|
||||
private void createButtonRecipientClickListener() {
|
||||
if (mRecipientButton != null) {
|
||||
mRecipientButton.setOnClickListener(v -> {
|
||||
loadContactsIntoContactsListView();
|
||||
showOnlyUIView(UIView.CONTACT_LIST_VIEW);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void showOnlyUIView(final UIView uiView) {
|
||||
if (mLayoutE2EEMainView == null || mLayoutE2EEAddContactView == null ||
|
||||
mLayoutE2EEContactListView == null || mLayoutE2EEMessagesListView == null)
|
||||
return;
|
||||
|
||||
if (uiView.equals(UIView.MAIN_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(VISIBLE);
|
||||
mLayoutE2EEAddContactView.setVisibility(GONE);
|
||||
mLayoutE2EEContactListView.setVisibility(GONE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(GONE);
|
||||
mLayoutE2EEHelpView.setVisibility(GONE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(GONE);
|
||||
} else if (uiView.equals(UIView.ADD_CONTACT_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(GONE);
|
||||
mLayoutE2EEAddContactView.setVisibility(VISIBLE);
|
||||
mLayoutE2EEContactListView.setVisibility(GONE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(GONE);
|
||||
mLayoutE2EEHelpView.setVisibility(GONE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(GONE);
|
||||
} else if (uiView.equals(UIView.CONTACT_LIST_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(GONE);
|
||||
mLayoutE2EEAddContactView.setVisibility(GONE);
|
||||
mLayoutE2EEContactListView.setVisibility(VISIBLE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(GONE);
|
||||
mLayoutE2EEHelpView.setVisibility(GONE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(GONE);
|
||||
} else if (uiView.equals(UIView.MESSAGES_LIST_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(GONE);
|
||||
mLayoutE2EEAddContactView.setVisibility(GONE);
|
||||
mLayoutE2EEContactListView.setVisibility(GONE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(VISIBLE);
|
||||
mLayoutE2EEHelpView.setVisibility(GONE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(GONE);
|
||||
} else if (uiView.equals(UIView.HELP_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(GONE);
|
||||
mLayoutE2EEAddContactView.setVisibility(GONE);
|
||||
mLayoutE2EEContactListView.setVisibility(GONE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(GONE);
|
||||
mLayoutE2EEHelpView.setVisibility(VISIBLE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(GONE);
|
||||
} else if (uiView.equals(UIView.VERIFY_CONTACT_VIEW)) {
|
||||
mLayoutE2EEMainView.setVisibility(GONE);
|
||||
mLayoutE2EEAddContactView.setVisibility(GONE);
|
||||
mLayoutE2EEContactListView.setVisibility(GONE);
|
||||
mLayoutE2EEMessagesListView.setVisibility(GONE);
|
||||
mLayoutE2EEHelpView.setVisibility(GONE);
|
||||
mLayoutE2EEVerifyContactView.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void createButtonClearUserInputClickListener() {
|
||||
if (mClearUserInputButton == null) return;
|
||||
mClearUserInputButton.setOnClickListener(v -> clearUserInputString());
|
||||
}
|
||||
|
||||
private void createButtonSelectEncryptionMethodClickListener() {
|
||||
if (mSelectEncodingFairyTaleButton == null || mSelectEncodingRawButton == null) return;
|
||||
|
||||
mSelectEncodingFairyTaleButton.setOnClickListener(v -> {
|
||||
mSelectEncodingFairyTaleButton.setVisibility(GONE);
|
||||
mSelectEncodingRawButton.setVisibility(VISIBLE);
|
||||
encodingMethod = Encoder.RAW;
|
||||
});
|
||||
|
||||
mSelectEncodingRawButton.setOnClickListener(v -> {
|
||||
mSelectEncodingFairyTaleButton.setVisibility(VISIBLE);
|
||||
mSelectEncodingRawButton.setVisibility(GONE);
|
||||
encodingMethod = Encoder.FAIRYTALE;
|
||||
});
|
||||
}
|
||||
|
||||
private void createButtonShowHelpClickListener() {
|
||||
if (mShowHelpButton == null) return;
|
||||
mShowHelpButton.setOnClickListener(v -> {
|
||||
showOnlyUIView(UIView.HELP_VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
private void createButtonChatLogsClickListener() {
|
||||
if (mChatLogsButton == null) return;
|
||||
mChatLogsButton.setOnClickListener(v -> {
|
||||
refreshContactInMessageInfoField();
|
||||
loadMessagesIntoMessagesListView();
|
||||
showOnlyUIView(UIView.MESSAGES_LIST_VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupMessageInputEditTextField() {
|
||||
mInputEditText.setMovementMethod(new ScrollingMovementMethod());
|
||||
mInputEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) mRichInputConnection.setOtherIC(mInputEditText);
|
||||
mRichInputConnection.setShouldUseOtherIC(hasFocus);
|
||||
changeVisibilityInputFieldButtons(hasFocus);
|
||||
});
|
||||
|
||||
mClearUserInputButton.setVisibility(GONE);
|
||||
mSelectEncodingFairyTaleButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void setupFirstNameInputEditTextField() {
|
||||
mAddContactFirstNameInputEditText.setMovementMethod(new ScrollingMovementMethod());
|
||||
mAddContactFirstNameInputEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) mRichInputConnection.setOtherIC(mAddContactFirstNameInputEditText);
|
||||
mRichInputConnection.setShouldUseOtherIC(hasFocus);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupLastNameInputEditTextField() {
|
||||
mAddContactLastNameInputEditText.setMovementMethod(new ScrollingMovementMethod());
|
||||
mAddContactLastNameInputEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) mRichInputConnection.setOtherIC(mAddContactLastNameInputEditText);
|
||||
mRichInputConnection.setShouldUseOtherIC(hasFocus);
|
||||
});
|
||||
}
|
||||
|
||||
private void sendPreKeyResponseMessageToApplication() {
|
||||
final String encoded;
|
||||
final String message = mE2EEStrip.getPreKeyResponseMessage();
|
||||
try {
|
||||
mE2EEStrip.checkMessageLengthForEncodingMethod(message, encodingMethod, true);
|
||||
encoded = mE2EEStrip.encode(message, encodingMethod);
|
||||
} catch (TooManyCharsException e) {
|
||||
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, e.getMessage());
|
||||
e.printStackTrace();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(getContext(), "Generating pre key message failed!", Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "Generating pre key message failed!");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
mInputEditText.setText(encoded);
|
||||
sendEncryptedMessageToApplication(encoded);
|
||||
}
|
||||
|
||||
private void decryptMessageInClipboard() {
|
||||
final CharSequence mEncryptedMessageFromClipboard = mE2EEStrip.getEncryptedMessageFromClipboard();
|
||||
if (mEncryptedMessageFromClipboard == null || mEncryptedMessageFromClipboard.length() == 0) {
|
||||
Toast.makeText(getContext(), INFO_NO_MESSAGE_TO_DECRYPT, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final String encodedMessage = mE2EEStrip.decodeMessage(mEncryptedMessageFromClipboard.toString());
|
||||
|
||||
final MessageEnvelope messageEnvelope = JsonUtil.fromJson(encodedMessage, MessageEnvelope.class);
|
||||
if (messageEnvelope == null) throw new IOException("Message is null. Abort!");
|
||||
|
||||
final MessageType messageType = mE2EEStrip.getMessageType(messageEnvelope);
|
||||
if (messageType == null) throw new IOException("Message type is null. Abort!");
|
||||
|
||||
final Contact extractedSender = (Contact) mE2EEStrip.getContactFromEnvelope(messageEnvelope);
|
||||
if (messageEnvelope.getSignalProtocolAddressName().equals(mE2EEStrip.getAccountName())) {
|
||||
Toast.makeText(getContext(), INFO_CANNOT_DECRYPT_OWN_MESSAGES, Toast.LENGTH_SHORT).show();
|
||||
mE2EEStrip.clearClipboard();
|
||||
showChosenContactInMainInfoField();
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageType.equals(MessageType.PRE_KEY_RESPONSE_MESSAGE)) {
|
||||
processPreKeyResponse(messageEnvelope, extractedSender);
|
||||
} else if (messageType.equals(MessageType.SIGNAL_MESSAGE)) {
|
||||
processSignalMessage(messageEnvelope, extractedSender);
|
||||
} else if (messageType.equals(MessageType.UPDATED_PRE_KEY_RESPONSE_MESSAGE_AND_SIGNAL_MESSAGE)) {
|
||||
processUpdatedPreKeyResponse(messageEnvelope, extractedSender);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
resetChosenContactAndInfoText();
|
||||
}
|
||||
showChosenContactInMainInfoField();
|
||||
mE2EEStrip.clearClipboard();
|
||||
changeImageButtonState(mDecryptButton, ButtonState.DISABLED);
|
||||
}
|
||||
|
||||
private void processSignalMessage(MessageEnvelope messageEnvelope, Contact sender) {
|
||||
if (sender == null) {
|
||||
// if no contact found, show add contact view
|
||||
Toast.makeText(getContext(), INFO_SIGNAL_MESSAGE_NO_CONTACT_FOUND, Toast.LENGTH_SHORT).show();
|
||||
showAddContactView(messageEnvelope);
|
||||
} else {
|
||||
chosenContact = sender;
|
||||
setInfoTextViewMessage(mInfoTextView, "Detected contact: " + chosenContact.getFirstName() + " " + chosenContact.getLastName());
|
||||
decryptMessageAndShowMessageInMainInputField(messageEnvelope, chosenContact, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void processPreKeyResponse(MessageEnvelope messageEnvelope, Contact sender) {
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_PRE_KEY_DETECTED);
|
||||
if (sender == null) {
|
||||
// add contact with preKey message
|
||||
showAddContactView(messageEnvelope);
|
||||
} else {
|
||||
// update contact with preKey information
|
||||
chosenContact = sender;
|
||||
setInfoTextViewMessage(mInfoTextView, "Detected contact: " + chosenContact.getFirstName() + " " + chosenContact.getLastName());
|
||||
decryptMessageAndShowMessageInMainInputField(messageEnvelope, chosenContact, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void processUpdatedPreKeyResponse(MessageEnvelope messageEnvelope, Contact sender) {
|
||||
// debug only Toast.makeText(getContext(), "Updated signed pre key detected!", Toast.LENGTH_SHORT).show();
|
||||
if (sender == null) {
|
||||
// contact was not added before -> proceed as normal preKeyMessage
|
||||
processPreKeyResponse(messageEnvelope, sender);
|
||||
} else {
|
||||
// update contact with preKey information
|
||||
chosenContact = sender;
|
||||
setInfoTextViewMessage(mInfoTextView, "Detected contact with updated keybundle: " + chosenContact.getFirstName() + " " + chosenContact.getLastName());
|
||||
decryptMessageAndShowMessageInMainInputField(messageEnvelope, chosenContact, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetChosenContactAndInfoText() {
|
||||
chosenContact = null;
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_NO_CONTACT_CHOSEN);
|
||||
}
|
||||
|
||||
private void showAddContactView(MessageEnvelope messageEnvelope) {
|
||||
createAddContactAddClickListener(messageEnvelope);
|
||||
showOnlyUIView(UIView.ADD_CONTACT_VIEW);
|
||||
}
|
||||
|
||||
private void decryptMessageAndShowMessageInMainInputField(final MessageEnvelope messageEnvelope, final Contact sender, boolean isSessionCreation) {
|
||||
final CharSequence decryptedMessage = mE2EEStrip.decryptMessage(messageEnvelope, sender);
|
||||
|
||||
if (!isSessionCreation && decryptedMessage != null) {
|
||||
mInputEditText.setText(decryptedMessage);
|
||||
changeVisibilityInputFieldButtons(true);
|
||||
} else if (isSessionCreation) {
|
||||
changeVisibilityInputFieldButtons(true);
|
||||
} else {
|
||||
Toast.makeText(getContext(), INFO_MESSAGE_DECRYPTION_FAILED, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "Error: Decrypted message is null");
|
||||
}
|
||||
mE2EEStrip.clearClipboard();
|
||||
}
|
||||
|
||||
private void sendEncryptedMessageToApplication(CharSequence encryptedMessage) {
|
||||
if (encryptedMessage == null) return;
|
||||
|
||||
mRichInputConnection.setShouldUseOtherIC(false);
|
||||
mListener.onTextInput((String) encryptedMessage);
|
||||
mInputEditText.clearFocus();
|
||||
clearUserInputString();
|
||||
mE2EEStrip.clearClipboard();
|
||||
}
|
||||
|
||||
private void clearUserInputString() {
|
||||
if (mInputEditText != null) mInputEditText.setText("");
|
||||
}
|
||||
|
||||
private void changeVisibilityInputFieldButtons(boolean shouldBeVisible) {
|
||||
if (mClearUserInputButton != null && mSelectEncodingFairyTaleButton != null && mSelectEncodingRawButton != null) {
|
||||
if (shouldBeVisible) {
|
||||
mClearUserInputButton.setVisibility(VISIBLE);
|
||||
if (encodingMethod.equals(Encoder.FAIRYTALE)) {
|
||||
mSelectEncodingFairyTaleButton.setVisibility(VISIBLE);
|
||||
} else {
|
||||
mSelectEncodingRawButton.setVisibility(VISIBLE);
|
||||
}
|
||||
} else {
|
||||
mClearUserInputButton.setVisibility(GONE);
|
||||
mSelectEncodingFairyTaleButton.setVisibility(GONE);
|
||||
mSelectEncodingRawButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showChosenContactInMainInfoField() {
|
||||
if (chosenContact != null) {
|
||||
setInfoTextViewMessage(mInfoTextView, "Chosen contact: " + chosenContact.getFirstName() + " " + chosenContact.getLastName());
|
||||
} else {
|
||||
setInfoTextViewMessage(mInfoTextView, INFO_NO_CONTACT_CHOSEN);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ButtonState {ENABLED, DISABLED}
|
||||
|
||||
private enum UIView {MAIN_VIEW, ADD_CONTACT_VIEW, CONTACT_LIST_VIEW, MESSAGES_LIST_VIEW, HELP_VIEW, VERIFY_CONTACT_VIEW}
|
||||
|
||||
@Override
|
||||
public void selectContact(Contact contact) {
|
||||
chosenContact = contact;
|
||||
showChosenContactInMainInfoField();
|
||||
Log.d(TAG, chosenContact.toString());
|
||||
showOnlyUIView(UIView.MAIN_VIEW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeContact(Contact contact) {
|
||||
mE2EEStrip.removeContact(contact);
|
||||
loadContactsIntoContactsListView();
|
||||
resetChosenContactAndInfoText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyContact(Contact contact) {
|
||||
chosenContact = contact;
|
||||
Log.d(TAG, chosenContact.toString());
|
||||
loadFingerprintInVerifyContactView();
|
||||
showOnlyUIView(UIView.VERIFY_CONTACT_VIEW);
|
||||
}
|
||||
|
||||
public void setRichInputConnection(RichInputConnection richInputConnection) {
|
||||
mRichInputConnection = richInputConnection;
|
||||
}
|
||||
|
||||
public void clearFocusEditTextView() {
|
||||
if (mInputEditText != null) mInputEditText.clearFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection back to the input method.
|
||||
*
|
||||
* @param listener Listener
|
||||
*/
|
||||
public void setListener(final Listener listener, final View inputView) {
|
||||
mListener = listener;
|
||||
mMainKeyboardView = inputView.findViewById(R.id.keyboard_view);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mE2EEMainStrip.removeAllViews();
|
||||
mE2EEStripVisibilityGroup.showE2EEStrip();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onTextInput(final String rawText);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin.e2ee.adapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.Contact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
public class ListAdapterContacts extends ArrayAdapter<Object> {
|
||||
|
||||
private ArrayList<Object> mContacts;
|
||||
private ListAdapterContactInterface mListener;
|
||||
|
||||
public ListAdapterContacts(
|
||||
Context context,
|
||||
int resource,
|
||||
ArrayList<Object> contacts) {
|
||||
super(context, resource, contacts);
|
||||
this.mContacts = contacts;
|
||||
}
|
||||
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
||||
convertView = layoutInflater.inflate(R.layout.e2ee_contact_list_element_view, null, false);
|
||||
}
|
||||
|
||||
final Contact contact = (Contact) getItem(position);
|
||||
|
||||
final TextView firstNameTextView = convertView.findViewById(R.id.e2ee_contact_first_name_element);
|
||||
firstNameTextView.setText(contact.getFirstName());
|
||||
firstNameTextView.setOnClickListener(v -> mListener.selectContact(contact));
|
||||
|
||||
final TextView lastNameTextView = convertView.findViewById(R.id.e2ee_contact_last_name_element);
|
||||
lastNameTextView.setText(contact.getLastName());
|
||||
lastNameTextView.setOnClickListener(v -> mListener.selectContact(contact));
|
||||
|
||||
final ImageButton deleteContactButton = convertView.findViewById(R.id.e2ee_contact_button_delete_contact);
|
||||
deleteContactButton.setOnClickListener(v -> mListener.removeContact(contact));
|
||||
|
||||
final ImageButton verifiedContactButton = convertView.findViewById(R.id.e2ee_verify_contact_verified_button);
|
||||
final ImageButton unverifiedContactButton = convertView.findViewById(R.id.e2ee_verify_contact_unverified_button);
|
||||
if (contact.isVerified()) {
|
||||
verifiedContactButton.setOnClickListener(v -> mListener.verifyContact(contact));
|
||||
verifiedContactButton.setVisibility(View.VISIBLE);
|
||||
unverifiedContactButton.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
unverifiedContactButton.setOnClickListener(v -> mListener.verifyContact(contact));
|
||||
unverifiedContactButton.setVisibility(View.VISIBLE);
|
||||
verifiedContactButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
Contact contact = null;
|
||||
try {
|
||||
contact = (Contact) mContacts.get(position);
|
||||
} catch (ClassCastException e) {
|
||||
LinkedHashMap linkedHashMap = (LinkedHashMap) mContacts.get(position);
|
||||
return new Contact((String) linkedHashMap.get("firstName"),
|
||||
(String) linkedHashMap.get("lastName"),
|
||||
(String) linkedHashMap.get("signalProtocolAddressName"),
|
||||
(Integer) linkedHashMap.get("deviceId"),
|
||||
(Boolean) linkedHashMap.get("verified"));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return contact;
|
||||
}
|
||||
|
||||
public void setListener(final ListAdapterContactInterface listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public interface ListAdapterContactInterface {
|
||||
void selectContact(Contact contact);
|
||||
|
||||
void removeContact(Contact contact);
|
||||
|
||||
void verifyContact(Contact contact);
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin.e2ee.adapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.signalprotocol.chat.StorageMessage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ListAdapterMessages extends ArrayAdapter<Object> {
|
||||
|
||||
private final ArrayList<Object> mListStorageMessages;
|
||||
private final String accountName;
|
||||
private static final String PATTERN_FORMAT = "dd.MM.yyyy HH:mm:ss";
|
||||
|
||||
public ListAdapterMessages(
|
||||
Context context,
|
||||
int resource,
|
||||
ArrayList<Object> listStorageMessages,
|
||||
String accountName) {
|
||||
super(context, resource, listStorageMessages);
|
||||
this.mListStorageMessages = listStorageMessages;
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
|
||||
final StorageMessage message = (StorageMessage) getItem(position);
|
||||
|
||||
if (convertView == null) {
|
||||
LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
||||
convertView = layoutInflater.inflate(R.layout.e2ee_messages_element_view, null, false);
|
||||
}
|
||||
|
||||
final TextView ownMessageTextView = convertView.findViewById(R.id.e2ee_own_messages_text_view_element);
|
||||
final TextView ownMessageTimestampTextView = convertView.findViewById(R.id.e2ee_own_messages_timestamp_text_view_element);
|
||||
|
||||
final TextView othersMessageTextView = convertView.findViewById(R.id.e2ee_others_messages_text_view_element);
|
||||
final TextView othersMessageTimestampTextView = convertView.findViewById(R.id.e2ee_others_messages_timestamp_text_view_element);
|
||||
|
||||
if (message != null && accountName != null && accountName.equals(message.getSenderUUID())) {
|
||||
ownMessageTextView.setText(message.getUnencryptedMessage());
|
||||
ownMessageTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
ownMessageTimestampTextView.setText(formatInstant(message.getTimestamp()));
|
||||
ownMessageTimestampTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
othersMessageTimestampTextView.setVisibility(View.GONE);
|
||||
othersMessageTextView.setVisibility(View.GONE);
|
||||
} else if (message != null && accountName != null && accountName.equals(message.getRecipientUUID())) {
|
||||
othersMessageTextView.setText(message.getUnencryptedMessage());
|
||||
othersMessageTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
othersMessageTimestampTextView.setText(formatInstant(message.getTimestamp()));
|
||||
othersMessageTimestampTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
ownMessageTimestampTextView.setVisibility(View.GONE);
|
||||
ownMessageTextView.setVisibility(View.GONE);
|
||||
}
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
StorageMessage message = (StorageMessage) mListStorageMessages.get(position);
|
||||
return message;
|
||||
}
|
||||
|
||||
private String formatInstant(Instant timestamp) {
|
||||
return DateTimeFormatter.ofPattern(PATTERN_FORMAT)
|
||||
.withZone(ZoneId.systemDefault()).format(timestamp);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.amnesica.kryptey.inputmethod.latin.e2ee.util;
|
||||
|
||||
public class HTMLHelper {
|
||||
public static String replaceHtmlCharacters(String item) {
|
||||
return replaceHtmlNBSPCharacters(item);
|
||||
}
|
||||
|
||||
private static String replaceHtmlNBSPCharacters(String item) {
|
||||
return item.replaceAll("\u00a0", " ").trim();
|
||||
}
|
||||
}
|
@ -0,0 +1,609 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.latin.inputlogic;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.event.Event;
|
||||
import com.amnesica.kryptey.inputmethod.event.InputTransaction;
|
||||
import com.amnesica.kryptey.inputmethod.latin.LatinIME;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputConnection;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.Constants;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.RecapitalizeStatus;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* This class manages the input logic.
|
||||
*/
|
||||
public final class InputLogic {
|
||||
// TODO : Remove this member when we can.
|
||||
final LatinIME mLatinIME;
|
||||
|
||||
// This has package visibility so it can be accessed from InputLogicHandler.
|
||||
public final RichInputConnection mConnection;
|
||||
private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
|
||||
|
||||
public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();
|
||||
|
||||
/**
|
||||
* Create a new instance of the input logic.
|
||||
*
|
||||
* @param latinIME the instance of the parent LatinIME. We should remove this when we can.
|
||||
* dictionary.
|
||||
*/
|
||||
public InputLogic(final LatinIME latinIME) {
|
||||
mLatinIME = latinIME;
|
||||
mConnection = new RichInputConnection(latinIME);
|
||||
// TODO handle better
|
||||
mLatinIME.setRichInputConnection(mConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the input logic for input in an editor.
|
||||
* <p>
|
||||
* Call this when input starts or restarts in some editor (typically, in onStartInputView).
|
||||
*/
|
||||
public void startInput() {
|
||||
mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
|
||||
mCurrentlyPressedHardwareKeys.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when the subtype changes.
|
||||
*/
|
||||
public void onSubtypeChanged() {
|
||||
startInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a string input.
|
||||
* <p>
|
||||
* This is triggered by keys that input many characters at once, like the ".com" key or
|
||||
* some additional keys for example.
|
||||
*
|
||||
* @param settingsValues the current values of the settings.
|
||||
* @param event the input event containing the data.
|
||||
* @return the complete transaction object
|
||||
*/
|
||||
public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event) {
|
||||
final String rawText = event.getTextToCommit().toString();
|
||||
final InputTransaction inputTransaction = new InputTransaction(settingsValues);
|
||||
mConnection.beginBatchEdit();
|
||||
final String text = performSpecificTldProcessingOnTextInput(rawText);
|
||||
mConnection.commitText(text, 1);
|
||||
mConnection.endBatchEdit();
|
||||
// Space state must be updated before calling updateShiftState
|
||||
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
|
||||
return inputTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider an update to the cursor position. Evaluate whether this update has happened as
|
||||
* part of normal typing or whether it was an explicit cursor move by the user. In any case,
|
||||
* do the necessary adjustments.
|
||||
*
|
||||
* @param newSelStart new selection start
|
||||
* @param newSelEnd new selection end
|
||||
* @return whether the cursor has moved as a result of user interaction.
|
||||
*/
|
||||
public boolean onUpdateSelection(final int newSelStart, final int newSelEnd) {
|
||||
resetEntireInputState(newSelStart, newSelEnd);
|
||||
|
||||
// The cursor has been moved : we now accept to perform recapitalization
|
||||
mRecapitalizeStatus.enable();
|
||||
// Stop the last recapitalization, if started.
|
||||
mRecapitalizeStatus.stop();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a code input. It may be a code point to insert, or a symbolic value that influences
|
||||
* the keyboard behavior.
|
||||
* <p>
|
||||
* Typically, this is called whenever a key is pressed on the software keyboard. This is not
|
||||
* the entry point for gesture input; see the onBatchInput* family of functions for this.
|
||||
*
|
||||
* @param settingsValues the current settings values.
|
||||
* @param event the event to handle.
|
||||
* @return the complete transaction object
|
||||
*/
|
||||
public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event) {
|
||||
final InputTransaction inputTransaction = new InputTransaction(settingsValues);
|
||||
mConnection.beginBatchEdit();
|
||||
|
||||
Event currentEvent = event;
|
||||
while (null != currentEvent) {
|
||||
if (currentEvent.isConsumed()) {
|
||||
handleConsumedEvent(currentEvent);
|
||||
} else if (currentEvent.isFunctionalKeyEvent()) {
|
||||
handleFunctionalEvent(currentEvent, inputTransaction);
|
||||
} else {
|
||||
handleNonFunctionalEvent(currentEvent, inputTransaction);
|
||||
}
|
||||
currentEvent = currentEvent.mNextEvent;
|
||||
}
|
||||
mConnection.endBatchEdit();
|
||||
return inputTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a consumed event.
|
||||
* <p>
|
||||
* Consumed events represent events that have already been consumed, typically by the
|
||||
* combining chain.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
*/
|
||||
private void handleConsumedEvent(final Event event) {
|
||||
// A consumed event may have text to commit and an update to the composing state, so
|
||||
// we evaluate both. With some combiners, it's possible than an event contains both
|
||||
// and we enter both of the following if clauses.
|
||||
final CharSequence textToCommit = event.getTextToCommit();
|
||||
if (!TextUtils.isEmpty(textToCommit)) {
|
||||
mConnection.commitText(textToCommit, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a functional key event.
|
||||
* <p>
|
||||
* A functional event is a special key, like delete, shift, emoji, or the settings key.
|
||||
* Non-special keys are those that generate a single code point.
|
||||
* This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
|
||||
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
||||
* any key that results in multiple code points like the ".com" key.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
* @param inputTransaction The transaction in progress.
|
||||
*/
|
||||
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction) {
|
||||
switch (event.mKeyCode) {
|
||||
case Constants.CODE_DELETE:
|
||||
handleBackspaceEvent(event, inputTransaction);
|
||||
// Backspace is a functional key, but it affects the contents of the editor.
|
||||
break;
|
||||
case Constants.CODE_SHIFT:
|
||||
performRecapitalization(inputTransaction.mSettingsValues);
|
||||
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
|
||||
break;
|
||||
case Constants.CODE_CAPSLOCK:
|
||||
// Note: Changing keyboard to shift lock state is handled in
|
||||
// {@link KeyboardSwitcher#onEvent(Event)}.
|
||||
break;
|
||||
case Constants.CODE_SYMBOL_SHIFT:
|
||||
// Note: Calling back to the keyboard on the symbol Shift key is handled in
|
||||
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
|
||||
break;
|
||||
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
|
||||
// Note: Calling back to the keyboard on symbol key is handled in
|
||||
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
|
||||
break;
|
||||
case Constants.CODE_SETTINGS:
|
||||
onSettingsKeyPressed();
|
||||
break;
|
||||
case Constants.CODE_ACTION_NEXT:
|
||||
performEditorAction(EditorInfo.IME_ACTION_NEXT);
|
||||
break;
|
||||
case Constants.CODE_ACTION_PREVIOUS:
|
||||
performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
|
||||
break;
|
||||
case Constants.CODE_LANGUAGE_SWITCH:
|
||||
handleLanguageSwitchKey();
|
||||
break;
|
||||
case Constants.CODE_SHIFT_ENTER:
|
||||
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
|
||||
event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
|
||||
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction);
|
||||
// Shift + Enter is treated as a functional key but it results in adding a new
|
||||
// line, so that does affect the contents of the editor.
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown key code : " + event.mKeyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an event that is not a functional event.
|
||||
* <p>
|
||||
* These events are generally events that cause input, but in some cases they may do other
|
||||
* things like trigger an editor action.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
* @param inputTransaction The transaction in progress.
|
||||
*/
|
||||
private void handleNonFunctionalEvent(final Event event,
|
||||
final InputTransaction inputTransaction) {
|
||||
switch (event.mCodePoint) {
|
||||
case Constants.CODE_ENTER:
|
||||
final EditorInfo editorInfo = getCurrentInputEditorInfo();
|
||||
final int imeOptionsActionId =
|
||||
InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
|
||||
if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
|
||||
// Either we have an actionLabel and we should performEditorAction with
|
||||
// actionId regardless of its value.
|
||||
performEditorAction(editorInfo.actionId);
|
||||
} else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
|
||||
// We didn't have an actionLabel, but we had another action to execute.
|
||||
// EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
|
||||
// EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
|
||||
// means there should be an action and the app didn't bother to set a specific
|
||||
// code for it - presumably it only handles one. It does not have to be treated
|
||||
// in any specific way: anything that is not IME_ACTION_NONE should be sent to
|
||||
// performEditorAction.
|
||||
performEditorAction(imeOptionsActionId);
|
||||
} else {
|
||||
// No action label, and the action from imeOptions is NONE: this is a regular
|
||||
// enter key that should input a carriage return.
|
||||
handleNonSpecialCharacterEvent(event, inputTransaction);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handleNonSpecialCharacterEvent(event, inputTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inputting a code point to the editor.
|
||||
* <p>
|
||||
* Non-special keys are those that generate a single code point.
|
||||
* This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
|
||||
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
||||
* any key that results in multiple code points like the ".com" key.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
* @param inputTransaction The transaction in progress.
|
||||
*/
|
||||
private void handleNonSpecialCharacterEvent(final Event event,
|
||||
final InputTransaction inputTransaction) {
|
||||
final int codePoint = event.mCodePoint;
|
||||
if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
|
||||
|| Character.getType(codePoint) == Character.OTHER_SYMBOL) {
|
||||
handleSeparatorEvent(event, inputTransaction);
|
||||
} else {
|
||||
handleNonSeparatorEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a non-separator.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
*/
|
||||
private void handleNonSeparatorEvent(final Event event) {
|
||||
mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input of a separator code point.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
* @param inputTransaction The transaction in progress.
|
||||
*/
|
||||
private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction) {
|
||||
mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);
|
||||
|
||||
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a press on the backspace key.
|
||||
*
|
||||
* @param event The event to handle.
|
||||
* @param inputTransaction The transaction in progress.
|
||||
*/
|
||||
private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction) {
|
||||
// In many cases after backspace, we need to update the shift state. Normally we need
|
||||
// to do this right away to avoid the shift state being out of date in case the user types
|
||||
// backspace then some other character very fast. However, in the case of backspace key
|
||||
// repeat, this can lead to flashiness when the cursor flies over positions where the
|
||||
// shift state should be updated, so if this is a key repeat, we update after a small delay.
|
||||
// Then again, even in the case of a key repeat, if the cursor is at start of text, it
|
||||
// can't go any further back, so we can update right away even if it's a key repeat.
|
||||
final int shiftUpdateKind =
|
||||
event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
|
||||
? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
|
||||
inputTransaction.requireShiftUpdate(shiftUpdateKind);
|
||||
|
||||
// No cancelling of commit/double space/swap: we have a regular backspace.
|
||||
// We should backspace one char and restart suggestion if at the end of a word.
|
||||
if (mConnection.hasSelection()) {
|
||||
// If there is a selection, remove it.
|
||||
// We also need to unlearn the selected text.
|
||||
final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
|
||||
- mConnection.getExpectedSelectionStart();
|
||||
mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
|
||||
mConnection.getExpectedSelectionEnd());
|
||||
mConnection.deleteTextBeforeCursor(numCharsDeleted);
|
||||
} else {
|
||||
// There is no selection, just delete one character.
|
||||
if (inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()
|
||||
|| Constants.NOT_A_CURSOR_POSITION
|
||||
== mConnection.getExpectedSelectionEnd()) {
|
||||
// There are three possible reasons to send a key event: either the field has
|
||||
// type TYPE_NULL, in which case the keyboard should send events, or we are
|
||||
// running in backward compatibility mode, or we don't know the cursor position.
|
||||
// Before Jelly bean, the keyboard would simulate a hardware keyboard event on
|
||||
// pressing enter or delete. This is bad for many reasons (there are race
|
||||
// conditions with commits) but some applications are relying on this behavior
|
||||
// so we continue to support it for older apps, so we retain this behavior if
|
||||
// the app has target SDK < JellyBean.
|
||||
// As for the case where we don't know the cursor position, it can happen
|
||||
// because of bugs in the framework. But the framework should know, so the next
|
||||
// best thing is to leave it to whatever it thinks is best.
|
||||
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
|
||||
} else {
|
||||
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
|
||||
if (codePointBeforeCursor == Constants.NOT_A_CODE) {
|
||||
// HACK for backward compatibility with broken apps that haven't realized
|
||||
// yet that hardware keyboards are not the only way of inputting text.
|
||||
// Nothing to delete before the cursor. We should not do anything, but many
|
||||
// broken apps expect something to happen in this case so that they can
|
||||
// catch it and have their broken interface react. If you need the keyboard
|
||||
// to do this, you're doing it wrong -- please fix your app.
|
||||
mConnection.deleteTextBeforeCursor(1);
|
||||
// TODO: Add a new StatsUtils method onBackspaceWhenNoText()
|
||||
return;
|
||||
}
|
||||
final int lengthToDelete =
|
||||
Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
|
||||
mConnection.deleteTextBeforeCursor(lengthToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a press on the language switch key (the "globe key")
|
||||
*/
|
||||
private void handleLanguageSwitchKey() {
|
||||
mLatinIME.switchToNextSubtype();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a recapitalization event.
|
||||
*
|
||||
* @param settingsValues The current settings values.
|
||||
*/
|
||||
private void performRecapitalization(final SettingsValues settingsValues) {
|
||||
if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) {
|
||||
return; // No selection or recapitalize is disabled for now
|
||||
}
|
||||
final int selectionStart = mConnection.getExpectedSelectionStart();
|
||||
final int selectionEnd = mConnection.getExpectedSelectionEnd();
|
||||
final int numCharsSelected = selectionEnd - selectionStart;
|
||||
if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) {
|
||||
// We bail out if we have too many characters for performance reasons. We don't want
|
||||
// to suck possibly multiple-megabyte data.
|
||||
return;
|
||||
}
|
||||
// If we have a recapitalize in progress, use it; otherwise, start a new one.
|
||||
if (!mRecapitalizeStatus.isStarted()
|
||||
|| !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) {
|
||||
final CharSequence selectedText =
|
||||
mConnection.getSelectedText(0 /* flags, 0 for no styles */);
|
||||
if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
|
||||
mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(), mLatinIME.getCurrentLayoutLocale());
|
||||
// We trim leading and trailing whitespace.
|
||||
mRecapitalizeStatus.trim();
|
||||
}
|
||||
mConnection.finishComposingText();
|
||||
mRecapitalizeStatus.rotate();
|
||||
mConnection.setSelection(selectionEnd, selectionEnd);
|
||||
mConnection.deleteTextBeforeCursor(numCharsSelected);
|
||||
mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
|
||||
mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
|
||||
mRecapitalizeStatus.getNewCursorEnd());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current auto-caps state, factoring in the space state.
|
||||
* <p>
|
||||
* This method tries its best to do this in the most efficient possible manner. It avoids
|
||||
* getting text from the editor if possible at all.
|
||||
* This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
|
||||
* needs to know auto caps state to display the right layout.
|
||||
*
|
||||
* @param settingsValues the relevant settings values
|
||||
* @param layoutSetName the name of the current keyboard layout set
|
||||
* @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
|
||||
*/
|
||||
public int getCurrentAutoCapsState(final SettingsValues settingsValues,
|
||||
final String layoutSetName) {
|
||||
if (!settingsValues.mAutoCap || !layoutUsesAutoCaps(layoutSetName)) {
|
||||
return Constants.TextUtils.CAP_MODE_OFF;
|
||||
}
|
||||
|
||||
final EditorInfo ei = getCurrentInputEditorInfo();
|
||||
if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
|
||||
final int inputType = ei.inputType;
|
||||
// Warning: this depends on mSpaceState, which may not be the most current value. If
|
||||
// mSpaceState gets updated later, whoever called this may need to be told about it.
|
||||
return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations);
|
||||
}
|
||||
|
||||
private boolean layoutUsesAutoCaps(final String layoutSetName) {
|
||||
switch (layoutSetName) {
|
||||
case SubtypeLocaleUtils.LAYOUT_ARABIC:
|
||||
case SubtypeLocaleUtils.LAYOUT_BENGALI:
|
||||
case SubtypeLocaleUtils.LAYOUT_BENGALI_AKKHOR:
|
||||
case SubtypeLocaleUtils.LAYOUT_FARSI:
|
||||
case SubtypeLocaleUtils.LAYOUT_GEORGIAN:
|
||||
case SubtypeLocaleUtils.LAYOUT_HEBREW:
|
||||
case SubtypeLocaleUtils.LAYOUT_HINDI:
|
||||
case SubtypeLocaleUtils.LAYOUT_HINDI_COMPACT:
|
||||
case SubtypeLocaleUtils.LAYOUT_KANNADA:
|
||||
case SubtypeLocaleUtils.LAYOUT_KHMER:
|
||||
case SubtypeLocaleUtils.LAYOUT_LAO:
|
||||
case SubtypeLocaleUtils.LAYOUT_MALAYALAM:
|
||||
case SubtypeLocaleUtils.LAYOUT_MARATHI:
|
||||
case SubtypeLocaleUtils.LAYOUT_NEPALI_ROMANIZED:
|
||||
case SubtypeLocaleUtils.LAYOUT_NEPALI_TRADITIONAL:
|
||||
case SubtypeLocaleUtils.LAYOUT_TAMIL:
|
||||
case SubtypeLocaleUtils.LAYOUT_TELUGU:
|
||||
case SubtypeLocaleUtils.LAYOUT_THAI:
|
||||
case SubtypeLocaleUtils.LAYOUT_URDU:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public int getCurrentRecapitalizeState() {
|
||||
if (!mRecapitalizeStatus.isStarted()
|
||||
|| !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
|
||||
mConnection.getExpectedSelectionEnd())) {
|
||||
// Not recapitalizing at the moment
|
||||
return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
|
||||
}
|
||||
return mRecapitalizeStatus.getCurrentMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the editor info for the current editor
|
||||
*/
|
||||
private EditorInfo getCurrentInputEditorInfo() {
|
||||
return mLatinIME.getCurrentInputEditorInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param actionId the action to perform
|
||||
*/
|
||||
private void performEditorAction(final int actionId) {
|
||||
mConnection.performEditorAction(actionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the processing specific to inputting TLDs.
|
||||
* <p>
|
||||
* Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
|
||||
* processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
|
||||
* of character in onCodeInput, but since this gets inputted as a whole string we need to
|
||||
* do it here specifically. Then, if the last character before the cursor is a period, then
|
||||
* we cut the dot at the start of ".com". This is because humans tend to type "www.google."
|
||||
* and then press the ".com" key and instinctively don't expect to get "www.google..com".
|
||||
*
|
||||
* @param text the raw text supplied to onTextInput
|
||||
* @return the text to actually send to the editor
|
||||
*/
|
||||
private String performSpecificTldProcessingOnTextInput(final String text) {
|
||||
if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
|
||||
|| !Character.isLetter(text.charAt(1))) {
|
||||
// Not a tld: do nothing.
|
||||
return text;
|
||||
}
|
||||
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
|
||||
// If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
|
||||
if (Constants.CODE_PERIOD == codePointBeforeCursor) {
|
||||
return text.substring(1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a press on the settings key.
|
||||
*/
|
||||
private void onSettingsKeyPressed() {
|
||||
mLatinIME.launchSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the whole input state to the starting state.
|
||||
* <p>
|
||||
* This will clear the composing word, reset the last composed word, clear the suggestion
|
||||
* strip and tell the input connection about it so that it can refresh its caches.
|
||||
*
|
||||
* @param newSelStart the new selection start, in java characters.
|
||||
* @param newSelEnd the new selection end, in java characters.
|
||||
*/
|
||||
// TODO: how is this different from startInput ?!
|
||||
private void resetEntireInputState(final int newSelStart, final int newSelEnd) {
|
||||
mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DOWN key event followed by an UP key event to the editor.
|
||||
* <p>
|
||||
* If possible at all, avoid using this method. It causes all sorts of race conditions with
|
||||
* the text view because it goes through a different, asynchronous binder. Also, batch edits
|
||||
* are ignored for key events. Use the normal software input methods instead.
|
||||
*
|
||||
* @param keyCode the key code to send inside the key event.
|
||||
*/
|
||||
public void sendDownUpKeyEvent(final int keyCode) {
|
||||
final long eventTime = SystemClock.uptimeMillis();
|
||||
mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
|
||||
KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
|
||||
mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
|
||||
KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
|
||||
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a code point to the editor, using the most appropriate method.
|
||||
* <p>
|
||||
* Normally we send code points with commitText, but there are some cases (where backward
|
||||
* compatibility is a concern for example) where we want to use deprecated methods.
|
||||
*
|
||||
* @param codePoint the code point to send.
|
||||
*/
|
||||
// TODO: replace these two parameters with an InputTransaction
|
||||
private void sendKeyCodePoint(final int codePoint) {
|
||||
// TODO: Remove this special handling of digit letters.
|
||||
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
|
||||
if (codePoint >= '0' && codePoint <= '9') {
|
||||
sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
|
||||
return;
|
||||
}
|
||||
|
||||
mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry resetting caches in the rich input connection.
|
||||
* <p>
|
||||
* When the editor can't be accessed we can't reset the caches, so we schedule a retry.
|
||||
* This method handles the retry, and re-schedules a new retry if we still can't access.
|
||||
* We only retry up to 5 times before giving up.
|
||||
*
|
||||
* @param tryResumeSuggestions Whether we should resume suggestions or not.
|
||||
* @param remainingTries How many times we may try again before giving up.
|
||||
* @return whether true if the caches were successfully reset, false otherwise.
|
||||
*/
|
||||
public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions,
|
||||
final int remainingTries, final LatinIME.UIHandler handler) {
|
||||
if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
|
||||
mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd())) {
|
||||
if (0 < remainingTries) {
|
||||
handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
|
||||
return false;
|
||||
}
|
||||
// If remainingTries is 0, we should stop waiting for new tries, however we'll still
|
||||
// return true as we need to perform other tasks (for example, loading the keyboard).
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardTheme;
|
||||
|
||||
/**
|
||||
* "Appearance" settings sub screen.
|
||||
*/
|
||||
public final class AppearanceSettingsFragment extends SubScreenFragment {
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
addPreferencesFromResource(R.xml.prefs_screen_appearance);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
removePreference(Settings.PREF_MATCHING_NAVBAR_COLOR);
|
||||
}
|
||||
|
||||
setupKeyboardHeightSettings();
|
||||
setupKeyboardColorSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
refreshSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
refreshSettings();
|
||||
}
|
||||
|
||||
private void refreshSettings() {
|
||||
ThemeSettingsFragment.updateKeyboardThemeSummary(findPreference(Settings.SCREEN_THEME));
|
||||
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final KeyboardTheme theme = KeyboardTheme.getKeyboardTheme(prefs);
|
||||
setPreferenceEnabled(Settings.PREF_KEYBOARD_COLOR, false);
|
||||
}
|
||||
|
||||
private void setupKeyboardHeightSettings() {
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference) findPreference(
|
||||
Settings.PREF_KEYBOARD_HEIGHT);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
|
||||
private static final float PERCENTAGE_FLOAT = 100.0f;
|
||||
|
||||
private float getValueFromPercentage(final int percentage) {
|
||||
return percentage / PERCENTAGE_FLOAT;
|
||||
}
|
||||
|
||||
private int getPercentageFromValue(final float floatValue) {
|
||||
return Math.round(floatValue * PERCENTAGE_FLOAT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeValue(final int value, final String key) {
|
||||
prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDefaultValue(final String key) {
|
||||
prefs.edit().remove(key).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readValue(final String key) {
|
||||
return getPercentageFromValue(Settings.readKeyboardHeight(prefs, 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readDefaultValue(final String key) {
|
||||
return getPercentageFromValue(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueText(final int value) {
|
||||
if (value < 0) {
|
||||
return res.getString(R.string.settings_system_default);
|
||||
}
|
||||
return res.getString(R.string.abbreviation_unit_percent, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackValue(final int value) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupKeyboardColorSettings() {
|
||||
final ColorDialogPreference pref = (ColorDialogPreference) findPreference(
|
||||
Settings.PREF_KEYBOARD_COLOR);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Context context = this.getActivity().getApplicationContext();
|
||||
pref.setInterface(new ColorDialogPreference.ValueProxy() {
|
||||
@Override
|
||||
public void writeValue(final int value, final String key) {
|
||||
prefs.edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readValue(final String key) {
|
||||
return Settings.readKeyboardColor(prefs, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDefaultValue(final String key) {
|
||||
Settings.removeKeyboardColor(prefs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.latin.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
|
||||
public final class ColorDialogPreference extends DialogPreference
|
||||
implements SeekBar.OnSeekBarChangeListener {
|
||||
public interface ValueProxy {
|
||||
int readValue(final String key);
|
||||
|
||||
void writeDefaultValue(final String key);
|
||||
|
||||
void writeValue(final int value, final String key);
|
||||
}
|
||||
|
||||
private TextView mValueView;
|
||||
private SeekBar mSeekBarRed;
|
||||
private SeekBar mSeekBarGreen;
|
||||
private SeekBar mSeekBarBlue;
|
||||
|
||||
private ValueProxy mValueProxy;
|
||||
|
||||
public ColorDialogPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setDialogLayoutResource(R.layout.color_dialog);
|
||||
}
|
||||
|
||||
public void setInterface(final ValueProxy proxy) {
|
||||
mValueProxy = proxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
final View view = super.onCreateDialogView();
|
||||
mSeekBarRed = view.findViewById(R.id.seek_bar_dialog_bar_red);
|
||||
mSeekBarRed.setMax(255);
|
||||
mSeekBarRed.setOnSeekBarChangeListener(this);
|
||||
mSeekBarRed.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
mSeekBarRed.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
mSeekBarGreen = view.findViewById(R.id.seek_bar_dialog_bar_green);
|
||||
mSeekBarGreen.setMax(255);
|
||||
mSeekBarGreen.setOnSeekBarChangeListener(this);
|
||||
mSeekBarGreen.getThumb().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_IN);
|
||||
mSeekBarGreen.getProgressDrawable().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_IN);
|
||||
mSeekBarBlue = view.findViewById(R.id.seek_bar_dialog_bar_blue);
|
||||
mSeekBarBlue.setMax(255);
|
||||
mSeekBarBlue.setOnSeekBarChangeListener(this);
|
||||
mSeekBarBlue.getThumb().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
|
||||
mSeekBarBlue.getProgressDrawable().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_IN);
|
||||
mValueView = view.findViewById(R.id.seek_bar_dialog_value);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(final View view) {
|
||||
final int color = mValueProxy.readValue(getKey());
|
||||
mSeekBarRed.setProgress(Color.red(color));
|
||||
mSeekBarGreen.setProgress(Color.green(color));
|
||||
mSeekBarBlue.setProgress(Color.blue(color));
|
||||
setHeaderText(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
|
||||
builder.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, this)
|
||||
.setNeutralButton(R.string.button_default, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
super.onClick(dialog, which);
|
||||
final String key = getKey();
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
super.onClick(dialog, which);
|
||||
final int value = Color.rgb(
|
||||
mSeekBarRed.getProgress(),
|
||||
mSeekBarGreen.getProgress(),
|
||||
mSeekBarBlue.getProgress());
|
||||
mValueProxy.writeValue(value, key);
|
||||
return;
|
||||
}
|
||||
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||
super.onClick(dialog, which);
|
||||
mValueProxy.writeDefaultValue(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
||||
final boolean fromUser) {
|
||||
int color = Color.rgb(
|
||||
mSeekBarRed.getProgress(),
|
||||
mSeekBarGreen.getProgress(),
|
||||
mSeekBarBlue.getProgress());
|
||||
setHeaderText(color);
|
||||
if (!fromUser) {
|
||||
seekBar.setProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
private void setHeaderText(int color) {
|
||||
mValueView.setText(getValueText(color));
|
||||
boolean bright = Color.red(color) + Color.green(color) + Color.blue(color) > 128 * 3;
|
||||
mValueView.setTextColor(bright ? Color.BLACK : Color.WHITE);
|
||||
mValueView.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
private String getValueText(final int value) {
|
||||
String temp = Integer.toHexString(value);
|
||||
for (; temp.length() < 8; temp = "0" + temp) ;
|
||||
return temp.substring(2).toUpperCase();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
|
||||
|
||||
/**
|
||||
* This is a helper class for an IME's settings preference fragment. It's recommended for every
|
||||
* IME to have its own settings preference fragment which inherits this class.
|
||||
*/
|
||||
public abstract class InputMethodSettingsFragment extends PreferenceFragment {
|
||||
private final InputMethodSettingsImpl mSettings = new InputMethodSettingsImpl();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Context context = getActivity();
|
||||
setPreferenceScreen(getPreferenceManager().createPreferenceScreen(
|
||||
PreferenceManagerCompat.getDeviceContext(context)));
|
||||
mSettings.init(context, getPreferenceScreen());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mSettings.updateEnabledSubtypeList();
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputMethodManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/* package private */ class InputMethodSettingsImpl {
|
||||
private Preference mSubtypeEnablerPreference;
|
||||
private RichInputMethodManager mRichImm;
|
||||
|
||||
/**
|
||||
* Initialize internal states of this object.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
* @param prefScreen a PreferenceScreen of PreferenceActivity or PreferenceFragment.
|
||||
* @return true if this application is an IME and has two or more subtypes, false otherwise.
|
||||
*/
|
||||
public boolean init(final Context context, final PreferenceScreen prefScreen) {
|
||||
RichInputMethodManager.init(context);
|
||||
mRichImm = RichInputMethodManager.getInstance();
|
||||
|
||||
mSubtypeEnablerPreference = new Preference(context);
|
||||
mSubtypeEnablerPreference.setTitle(R.string.select_language);
|
||||
mSubtypeEnablerPreference.setFragment(LanguagesSettingsFragment.class.getName());
|
||||
prefScreen.addPreference(mSubtypeEnablerPreference);
|
||||
updateEnabledSubtypeList();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String getEnabledSubtypesLabel(final RichInputMethodManager richImm) {
|
||||
if (richImm == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Set<Subtype> subtypes = richImm.getEnabledSubtypes(true);
|
||||
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final Subtype subtype : subtypes) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(subtype.getName());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void updateEnabledSubtypeList() {
|
||||
if (mSubtypeEnablerPreference != null) {
|
||||
final String summary = getEnabledSubtypesLabel(mRichImm);
|
||||
if (!TextUtils.isEmpty(summary)) {
|
||||
mSubtypeEnablerPreference.setSummary(summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.AudioAndHapticFeedbackManager;
|
||||
|
||||
/**
|
||||
* "Preferences" settings sub screen.
|
||||
* <p>
|
||||
* This settings sub screen handles the following input preferences.
|
||||
* - Vibrate on keypress
|
||||
* - Keypress vibration duration
|
||||
* - Sound on keypress
|
||||
* - Keypress sound volume
|
||||
* - Popup on keypress
|
||||
* - Key long press delay
|
||||
*/
|
||||
public final class KeyPressSettingsFragment extends SubScreenFragment {
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
addPreferencesFromResource(R.xml.prefs_screen_key_press);
|
||||
|
||||
final Context context = getActivity();
|
||||
|
||||
// When we are called from the Settings application but we are not already running, some
|
||||
// singleton and utility classes may not have been initialized. We have to call
|
||||
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
|
||||
AudioAndHapticFeedbackManager.init(context);
|
||||
|
||||
if (!AudioAndHapticFeedbackManager.getInstance().hasVibrator()) {
|
||||
removePreference(Settings.PREF_VIBRATE_ON);
|
||||
removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS);
|
||||
}
|
||||
|
||||
setupKeypressVibrationDurationSettings();
|
||||
setupKeypressSoundVolumeSettings();
|
||||
setupKeyLongpressTimeoutSettings();
|
||||
}
|
||||
|
||||
private void setupKeypressVibrationDurationSettings() {
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference) findPreference(
|
||||
Settings.PREF_VIBRATION_DURATION_SETTINGS);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
|
||||
@Override
|
||||
public void writeValue(final int value, final String key) {
|
||||
prefs.edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDefaultValue(final String key) {
|
||||
prefs.edit().remove(key).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readValue(final String key) {
|
||||
return Settings.readKeypressVibrationDuration(prefs, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readDefaultValue(final String key) {
|
||||
return Settings.readDefaultKeypressVibrationDuration(res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackValue(final int value) {
|
||||
AudioAndHapticFeedbackManager.getInstance().vibrate(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueText(final int value) {
|
||||
if (value < 0) {
|
||||
return res.getString(R.string.settings_system_default);
|
||||
}
|
||||
return res.getString(R.string.abbreviation_unit_milliseconds, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupKeypressSoundVolumeSettings() {
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference) findPreference(
|
||||
Settings.PREF_KEYPRESS_SOUND_VOLUME);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
final AudioManager am = (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
|
||||
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
|
||||
private static final float PERCENTAGE_FLOAT = 100.0f;
|
||||
|
||||
private float getValueFromPercentage(final int percentage) {
|
||||
return percentage / PERCENTAGE_FLOAT;
|
||||
}
|
||||
|
||||
private int getPercentageFromValue(final float floatValue) {
|
||||
return (int) (floatValue * PERCENTAGE_FLOAT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeValue(final int value, final String key) {
|
||||
prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDefaultValue(final String key) {
|
||||
prefs.edit().remove(key).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readValue(final String key) {
|
||||
return getPercentageFromValue(Settings.readKeypressSoundVolume(prefs, res));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readDefaultValue(final String key) {
|
||||
return getPercentageFromValue(Settings.readDefaultKeypressSoundVolume(res));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueText(final int value) {
|
||||
if (value < 0) {
|
||||
return res.getString(R.string.settings_system_default);
|
||||
}
|
||||
return Integer.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackValue(final int value) {
|
||||
am.playSoundEffect(
|
||||
AudioManager.FX_KEYPRESS_STANDARD, getValueFromPercentage(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupKeyLongpressTimeoutSettings() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final Resources res = getResources();
|
||||
final SeekBarDialogPreference pref = (SeekBarDialogPreference) findPreference(
|
||||
Settings.PREF_KEY_LONGPRESS_TIMEOUT);
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
|
||||
@Override
|
||||
public void writeValue(final int value, final String key) {
|
||||
prefs.edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDefaultValue(final String key) {
|
||||
prefs.edit().remove(key).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readValue(final String key) {
|
||||
return Settings.readKeyLongpressTimeout(prefs, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readDefaultValue(final String key) {
|
||||
return Settings.readDefaultKeyLongpressTimeout(res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValueText(final int value) {
|
||||
return res.getString(R.string.abbreviation_unit_milliseconds, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackValue(final int value) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,398 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import static com.amnesica.kryptey.inputmethod.latin.settings.SingleLanguageSettingsFragment.LOCALE_BUNDLE_KEY;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.compat.MenuItemIconColorCompat;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputMethodManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.LocaleUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* "Languages" settings sub screen.
|
||||
*/
|
||||
public final class LanguagesSettingsFragment extends PreferenceFragment {
|
||||
private static final String TAG = LanguagesSettingsFragment.class.getSimpleName();
|
||||
|
||||
private static final boolean DEBUG_SUBTYPE_ID = false;
|
||||
|
||||
private RichInputMethodManager mRichImm;
|
||||
private CharSequence[] mUsedLocaleNames;
|
||||
private String[] mUsedLocaleValues;
|
||||
private CharSequence[] mUnusedLocaleNames;
|
||||
private String[] mUnusedLocaleValues;
|
||||
private AlertDialog mAlertDialog;
|
||||
private View mView;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
RichInputMethodManager.init(getActivity());
|
||||
mRichImm = RichInputMethodManager.getInstance();
|
||||
|
||||
addPreferencesFromResource(R.xml.empty_settings);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
mView = super.onCreateView(inflater, container, savedInstanceState);
|
||||
return mView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
buildContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.remove_language, menu);
|
||||
inflater.inflate(R.menu.add_language, menu);
|
||||
|
||||
MenuItem addLanguageMenuItem = menu.findItem(R.id.action_add_language);
|
||||
MenuItemIconColorCompat.matchMenuIconColor(mView, addLanguageMenuItem,
|
||||
getActivity().getActionBar());
|
||||
MenuItem removeLanguageMenuItem = menu.findItem(R.id.action_remove_language);
|
||||
MenuItemIconColorCompat.matchMenuIconColor(mView, removeLanguageMenuItem,
|
||||
getActivity().getActionBar());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_add_language) {
|
||||
showAddLanguagePopup();
|
||||
} else if (itemId == R.id.action_remove_language) {
|
||||
showRemoveLanguagePopup();
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
if (mUsedLocaleNames != null) {
|
||||
menu.findItem(R.id.action_remove_language).setVisible(mUsedLocaleNames.length > 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the preferences and them to this settings screen.
|
||||
*/
|
||||
private void buildContent() {
|
||||
final Context context = getActivity();
|
||||
final PreferenceGroup group = getPreferenceScreen();
|
||||
group.removeAll();
|
||||
|
||||
final PreferenceCategory languageCategory = new PreferenceCategory(context);
|
||||
languageCategory.setTitle(R.string.user_languages);
|
||||
group.addPreference(languageCategory);
|
||||
|
||||
final Comparator<Locale> comparator = new LocaleUtils.LocaleComparator();
|
||||
final Set<Subtype> enabledSubtypes = mRichImm.getEnabledSubtypes(false);
|
||||
final SortedSet<Locale> usedLocales = getUsedLocales(enabledSubtypes, comparator);
|
||||
final SortedSet<Locale> unusedLocales = getUnusedLocales(usedLocales, comparator);
|
||||
|
||||
buildLanguagePreferences(usedLocales, group, context);
|
||||
setLocaleEntries(usedLocales, unusedLocales);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the unique languages from the subtypes that have been enabled.
|
||||
*
|
||||
* @param subtypes the list of subtypes for this IME that have been enabled.
|
||||
* @param comparator the comparator to sort the languages.
|
||||
* @return a set of locales for the used languages sorted using the specified comparator.
|
||||
*/
|
||||
private SortedSet<Locale> getUsedLocales(final Set<Subtype> subtypes,
|
||||
final Comparator<Locale> comparator) {
|
||||
final SortedSet<Locale> locales = new TreeSet<>(comparator);
|
||||
|
||||
for (final Subtype subtype : subtypes) {
|
||||
if (DEBUG_SUBTYPE_ID) {
|
||||
Log.d(TAG, String.format("Enabled subtype: %-6s 0x%08x %11d %s",
|
||||
subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
|
||||
subtype.getName()));
|
||||
}
|
||||
locales.add(subtype.getLocaleObject());
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of languages supported by this IME that aren't included in
|
||||
* {@link #getUsedLocales}.
|
||||
*
|
||||
* @param usedLocales the used locales.
|
||||
* @param comparator the comparator to sort the languages.
|
||||
* @return a set of locales for the unused languages sorted using the specified comparator.
|
||||
*/
|
||||
private SortedSet<Locale> getUnusedLocales(final Set<Locale> usedLocales,
|
||||
final Comparator<Locale> comparator) {
|
||||
final SortedSet<Locale> locales = new TreeSet<>(comparator);
|
||||
for (String localeString : SubtypeLocaleUtils.getSupportedLocales()) {
|
||||
final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
|
||||
if (usedLocales.contains(locale)) {
|
||||
continue;
|
||||
}
|
||||
locales.add(locale);
|
||||
}
|
||||
return locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a language preference for each of the specified locales in the preference group. These
|
||||
* preferences will be added to the group in the order of the locales that are passed in.
|
||||
*
|
||||
* @param locales the locales to add preferences for.
|
||||
* @param group the preference group to add preferences to.
|
||||
* @param context the context for this application.
|
||||
*/
|
||||
private void buildLanguagePreferences(final SortedSet<Locale> locales,
|
||||
final PreferenceGroup group, final Context context) {
|
||||
for (final Locale locale : locales) {
|
||||
final String localeString = LocaleUtils.getLocaleString(locale);
|
||||
final SingleLanguagePreference pref =
|
||||
new SingleLanguagePreference(context, localeString);
|
||||
group.addPreference(pref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lists of used languages that can be removed and unused languages that can be added.
|
||||
*
|
||||
* @param usedLocales the enabled locales for this IME.
|
||||
* @param unusedLocales the unused locales that are supported in this IME.
|
||||
*/
|
||||
private void setLocaleEntries(final SortedSet<Locale> usedLocales,
|
||||
final SortedSet<Locale> unusedLocales) {
|
||||
mUsedLocaleNames = new CharSequence[usedLocales.size()];
|
||||
mUsedLocaleValues = new String[usedLocales.size()];
|
||||
int i = 0;
|
||||
for (Locale locale : usedLocales) {
|
||||
final String localeString = LocaleUtils.getLocaleString(locale);
|
||||
mUsedLocaleValues[i] = localeString;
|
||||
mUsedLocaleNames[i] =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(localeString);
|
||||
i++;
|
||||
}
|
||||
|
||||
mUnusedLocaleNames = new CharSequence[unusedLocales.size()];
|
||||
mUnusedLocaleValues = new String[unusedLocales.size()];
|
||||
i = 0;
|
||||
for (Locale locale : unusedLocales) {
|
||||
final String localeString = LocaleUtils.getLocaleString(locale);
|
||||
mUnusedLocaleValues[i] = localeString;
|
||||
mUnusedLocaleNames[i] =
|
||||
LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(localeString);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the popup to add a new language.
|
||||
*/
|
||||
private void showAddLanguagePopup() {
|
||||
showMultiChoiceDialog(mUnusedLocaleNames, R.string.add_language, R.string.add, true,
|
||||
new OnMultiChoiceDialogAcceptListener() {
|
||||
@Override
|
||||
public void onClick(boolean[] checkedItems) {
|
||||
// enable the default layout for all of the checked languages
|
||||
for (int i = 0; i < checkedItems.length; i++) {
|
||||
if (!checkedItems[i]) {
|
||||
continue;
|
||||
}
|
||||
final Subtype subtype = SubtypeLocaleUtils.getDefaultSubtype(
|
||||
mUnusedLocaleValues[i],
|
||||
LanguagesSettingsFragment.this.getResources());
|
||||
mRichImm.addSubtype(subtype);
|
||||
}
|
||||
|
||||
// refresh the list of enabled languages
|
||||
getActivity().invalidateOptionsMenu();
|
||||
buildContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the popup to remove an existing language.
|
||||
*/
|
||||
private void showRemoveLanguagePopup() {
|
||||
showMultiChoiceDialog(mUsedLocaleNames, R.string.remove_language, R.string.remove, false,
|
||||
new OnMultiChoiceDialogAcceptListener() {
|
||||
@Override
|
||||
public void onClick(boolean[] checkedItems) {
|
||||
// disable the layouts for all of the checked languages
|
||||
for (int i = 0; i < checkedItems.length; i++) {
|
||||
if (!checkedItems[i]) {
|
||||
continue;
|
||||
}
|
||||
final Set<Subtype> subtypes =
|
||||
mRichImm.getEnabledSubtypesForLocale(mUsedLocaleValues[i]);
|
||||
for (final Subtype subtype : subtypes) {
|
||||
mRichImm.removeSubtype(subtype);
|
||||
}
|
||||
}
|
||||
|
||||
// refresh the list of enabled languages
|
||||
getActivity().invalidateOptionsMenu();
|
||||
buildContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a multi-select popup.
|
||||
*
|
||||
* @param names the list of the choice display names.
|
||||
* @param titleRes the title of the dialog.
|
||||
* @param positiveButtonRes the text for the positive button.
|
||||
* @param allowAllChecked whether the positive button should be enabled when all items are
|
||||
* checked.
|
||||
* @param listener the listener for when the user clicks the positive button.
|
||||
*/
|
||||
private void showMultiChoiceDialog(final CharSequence[] names, final int titleRes,
|
||||
final int positiveButtonRes, final boolean allowAllChecked,
|
||||
final OnMultiChoiceDialogAcceptListener listener) {
|
||||
final boolean[] checkedItems = new boolean[names.length];
|
||||
mAlertDialog = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(titleRes)
|
||||
.setMultiChoiceItems(names, checkedItems,
|
||||
new DialogInterface.OnMultiChoiceClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialogInterface,
|
||||
final int which, final boolean isChecked) {
|
||||
// make sure the positive button is only enabled when at least one
|
||||
// item is checked and when not all of the items are checked (unless
|
||||
// allowAllChecked is true)
|
||||
boolean hasCheckedItem = false;
|
||||
boolean hasUncheckedItem = false;
|
||||
for (final boolean itemChecked : checkedItems) {
|
||||
if (itemChecked) {
|
||||
hasCheckedItem = true;
|
||||
if (allowAllChecked) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
hasUncheckedItem = true;
|
||||
}
|
||||
if (hasCheckedItem && hasUncheckedItem) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(
|
||||
hasCheckedItem && (hasUncheckedItem || allowAllChecked));
|
||||
}
|
||||
})
|
||||
.setPositiveButton(positiveButtonRes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
listener.onClick(checkedItems);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
}
|
||||
})
|
||||
.create();
|
||||
mAlertDialog.show();
|
||||
// disable the positive button since nothing is checked by default
|
||||
mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to add some code to run when the positive button on a multi-select dialog is
|
||||
* clicked.
|
||||
*/
|
||||
private interface OnMultiChoiceDialogAcceptListener {
|
||||
/**
|
||||
* Handler triggered when the positive button of the dialog is clicked.
|
||||
*
|
||||
* @param checkedItems a list of whether each item in the dialog was checked.
|
||||
*/
|
||||
void onClick(final boolean[] checkedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preference to link to a language specific settings screen.
|
||||
*/
|
||||
private static class SingleLanguagePreference extends Preference {
|
||||
private final String mLocale;
|
||||
private Bundle mExtras;
|
||||
|
||||
/**
|
||||
* Create a new preference for a language.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
* @param localeString a string specification of a locale, in a format of "ll_cc_variant",
|
||||
* where "ll" is a language code, "cc" is a country code.
|
||||
*/
|
||||
public SingleLanguagePreference(final Context context, final String localeString) {
|
||||
super(context);
|
||||
mLocale = localeString;
|
||||
|
||||
setTitle(LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(localeString));
|
||||
setFragment(SingleLanguageSettingsFragment.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getExtras() {
|
||||
if (mExtras == null) {
|
||||
mExtras = new Bundle();
|
||||
mExtras.putString(LOCALE_BUNDLE_KEY, mLocale);
|
||||
}
|
||||
return mExtras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle peekExtras() {
|
||||
return mExtras;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardLayoutSet;
|
||||
|
||||
/**
|
||||
* "Preferences" settings sub screen.
|
||||
* <p>
|
||||
* This settings sub screen handles the following input preferences.
|
||||
* - Auto-capitalization
|
||||
* - Show separate number row
|
||||
* - Hide special characters
|
||||
* - Hide language switch key
|
||||
* - Switch to other keyboards
|
||||
* - Space swipe cursor move
|
||||
* - Delete swipe
|
||||
*/
|
||||
public final class PreferencesSettingsFragment extends SubScreenFragment {
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
addPreferencesFromResource(R.xml.prefs_screen_preferences);
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||
removePreference(Settings.PREF_ENABLE_IME_SWITCH);
|
||||
} else {
|
||||
updateImeSwitchEnabledPref();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
if (key.equals(Settings.PREF_HIDE_SPECIAL_CHARS) ||
|
||||
key.equals(Settings.PREF_SHOW_NUMBER_ROW)) {
|
||||
KeyboardLayoutSet.onKeyboardThemeChanged();
|
||||
} else if (key.equals(Settings.PREF_HIDE_LANGUAGE_SWITCH_KEY)) {
|
||||
updateImeSwitchEnabledPref();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the preference for switching IMEs only when the preference is set to not hide the
|
||||
* language switch key.
|
||||
*/
|
||||
private void updateImeSwitchEnabledPref() {
|
||||
final Preference enableImeSwitch = findPreference(Settings.PREF_ENABLE_IME_SWITCH);
|
||||
final Preference hideLanguageSwitchKey =
|
||||
findPreference(Settings.PREF_HIDE_LANGUAGE_SWITCH_KEY);
|
||||
if (enableImeSwitch == null || hideLanguageSwitchKey == null) {
|
||||
return;
|
||||
}
|
||||
final boolean hideLanguageSwitchKeyIsChecked;
|
||||
// depending on the version of Android, the preferences could be different types
|
||||
if (hideLanguageSwitchKey instanceof CheckBoxPreference) {
|
||||
hideLanguageSwitchKeyIsChecked =
|
||||
((CheckBoxPreference) hideLanguageSwitchKey).isChecked();
|
||||
} else if (hideLanguageSwitchKey instanceof SwitchPreference) {
|
||||
hideLanguageSwitchKeyIsChecked =
|
||||
((SwitchPreference) hideLanguageSwitchKey).isChecked();
|
||||
} else {
|
||||
// in case it can be something else, don't bother doing anything
|
||||
return;
|
||||
}
|
||||
enableImeSwitch.setEnabled(!hideLanguageSwitchKeyIsChecked);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
|
||||
/**
|
||||
* Radio Button preference
|
||||
*/
|
||||
public class RadioButtonPreference extends Preference {
|
||||
interface OnRadioButtonClickedListener {
|
||||
/**
|
||||
* Called when this preference needs to be saved its state.
|
||||
*
|
||||
* @param preference This preference.
|
||||
*/
|
||||
void onRadioButtonClicked(RadioButtonPreference preference);
|
||||
}
|
||||
|
||||
private boolean mIsSelected;
|
||||
private RadioButton mRadioButton;
|
||||
private OnRadioButtonClickedListener mListener;
|
||||
private final View.OnClickListener mClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
callListenerOnRadioButtonClicked();
|
||||
}
|
||||
};
|
||||
|
||||
public RadioButtonPreference(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public RadioButtonPreference(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.preferenceStyle);
|
||||
}
|
||||
|
||||
public RadioButtonPreference(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.radio_button_preference_widget);
|
||||
}
|
||||
|
||||
public void setOnRadioButtonClickedListener(final OnRadioButtonClickedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
void callListenerOnRadioButtonClicked() {
|
||||
if (mListener != null) {
|
||||
mListener.onRadioButtonClicked(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(final View view) {
|
||||
super.onBindView(view);
|
||||
mRadioButton = view.findViewById(R.id.radio_button);
|
||||
mRadioButton.setChecked(mIsSelected);
|
||||
mRadioButton.setOnClickListener(mClickListener);
|
||||
view.setOnClickListener(mClickListener);
|
||||
}
|
||||
|
||||
public void setSelected(final boolean selected) {
|
||||
if (selected == mIsSelected) {
|
||||
return;
|
||||
}
|
||||
mIsSelected = selected;
|
||||
if (mRadioButton != null) {
|
||||
mRadioButton.setChecked(selected);
|
||||
}
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.latin.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.TypedArray;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
|
||||
public final class SeekBarDialogPreference extends DialogPreference
|
||||
implements SeekBar.OnSeekBarChangeListener {
|
||||
public interface ValueProxy {
|
||||
int readValue(final String key);
|
||||
|
||||
int readDefaultValue(final String key);
|
||||
|
||||
void writeValue(final int value, final String key);
|
||||
|
||||
void writeDefaultValue(final String key);
|
||||
|
||||
String getValueText(final int value);
|
||||
|
||||
void feedbackValue(final int value);
|
||||
}
|
||||
|
||||
private final int mMaxValue;
|
||||
private final int mMinValue;
|
||||
private final int mStepValue;
|
||||
|
||||
private TextView mValueView;
|
||||
private SeekBar mSeekBar;
|
||||
|
||||
private ValueProxy mValueProxy;
|
||||
|
||||
public SeekBarDialogPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
final TypedArray a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.SeekBarDialogPreference, 0, 0);
|
||||
mMaxValue = a.getInt(R.styleable.SeekBarDialogPreference_maxValue, 0);
|
||||
mMinValue = a.getInt(R.styleable.SeekBarDialogPreference_minValue, 0);
|
||||
mStepValue = a.getInt(R.styleable.SeekBarDialogPreference_stepValue, 0);
|
||||
a.recycle();
|
||||
setDialogLayoutResource(R.layout.seek_bar_dialog);
|
||||
}
|
||||
|
||||
public void setInterface(final ValueProxy proxy) {
|
||||
mValueProxy = proxy;
|
||||
final int value = mValueProxy.readValue(getKey());
|
||||
setSummary(mValueProxy.getValueText(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
final View view = super.onCreateDialogView();
|
||||
mSeekBar = view.findViewById(R.id.seek_bar_dialog_bar);
|
||||
mSeekBar.setMax(mMaxValue - mMinValue);
|
||||
mSeekBar.setOnSeekBarChangeListener(this);
|
||||
mValueView = view.findViewById(R.id.seek_bar_dialog_value);
|
||||
return view;
|
||||
}
|
||||
|
||||
private int getProgressFromValue(final int value) {
|
||||
return value - mMinValue;
|
||||
}
|
||||
|
||||
private int getValueFromProgress(final int progress) {
|
||||
return progress + mMinValue;
|
||||
}
|
||||
|
||||
private int clipValue(final int value) {
|
||||
final int clippedValue = Math.min(mMaxValue, Math.max(mMinValue, value));
|
||||
if (mStepValue <= 1) {
|
||||
return clippedValue;
|
||||
}
|
||||
return clippedValue - (clippedValue % mStepValue);
|
||||
}
|
||||
|
||||
private int getClippedValueFromProgress(final int progress) {
|
||||
return clipValue(getValueFromProgress(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(final View view) {
|
||||
final int value = mValueProxy.readValue(getKey());
|
||||
mValueView.setText(mValueProxy.getValueText(value));
|
||||
mSeekBar.setProgress(getProgressFromValue(clipValue(value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
|
||||
builder.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, this)
|
||||
.setNeutralButton(R.string.button_default, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
super.onClick(dialog, which);
|
||||
final String key = getKey();
|
||||
if (which == DialogInterface.BUTTON_NEUTRAL) {
|
||||
final int value = mValueProxy.readDefaultValue(key);
|
||||
setSummary(mValueProxy.getValueText(value));
|
||||
mValueProxy.writeDefaultValue(key);
|
||||
return;
|
||||
}
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
final int value = getClippedValueFromProgress(mSeekBar.getProgress());
|
||||
setSummary(mValueProxy.getValueText(value));
|
||||
mValueProxy.writeValue(value, key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekBar, final int progress,
|
||||
final boolean fromUser) {
|
||||
final int value = getClippedValueFromProgress(progress);
|
||||
mValueView.setText(mValueProxy.getValueText(value));
|
||||
if (!fromUser) {
|
||||
mSeekBar.setProgress(getProgressFromValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(final SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(final SeekBar seekBar) {
|
||||
mValueProxy.feedbackValue(getClippedValueFromProgress(seekBar.getProgress()));
|
||||
}
|
||||
}
|
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
|
||||
import com.amnesica.kryptey.inputmethod.keyboard.KeyboardTheme;
|
||||
import com.amnesica.kryptey.inputmethod.latin.AudioAndHapticFeedbackManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.InputAttributes;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
// Settings screens
|
||||
public static final String SCREEN_THEME = "screen_theme";
|
||||
// In the same order as xml/prefs.xml
|
||||
public static final String PREF_AUTO_CAP = "auto_cap";
|
||||
public static final String PREF_VIBRATE_ON = "vibrate_on";
|
||||
public static final String PREF_SOUND_ON = "sound_on";
|
||||
public static final String PREF_POPUP_ON = "popup_on";
|
||||
public static final String PREF_HIDE_LANGUAGE_SWITCH_KEY = "pref_hide_language_switch_key";
|
||||
public static final String PREF_ENABLE_IME_SWITCH = "pref_enable_ime_switch";
|
||||
public static final String PREF_ENABLED_SUBTYPES = "pref_enabled_subtypes";
|
||||
public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings";
|
||||
public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume";
|
||||
public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
|
||||
public static final String PREF_KEYBOARD_HEIGHT = "pref_keyboard_height";
|
||||
public static final String PREF_KEYBOARD_COLOR = "pref_keyboard_color";
|
||||
public static final String PREF_HIDE_SPECIAL_CHARS = "pref_hide_special_chars";
|
||||
public static final String PREF_SHOW_NUMBER_ROW = "pref_show_number_row";
|
||||
public static final String PREF_SPACE_SWIPE = "pref_space_swipe";
|
||||
public static final String PREF_DELETE_SWIPE = "pref_delete_swipe";
|
||||
public static final String PREF_MATCHING_NAVBAR_COLOR = "pref_matching_navbar_color";
|
||||
|
||||
private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f;
|
||||
private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1;
|
||||
|
||||
private Resources mRes;
|
||||
private SharedPreferences mPrefs;
|
||||
private SettingsValues mSettingsValues;
|
||||
private final ReentrantLock mSettingsValuesLock = new ReentrantLock();
|
||||
|
||||
private static final Settings sInstance = new Settings();
|
||||
|
||||
public static Settings getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public static void init(final Context context) {
|
||||
sInstance.onCreate(context);
|
||||
}
|
||||
|
||||
private Settings() {
|
||||
// Intentional empty constructor for singleton.
|
||||
}
|
||||
|
||||
private void onCreate(final Context context) {
|
||||
mRes = context.getResources();
|
||||
mPrefs = PreferenceManagerCompat.getDeviceSharedPreferences(context);
|
||||
mPrefs.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||
mSettingsValuesLock.lock();
|
||||
try {
|
||||
if (mSettingsValues == null) {
|
||||
// TODO: Introduce a static function to register this class and ensure that
|
||||
// loadSettings must be called before "onSharedPreferenceChanged" is called.
|
||||
Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
|
||||
return;
|
||||
}
|
||||
loadSettings(mSettingsValues.mInputAttributes);
|
||||
} finally {
|
||||
mSettingsValuesLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadSettings(final InputAttributes inputAttributes) {
|
||||
mSettingsValues = new SettingsValues(mPrefs, mRes, inputAttributes);
|
||||
}
|
||||
|
||||
// TODO: Remove this method and add proxy method to SettingsValues.
|
||||
public SettingsValues getCurrent() {
|
||||
return mSettingsValues;
|
||||
}
|
||||
|
||||
|
||||
// Accessed from the settings interface, hence public
|
||||
public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
return prefs.getBoolean(PREF_SOUND_ON,
|
||||
res.getBoolean(R.bool.config_default_sound_enabled));
|
||||
}
|
||||
|
||||
public static boolean readVibrationEnabled(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
final boolean hasVibrator = AudioAndHapticFeedbackManager.getInstance().hasVibrator();
|
||||
return hasVibrator && prefs.getBoolean(PREF_VIBRATE_ON,
|
||||
res.getBoolean(R.bool.config_default_vibration_enabled));
|
||||
}
|
||||
|
||||
public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
final boolean defaultKeyPreviewPopup = res.getBoolean(
|
||||
R.bool.config_default_key_preview_popup);
|
||||
return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup);
|
||||
}
|
||||
|
||||
public static boolean readShowLanguageSwitchKey(final SharedPreferences prefs) {
|
||||
return !prefs.getBoolean(PREF_HIDE_LANGUAGE_SWITCH_KEY, false);
|
||||
}
|
||||
|
||||
public static boolean readEnableImeSwitch(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_ENABLE_IME_SWITCH, false);
|
||||
}
|
||||
|
||||
public static boolean readHideSpecialChars(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_HIDE_SPECIAL_CHARS, false);
|
||||
}
|
||||
|
||||
public static boolean readShowNumberRow(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_SHOW_NUMBER_ROW, false);
|
||||
}
|
||||
|
||||
public static boolean readSpaceSwipeEnabled(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_SPACE_SWIPE, false);
|
||||
}
|
||||
|
||||
public static boolean readDeleteSwipeEnabled(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_DELETE_SWIPE, false);
|
||||
}
|
||||
|
||||
public static String readPrefSubtypes(final SharedPreferences prefs) {
|
||||
return prefs.getString(PREF_ENABLED_SUBTYPES, "");
|
||||
}
|
||||
|
||||
public static void writePrefSubtypes(final SharedPreferences prefs, final String prefSubtypes) {
|
||||
prefs.edit().putString(PREF_ENABLED_SUBTYPES, prefSubtypes).apply();
|
||||
}
|
||||
|
||||
public static float readKeypressSoundVolume(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
final float volume = prefs.getFloat(
|
||||
PREF_KEYPRESS_SOUND_VOLUME, UNDEFINED_PREFERENCE_VALUE_FLOAT);
|
||||
return (volume != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? volume
|
||||
: readDefaultKeypressSoundVolume(res);
|
||||
}
|
||||
|
||||
// Default keypress sound volume for unknown devices.
|
||||
// The negative value means system default.
|
||||
private static final String DEFAULT_KEYPRESS_SOUND_VOLUME = Float.toString(-1.0f);
|
||||
|
||||
public static float readDefaultKeypressSoundVolume(final Resources res) {
|
||||
return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(res,
|
||||
R.array.keypress_volumes, DEFAULT_KEYPRESS_SOUND_VOLUME));
|
||||
}
|
||||
|
||||
public static int readKeyLongpressTimeout(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
final int milliseconds = prefs.getInt(
|
||||
PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT);
|
||||
return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
|
||||
: readDefaultKeyLongpressTimeout(res);
|
||||
}
|
||||
|
||||
public static int readDefaultKeyLongpressTimeout(final Resources res) {
|
||||
return res.getInteger(R.integer.config_default_longpress_key_timeout);
|
||||
}
|
||||
|
||||
public static int readKeypressVibrationDuration(final SharedPreferences prefs,
|
||||
final Resources res) {
|
||||
final int milliseconds = prefs.getInt(
|
||||
PREF_VIBRATION_DURATION_SETTINGS, UNDEFINED_PREFERENCE_VALUE_INT);
|
||||
return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
|
||||
: readDefaultKeypressVibrationDuration(res);
|
||||
}
|
||||
|
||||
// Default keypress vibration duration for unknown devices.
|
||||
// The negative value means system default.
|
||||
private static final String DEFAULT_KEYPRESS_VIBRATION_DURATION = Integer.toString(-1);
|
||||
|
||||
public static int readDefaultKeypressVibrationDuration(final Resources res) {
|
||||
return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(res,
|
||||
R.array.keypress_vibration_durations, DEFAULT_KEYPRESS_VIBRATION_DURATION));
|
||||
}
|
||||
|
||||
public static float readKeyboardHeight(final SharedPreferences prefs,
|
||||
final float defaultValue) {
|
||||
return prefs.getFloat(PREF_KEYBOARD_HEIGHT, defaultValue);
|
||||
}
|
||||
|
||||
public static int readKeyboardDefaultColor(final Context context) {
|
||||
final int[] keyboardThemeColors = context.getResources().getIntArray(R.array.keyboard_theme_colors);
|
||||
final int[] keyboardThemeIds = context.getResources().getIntArray(R.array.keyboard_theme_ids);
|
||||
final int themeId = KeyboardTheme.getKeyboardTheme(context).mThemeId;
|
||||
for (int index = 0; index < keyboardThemeIds.length; index++) {
|
||||
if (themeId == keyboardThemeIds[index]) {
|
||||
return keyboardThemeColors[index];
|
||||
}
|
||||
}
|
||||
|
||||
return Color.LTGRAY;
|
||||
}
|
||||
|
||||
public static int readKeyboardColor(final SharedPreferences prefs, final Context context) {
|
||||
return prefs.getInt(PREF_KEYBOARD_COLOR, readKeyboardDefaultColor(context));
|
||||
}
|
||||
|
||||
public static void removeKeyboardColor(final SharedPreferences prefs) {
|
||||
prefs.edit().remove(PREF_KEYBOARD_COLOR).apply();
|
||||
}
|
||||
|
||||
public static boolean readUseFullscreenMode(final Resources res) {
|
||||
return res.getBoolean(R.bool.config_use_fullscreen_mode);
|
||||
}
|
||||
|
||||
public static boolean readHasHardwareKeyboard(final Configuration conf) {
|
||||
// The standard way of finding out whether we have a hardware keyboard. This code is taken
|
||||
// from InputMethodService#onEvaluateInputShown, which canonically determines this.
|
||||
// In a nutshell, we have a keyboard if the configuration says the type of hardware keyboard
|
||||
// is NOKEYS and if it's not hidden (e.g. folded inside the device).
|
||||
return conf.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||
&& conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES;
|
||||
}
|
||||
|
||||
public static boolean readUseMatchingNavbarColor(final SharedPreferences prefs) {
|
||||
return prefs.getBoolean(PREF_MATCHING_NAVBAR_COLOR, false);
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.FragmentUtils;
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
|
||||
private static final String TAG = SettingsActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
boolean enabled = false;
|
||||
try {
|
||||
enabled = isInputMethodOfThisImeEnabled();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Exception in check if input method is enabled", e);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
final Context context = this;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.setup_message);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_INPUT_METHOD_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
builder.setCancelable(false);
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this IME is enabled in the system.
|
||||
*
|
||||
* @return whether this IME is enabled in the system.
|
||||
*/
|
||||
private boolean isInputMethodOfThisImeEnabled() {
|
||||
final InputMethodManager imm =
|
||||
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
final String imePackageName = getPackageName();
|
||||
for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) {
|
||||
if (imi.getPackageName().equals(imePackageName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
final ActionBar actionBar = getActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setHomeButtonEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
super.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
final Intent intent = super.getIntent();
|
||||
final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
|
||||
if (fragment == null) {
|
||||
intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
|
||||
}
|
||||
intent.putExtra(EXTRA_NO_HEADERS, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidFragment(final String fragmentName) {
|
||||
return FragmentUtils.isValidFragment(fragmentName);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.ApplicationUtils;
|
||||
|
||||
public final class SettingsFragment extends InputMethodSettingsFragment {
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
setHasOptionsMenu(true);
|
||||
addPreferencesFromResource(R.xml.prefs);
|
||||
final PreferenceScreen preferenceScreen = getPreferenceScreen();
|
||||
preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.InputAttributes;
|
||||
|
||||
// Non-final for testing via mock library.
|
||||
public class SettingsValues {
|
||||
public static final float DEFAULT_SIZE_SCALE = 1.0f; // 100%
|
||||
|
||||
// From resources:
|
||||
public final SpacingAndPunctuations mSpacingAndPunctuations;
|
||||
// From configuration:
|
||||
public final boolean mHasHardwareKeyboard;
|
||||
public final int mDisplayOrientation;
|
||||
// From preferences, in the same order as xml/prefs.xml:
|
||||
public final boolean mAutoCap;
|
||||
public final boolean mVibrateOn;
|
||||
public final boolean mSoundOn;
|
||||
public final boolean mKeyPreviewPopupOn;
|
||||
public final boolean mShowsLanguageSwitchKey;
|
||||
public final boolean mImeSwitchEnabled;
|
||||
public final int mKeyLongpressTimeout;
|
||||
public final boolean mHideSpecialChars;
|
||||
public final boolean mShowNumberRow;
|
||||
public final boolean mSpaceSwipeEnabled;
|
||||
public final boolean mDeleteSwipeEnabled;
|
||||
public final boolean mUseMatchingNavbarColor;
|
||||
|
||||
// From the input box
|
||||
public final InputAttributes mInputAttributes;
|
||||
|
||||
// Deduced settings
|
||||
public final int mKeypressVibrationDuration;
|
||||
public final float mKeypressSoundVolume;
|
||||
public final int mKeyPreviewPopupDismissDelay;
|
||||
|
||||
// Debug settings
|
||||
public final float mKeyboardHeightScale;
|
||||
|
||||
public SettingsValues(final SharedPreferences prefs, final Resources res,
|
||||
final InputAttributes inputAttributes) {
|
||||
// Get the resources
|
||||
mSpacingAndPunctuations = new SpacingAndPunctuations(res);
|
||||
|
||||
// Store the input attributes
|
||||
mInputAttributes = inputAttributes;
|
||||
|
||||
// Get the settings preferences
|
||||
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
|
||||
mVibrateOn = Settings.readVibrationEnabled(prefs, res);
|
||||
mSoundOn = Settings.readKeypressSoundEnabled(prefs, res);
|
||||
mKeyPreviewPopupOn = Settings.readKeyPreviewPopupEnabled(prefs, res);
|
||||
mShowsLanguageSwitchKey = Settings.readShowLanguageSwitchKey(prefs);
|
||||
mImeSwitchEnabled = Settings.readEnableImeSwitch(prefs);
|
||||
mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
|
||||
|
||||
// Compute other readable settings
|
||||
mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res);
|
||||
mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res);
|
||||
mKeypressSoundVolume = Settings.readKeypressSoundVolume(prefs, res);
|
||||
mKeyPreviewPopupDismissDelay = res.getInteger(R.integer.config_key_preview_linger_timeout);
|
||||
mKeyboardHeightScale = Settings.readKeyboardHeight(prefs, DEFAULT_SIZE_SCALE);
|
||||
mDisplayOrientation = res.getConfiguration().orientation;
|
||||
mHideSpecialChars = Settings.readHideSpecialChars(prefs);
|
||||
mShowNumberRow = Settings.readShowNumberRow(prefs);
|
||||
mSpaceSwipeEnabled = Settings.readSpaceSwipeEnabled(prefs);
|
||||
mDeleteSwipeEnabled = Settings.readDeleteSwipeEnabled(prefs);
|
||||
mUseMatchingNavbarColor = Settings.readUseMatchingNavbarColor(prefs);
|
||||
}
|
||||
|
||||
public boolean isWordSeparator(final int code) {
|
||||
return mSpacingAndPunctuations.isWordSeparator(code);
|
||||
}
|
||||
|
||||
public boolean isLanguageSwitchKeyDisabled() {
|
||||
return !mShowsLanguageSwitchKey;
|
||||
}
|
||||
|
||||
public boolean isSameInputType(final EditorInfo editorInfo) {
|
||||
return mInputAttributes.isSameInputType(editorInfo);
|
||||
}
|
||||
|
||||
public boolean hasSameOrientation(final Configuration configuration) {
|
||||
return mDisplayOrientation == configuration.orientation;
|
||||
}
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.RichInputMethodManager;
|
||||
import com.amnesica.kryptey.inputmethod.latin.Subtype;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.LocaleResourceUtils;
|
||||
import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Settings sub screen for a specific language.
|
||||
*/
|
||||
public final class SingleLanguageSettingsFragment extends PreferenceFragment {
|
||||
|
||||
public static final String LOCALE_BUNDLE_KEY = "LOCALE";
|
||||
|
||||
private RichInputMethodManager mRichImm;
|
||||
private List<SubtypePreference> mSubtypePreferences;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
RichInputMethodManager.init(getActivity());
|
||||
mRichImm = RichInputMethodManager.getInstance();
|
||||
addPreferencesFromResource(R.xml.empty_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(final Bundle savedInstanceState) {
|
||||
final Context context = getActivity();
|
||||
|
||||
final Bundle args = getArguments();
|
||||
if (args != null) {
|
||||
final String locale = getArguments().getString(LOCALE_BUNDLE_KEY);
|
||||
buildContent(locale, context);
|
||||
}
|
||||
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the preferences and them to this settings screen.
|
||||
*
|
||||
* @param locale the locale string of the locale to display content for.
|
||||
* @param context the context for this application.
|
||||
*/
|
||||
private void buildContent(final String locale, final Context context) {
|
||||
if (locale == null) {
|
||||
return;
|
||||
}
|
||||
final PreferenceGroup group = getPreferenceScreen();
|
||||
group.removeAll();
|
||||
|
||||
final PreferenceCategory mainCategory = new PreferenceCategory(context);
|
||||
final String localeName = LocaleResourceUtils.getLocaleDisplayNameInSystemLocale(locale);
|
||||
mainCategory.setTitle(context.getString(R.string.generic_language_layouts, localeName));
|
||||
group.addPreference(mainCategory);
|
||||
|
||||
buildSubtypePreferences(locale, group, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build preferences for all of the available subtypes for a locale and them to the settings
|
||||
* screen.
|
||||
*
|
||||
* @param locale the locale string of the locale to add subtypes for.
|
||||
* @param group the preference group to add preferences to.
|
||||
* @param context the context for this application.
|
||||
*/
|
||||
private void buildSubtypePreferences(final String locale, final PreferenceGroup group,
|
||||
final Context context) {
|
||||
final Set<Subtype> enabledSubtypes = mRichImm.getEnabledSubtypes(false);
|
||||
final List<Subtype> subtypes =
|
||||
SubtypeLocaleUtils.getSubtypes(locale, context.getResources());
|
||||
mSubtypePreferences = new ArrayList<>();
|
||||
for (final Subtype subtype : subtypes) {
|
||||
final boolean isChecked = enabledSubtypes.contains(subtype);
|
||||
final SubtypePreference pref = createSubtypePreference(subtype, isChecked, context);
|
||||
group.addPreference(pref);
|
||||
mSubtypePreferences.add(pref);
|
||||
}
|
||||
|
||||
// if there is only one subtype that is checked, the preference for it should be disabled to
|
||||
// prevent all of the subtypes for the language from being removed
|
||||
final List<SubtypePreference> checkedPrefs = getCheckedSubtypePreferences();
|
||||
if (checkedPrefs.size() == 1) {
|
||||
checkedPrefs.get(0).setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a preference for a keyboard layout subtype.
|
||||
*
|
||||
* @param subtype the subtype that the preference enables.
|
||||
* @param checked whether the preference should be initially checked.
|
||||
* @param context the context for this application.
|
||||
* @return the preference that was created.
|
||||
*/
|
||||
private SubtypePreference createSubtypePreference(final Subtype subtype,
|
||||
final boolean checked,
|
||||
final Context context) {
|
||||
final SubtypePreference pref = new SubtypePreference(context, subtype);
|
||||
pref.setTitle(subtype.getLayoutDisplayName());
|
||||
pref.setChecked(checked);
|
||||
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(final Preference preference, final Object newValue) {
|
||||
if (!(newValue instanceof Boolean)) {
|
||||
return false;
|
||||
}
|
||||
final boolean isEnabling = (boolean) newValue;
|
||||
final SubtypePreference pref = (SubtypePreference) preference;
|
||||
final List<SubtypePreference> checkedPrefs = getCheckedSubtypePreferences();
|
||||
if (checkedPrefs.size() == 1) {
|
||||
checkedPrefs.get(0).setEnabled(false);
|
||||
}
|
||||
if (isEnabling) {
|
||||
final boolean added = mRichImm.addSubtype(pref.getSubtype());
|
||||
// if only one subtype was checked before, the preference would have been
|
||||
// disabled, but now that there are two, it can be enabled to allow it to be
|
||||
// unchecked
|
||||
if (added && checkedPrefs.size() == 1) {
|
||||
checkedPrefs.get(0).setEnabled(true);
|
||||
}
|
||||
return added;
|
||||
} else {
|
||||
final boolean removed = mRichImm.removeSubtype(pref.getSubtype());
|
||||
// if there is going to be only one subtype that is checked, the preference for
|
||||
// it should be disabled to prevent all of the subtypes for the language from
|
||||
// being removed
|
||||
if (removed && checkedPrefs.size() == 2) {
|
||||
final SubtypePreference onlyCheckedPref;
|
||||
if (checkedPrefs.get(0).equals(pref)) {
|
||||
onlyCheckedPref = checkedPrefs.get(1);
|
||||
} else {
|
||||
onlyCheckedPref = checkedPrefs.get(0);
|
||||
}
|
||||
onlyCheckedPref.setEnabled(false);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all of the subtype preferences that are currently checked.
|
||||
*
|
||||
* @return a list of all of the subtype preferences that are checked.
|
||||
*/
|
||||
private List<SubtypePreference> getCheckedSubtypePreferences() {
|
||||
final List<SubtypePreference> prefs = new ArrayList<>();
|
||||
for (final SubtypePreference pref : mSubtypePreferences) {
|
||||
if (pref.isChecked()) {
|
||||
prefs.add(pref);
|
||||
}
|
||||
}
|
||||
return prefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preference for a keyboard layout.
|
||||
*/
|
||||
private static class SubtypePreference extends SwitchPreference {
|
||||
final Subtype mSubtype;
|
||||
|
||||
/**
|
||||
* Create a subtype preference.
|
||||
*
|
||||
* @param context the context for this application.
|
||||
* @param subtype the subtype to create the preference for.
|
||||
*/
|
||||
public SubtypePreference(final Context context, final Subtype subtype) {
|
||||
super(context);
|
||||
mSubtype = subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtype that this preference represents.
|
||||
*
|
||||
* @return the subtype.
|
||||
*/
|
||||
public Subtype getSubtype() {
|
||||
return mSubtype;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.latin.settings;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import com.amnesica.kryptey.inputmethod.R;
|
||||
import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class SpacingAndPunctuations {
|
||||
public final int[] mSortedWordSeparators;
|
||||
private final int mSentenceSeparator;
|
||||
private final int mAbbreviationMarker;
|
||||
private final int[] mSortedSentenceTerminators;
|
||||
public final boolean mUsesAmericanTypography;
|
||||
public final boolean mUsesGermanRules;
|
||||
|
||||
public SpacingAndPunctuations(final Resources res) {
|
||||
mSortedWordSeparators = StringUtils.toSortedCodePointArray(
|
||||
res.getString(R.string.symbols_word_separators));
|
||||
mSortedSentenceTerminators = StringUtils.toSortedCodePointArray(
|
||||
res.getString(R.string.symbols_sentence_terminators));
|
||||
mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
|
||||
mAbbreviationMarker = res.getInteger(R.integer.abbreviation_marker);
|
||||
final Locale locale = res.getConfiguration().locale;
|
||||
// Heuristic: we use American Typography rules because it's the most common rules for all
|
||||
// English variants. German rules (not "German typography") also have small gotchas.
|
||||
mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage());
|
||||
mUsesGermanRules = Locale.GERMAN.getLanguage().equals(locale.getLanguage());
|
||||
}
|
||||
|
||||
public boolean isWordSeparator(final int code) {
|
||||
return Arrays.binarySearch(mSortedWordSeparators, code) >= 0;
|
||||
}
|
||||
|
||||
public boolean isSentenceTerminator(final int code) {
|
||||
return Arrays.binarySearch(mSortedSentenceTerminators, code) >= 0;
|
||||
}
|
||||
|
||||
public boolean isAbbreviationMarker(final int code) {
|
||||
return code == mAbbreviationMarker;
|
||||
}
|
||||
|
||||
public boolean isSentenceSeparator(final int code) {
|
||||
return code == mSentenceSeparator;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user