The problem: begin modifying the server settings by changing the security
type (which will change the port to a default value), then change the port
to a custom value, then change screen orientation. The default port value
is restored, wiping out the custom value.
When onRestoreInstanceState() is called, the custom port value is
restored. But the spinner doesn't actually restore its state at that
time. Instead, it waits until View.layout(), at which time it posts a
runnable to call onItemSelected() if the restored state doesn't match the
state initialized in onCreate(). When onItemSelected() is eventually run
sometime later, it wipes out the custom port value that was restored.
The solution is to keep track of the spinner state ourselves and only
revert the port to a default when we see the spinner state changed by the
user.
This problem goes back to 4.904 and before.
For convenience. Implemented in onCheckChanged().
As a consequence, onCheckChanged() must not be triggered when the instance
state is restored (would occur if the check box state was checked when
saved), otherwise the certificate chooser would pop up once the state was
restored. Therefore, all listeners have been moved into
initializeViewListeners() which is invoked after the state has been
restored.
Because onCheckChanged() is no longer triggered in
onRestoreInstanceState(), updateViewVisibility() was implemented to
restore the view visibility.
This is done when the SASL EXTERNAL mechanism isn't advertised (indicating
the possibility that the server did not accept the client certificate) or
when the command for authenticating with SASL EXTERNAL fails.
The CertificateValidationException will trigger a notification to the user
that there's an authentication problem that needs addressing.
Also, there were instances where CertificateValidationException was being
thrown with a new CertificateException as the cause for the purpose of
notifying the user when STARTTLS is not available. This has been slightly
simplified by eliminating the need to include a new CertificateException
as a cause.
If the alias is empty or null, don't bother using KeyChainKeyManager.
If the alias is not empty, confirm that it is associated with a
certificate, otherwise throw a CertificateValidationException
which will notify the user of the problem and ask the user to
check the server settings.
Likewise, the user is notified if the client certificate was
not accepted by the server.
If the user chooses client certificate authentication,
immediately pop up the certificate chooser.
If the user chooses password authentication, move the focus to the
password View.
With this commit, KeyChainKeyManager no longer throws the exception and
AccountSetupCheckSettings no longer catches it.
It was being thrown when the server requested a client certificate but no
client certificate alias had been configured for the server.
The code was making the incorrect assumption that the server would only
request a client certificate when such a certificate was *required*.
However, servers can be configured to accept multiple forms of
authentication, including both password authentication and client
certificate authentication. So a server may request a certificate without
requiring it. If a user has not configured a client certificate, then
that should not be treated as an error because the configuration may be
valid and the server may accept it.
The only indication that a certificate is *required* is when a
SSLProtocolException is thrown, caused by a SSLHandshakeException
resulting from a fatal handshake alert message received from the server.
Unfortunately, such a message is fairly generic and only "indicates that
the sender was unable to negotiate an acceptable set of security
parameters given the options available." So there is no definitive way to
know that a client certificate is required.
Also, KeyChainKeyManager.getCertificateChain() and getPrivateKey() no
longer throw IllegalStateException(). These methods are permitted to
return null, and such a response is appropriate if the user has deleted
client certificates from the device. Again, this may or may not cause the
server to abort the connection, depending on whether the server *requires*
a client certificate.
The app's minSdkVersion = 15 (Android 4.0.3, Ice Cream Sandwich MR1),
so there's no need to test the API level.
This also removes '@SuppressLint("TrulyRandom")'. I find no
documentation for it, nor do I find any additional lint errors
with its removal.
The user may toggle the checkbox, and then decide to toggle it again.
This also fixes a problem when restoring the activity state. When the
checkbox was restored as checked, the listener was firing and wiping the
the restored alias.
Previously, with settings of Security=SSL and authentication=certificate,
attempting to change Security=None would (of course) be blocked. So
Security would remain SSL. But the authentication options would then
include "Password, transmitted insecurely", whereas the option should
have remained as "Normal password" (because the security remained SSL).
The problem could have been fixed with a simple shuffling in
updatePortFromSecurityType() so that updateAuthPlainTextFromSecurityType()
was invoked before mPortView.setText(), but the logic for requiring that
ordering was not plain to see. (Although no longer necessary, the
shuffling was done as well.)
Add test for username == "".
Without it, the mRequireLoginView remains checked, and the empty username
and password boxes are not hidden.
The problem occurs if importing an SMTP server that has an
authenticationType, but no username or password (i.e., no authentication
required).
That's the way settings were exported in 4.904 and before.
Occurs when the SMTP server doesn't require authentication
(authenticationType == null).
The javadoc for ServerSettings says that both authenticationType and
connectionSecurity may be null.
The referenced issue states that it is only applicable to Android < 4.2
(testing confirms the problem on 4.1.2, but not on 4.2.2).
A test was added for the version code, primarily as a finder's aid for a
day when K-9 Mail no longer supports Android < 4.2 and the work-around can
be removed.
The referenced issue also states that it is only necessary to hold a
reference to the first PrivateKey retrieved. (Testing indicates that the
problem is avoided so long at least one reference is always maintained to
a PrivateKey -- it doesn't actually need to be a continuous reference to
the first PrivateKey.)
From my understanding, a normal class loader never unloads a class, so the
static reference can be safely kept privately in KeyChainKeyManager.
This only changes the vertical display order of the widgets.
The user will likely review the settings from top to bottom, but
the way they were previously arranged, settings lower on the list
were affecting things higher on the list.
Generally show top to bottom:
Server
security type
port (affected by security type above)
require login checkbox (SMTP only, affects everything below)
user
auth. type (affected by security type above, affects everything below)
password (affected by auth. type above)
client cert (affected by auth. type above)
Specifically, warn and block them when attempting to configure Client
Certificate Authentication in combination with Connection Security = None.
If this were not made obvious to the user, they might not understand why
they are not permitted to tap "Next".
Also, move the initialization of all view listeners out of onCreate() into
initializeViewListeners() which is then called near the end of onCreate(),
helping to assure that the listeners won't be triggered during the
initialization of views inside onCreate().
Previously, it was possible to have "Require sign-in" unchecked and a
"Security = None" setting for the outgoing server and still not be able to
tap "Next" because of a hidden (and irrelevant) "Authentication = Client
certificate" setting.
Check that the user has actually chosen a client certificate in
AccountSetupOutgoing.validateFields().
Also, there's no need to clear the password and certificate fields when
hiding them. The user may accidentally change settings and want to change
them back without wiping out the existing settings.
Leave the hostname == null checks so we can fall back if a hostname is not
found. Also convert message-id to upper case to match Apple Mail (for
privacy).
I wrote this fix to avoid obviously specifying that I am using a mobile device
to reply to an email.
Others want this for ease of filtering messages from their host by Message-ID.
E/StrictMode(9278): A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
E/StrictMode(9278): java.lang.Throwable: Explicit termination method 'close' not called
E/StrictMode(9278): at dalvik.system.CloseGuard.open(CloseGuard.java:184)
E/StrictMode(9278): at android.os.ParcelFileDescriptor.<init>(ParcelFileDescriptor.java:179)
E/StrictMode(9278): at android.os.ParcelFileDescriptor.<init>(ParcelFileDescriptor.java:168)
E/StrictMode(9278): at android.os.ParcelFileDescriptor.createPipe(ParcelFileDescriptor.java:362)
E/StrictMode(9278): at org.openintents.openpgp.util.ParcelFileDescriptorUtil.pipeFrom(ParcelFileDescriptorUtil.java:34)
E/StrictMode(9278): at org.openintents.openpgp.util.OpenPgpApi.executeApi(OpenPgpApi.java:222)
E/StrictMode(9278): at org.openintents.openpgp.util.OpenPgpApi$OpenPgpAsyncTask.doInBackground(OpenPgpApi.java:189)
E/StrictMode(9278): at org.openintents.openpgp.util.OpenPgpApi$OpenPgpAsyncTask.doInBackground(OpenPgpApi.java:1)
E/StrictMode(9278): at android.os.AsyncTask$2.call(AsyncTask.java:288)
E/StrictMode(9278): at java.util.concurrent.FutureTask.run(FutureTask.java:237)
E/StrictMode(9278): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
E/StrictMode(9278): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
E/StrictMode(9278): at java.lang.Thread.run(Thread.java:841)
E/AndroidRuntime(24914): FATAL EXCEPTION: main
E/AndroidRuntime(24914): Process: com.fsck.k9, PID: 24914
E/AndroidRuntime(24914): java.lang.NullPointerException
E/AndroidRuntime(24914): at org.openintents.openpgp.util.OpenPgpServiceConnection.<init>(OpenPgpServiceConnection.java:35)
E/AndroidRuntime(24914): at com.fsck.k9.view.MessageOpenPgpView.updateLayout(MessageOpenPgpView.java:115)
E/AndroidRuntime(24914): at com.fsck.k9.view.SingleMessageView.setMessage(SingleMessageView.java:623)
E/AndroidRuntime(24914): at com.fsck.k9.fragment.MessageViewFragment$Listener$2.run(MessageViewFragment.java:602)
E/AndroidRuntime(24914): at android.os.Handler.handleCallback(Handler.java:733)
E/AndroidRuntime(24914): at android.os.Handler.dispatchMessage(Handler.java:95)
E/AndroidRuntime(24914): at android.os.Looper.loop(Looper.java:136)
E/AndroidRuntime(24914): at android.app.ActivityThread.main(ActivityThread.java:5081)
E/AndroidRuntime(24914): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(24914): at java.lang.reflect.Method.invoke(Method.java:515)
E/AndroidRuntime(24914): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:791)
E/AndroidRuntime(24914): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:607)
E/AndroidRuntime(24914): at dalvik.system.NativeStart.main(Native Method)
This gives the openpgp-api-library an Eclipse
project name consistent with the names used for
the other plugins. This helps with initial
project setup and avoids project naming collisions.