3 Commits

Author SHA1 Message Date
6878f80a54 🐛 Fix connectivity issues 2018-06-14 07:07:48 +02:00
a01f116065 🐛 Fix connectivity issues 2018-06-14 06:56:07 +02:00
c6e29c056b 🔥 Drop support for API 19 (KitKat)
I'm fed up with all the extra effort for supporting an outdated
Android version. Apologies for the few users that are now excluded,
but I feel like it's just not worth the hassle.
2018-06-13 22:03:06 +02:00
27 changed files with 55 additions and 607 deletions

View File

@ -21,7 +21,7 @@ android {
} }
defaultConfig { defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}" applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 27
versionCode 23 versionCode 23
versionName "1.0-rc1" versionName "1.0-rc1"

View File

@ -175,20 +175,7 @@
<!-- Receive Wi-Fi connection state changes --> <!-- Receive Wi-Fi connection state changes -->
<receiver <receiver
android:name=".listener.WifiReceiver" android:name=".service.StartServiceReceiver">
android:enabled="@bool/is_pre_api_21">
<intent-filter>
<!-- This is bad for battery life, but needed on older devices to check
if WiFi is available. Let's be honest, the whole app is bad for
battery life. -->
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
</intent-filter>
</receiver>
<receiver
android:name=".service.StartServiceReceiver"
android:enabled="@bool/is_post_api_21">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>

View File

@ -179,6 +179,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.setDelay(1000) .setDelay(1000)
.show() .show()
} }
SyncAdapter.startSync(this)
} }
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> { private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {

View File

@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.NavUtils import android.support.v4.app.NavUtils
import android.view.MenuItem import android.view.MenuItem
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext

View File

@ -18,8 +18,6 @@ package ch.dissem.apps.abit
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
import android.os.Build
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
@ -191,21 +189,10 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) { when (key) {
PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences)
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences) PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
} }
} }
private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
val ctx = context ?: throw IllegalStateException("No context available")
if (node != null) {
SyncAdapter.startSync(ctx)
} else {
SyncAdapter.stopSync(ctx)
}
}
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) { private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
if (node != null) { if (node != null) {
@ -264,7 +251,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
private fun connectivityChangeListener() = private fun connectivityChangeListener() =
OnPreferenceChangeListener { _, _ -> OnPreferenceChangeListener { _, _ ->
context?.let { ctx -> context?.let { ctx ->
if (Build.VERSION.SDK_INT >= LOLLIPOP && Preferences.isFullNodeActive(ctx)) { if (Preferences.isFullNodeActive(ctx)) {
NetworkUtils.scheduleNodeStart(ctx) NetworkUtils.scheduleNodeStart(ctx)
} }
} }

View File

@ -1,29 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* 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 ch.dissem.apps.abit.adapter
import ch.dissem.apps.abit.util.PRNGFixes
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
/**
* @author Christian Basler
*/
class AndroidCryptography : SpongyCryptography() {
init {
PRNGFixes.apply()
}
}

View File

@ -149,7 +149,7 @@ class ConversationAdapter internal constructor(
linksClickable = true linksClickable = true
setTextIsSelectable(true) setTextIsSelectable(true)
} }
val labelAdapter = LabelAdapter(itemView.context, emptySet<Label>()) val labelAdapter = LabelAdapter(itemView.context, emptySet())
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply { val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
adapter = labelAdapter adapter = labelAdapter
layoutManager = GridLayoutManager(itemView.context, 2) layoutManager = GridLayoutManager(itemView.context, 2)

View File

@ -2,7 +2,6 @@ package ch.dissem.apps.abit.adapter
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Build
import android.support.annotation.ColorInt import android.support.annotation.ColorInt
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
@ -15,7 +14,6 @@ import ch.dissem.apps.abit.util.getIcon
import ch.dissem.apps.abit.util.getText import ch.dissem.apps.abit.util.getText
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.iconics.view.IconicsImageView import com.mikepenz.iconics.view.IconicsImageView
import org.jetbrains.anko.backgroundColor
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) : class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
RecyclerView.Adapter<LabelAdapter.ViewHolder>() { RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
@ -50,11 +48,7 @@ class LabelAdapter internal constructor(private val ctx: Context, labels: Collec
var label = itemView.findViewById<TextView>(R.id.label)!! var label = itemView.findViewById<TextView>(R.id.label)!!
fun setBackground(@ColorInt color: Int) { fun setBackground(@ColorInt color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { itemView.backgroundTintList = ColorStateList.valueOf(color)
itemView.backgroundTintList = ColorStateList.valueOf(color)
} else {
itemView.backgroundColor = color
}
} }
} }
} }

View File

@ -17,7 +17,6 @@
package ch.dissem.apps.abit.dialog package ch.dissem.apps.abit.dialog
import android.app.Activity import android.app.Activity
import android.os.Build
import android.os.Bundle import android.os.Bundle
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.NetworkUtils import ch.dissem.apps.abit.util.NetworkUtils
@ -37,9 +36,7 @@ class FullNodeDialogActivity : Activity() {
finish() finish()
} }
dismiss.setOnClickListener { dismiss.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { NetworkUtils.scheduleNodeStart(applicationContext)
NetworkUtils.scheduleNodeStart(applicationContext)
}
finish() finish()
} }
} }

View File

@ -1,36 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* 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 ch.dissem.apps.abit.listener
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import org.jetbrains.anko.connectivityManager
class WifiReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) {
val bmc = Singleton.getBitmessageContext(ctx)
if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) {
NetworkUtils.doStartBitmessageService(ctx)
}
}
}
}

View File

@ -22,7 +22,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.BatteryManager
import android.os.Handler import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID

View File

@ -20,7 +20,6 @@ import android.content.Context
import android.widget.Toast import android.widget.Toast
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.adapter.AndroidCryptography
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
import ch.dissem.apps.abit.listener.MessageListener import ch.dissem.apps.abit.listener.MessageListener
@ -29,6 +28,7 @@ import ch.dissem.apps.abit.repository.*
import ch.dissem.apps.abit.util.Constants import ch.dissem.apps.abit.util.Constants
import ch.dissem.apps.abit.util.Observable import ch.dissem.apps.abit.util.Observable
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
@ -112,7 +112,7 @@ object Singleton {
ServerPowEngine(ctx), ServerPowEngine(ctx),
ServicePowEngine(ctx) ServicePowEngine(ctx)
) )
cryptography = AndroidCryptography() cryptography = SpongyCryptography()
nodeRegistry = AndroidNodeRegistry(sqlHelper) nodeRegistry = AndroidNodeRegistry(sqlHelper)
inventory = AndroidInventory(sqlHelper) inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper) addressRepo = AndroidAddressRepository(sqlHelper)

View File

@ -14,8 +14,8 @@ import ch.dissem.apps.abit.util.Preferences
* *
* And stops it when the preconditions for the job (unmetered network) aren't met anymore. * And stops it when the preconditions for the job (unmetered network) aren't met anymore.
*/ */
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class StartupNodeOnWifiService : JobService() { class StartupNodeOnWifiService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean { override fun onStartJob(params: JobParameters?): Boolean {
val bmc = Singleton.getBitmessageContext(this) val bmc = Singleton.getBitmessageContext(this)
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) { if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
@ -29,4 +29,5 @@ class StartupNodeOnWifiService : JobService() {
* depending on Android version. * depending on Android version.
*/ */
override fun onStopJob(params: JobParameters?) = Preferences.isFullNodeActive(this) override fun onStopJob(params: JobParameters?) = Preferences.isFullNodeActive(this)
} }

View File

@ -24,6 +24,7 @@ import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.exception.DecryptionFailedException import ch.dissem.bitmessage.exception.DecryptionFailedException
import ch.dissem.bitmessage.extensions.CryptoCustomMessage import ch.dissem.bitmessage.extensions.CryptoCustomMessage
@ -43,11 +44,11 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
private val bmc = Singleton.getBitmessageContext(context) private val bmc = Singleton.getBitmessageContext(context)
override fun onPerformSync( override fun onPerformSync(
account: Account, account: Account,
extras: Bundle, extras: Bundle,
authority: String, authority: String,
provider: ContentProviderClient, provider: ContentProviderClient,
syncResult: SyncResult syncResult: SyncResult
) { ) {
try { try {
if (account == ACCOUNT_SYNC) { if (account == ACCOUNT_SYNC) {
@ -75,16 +76,16 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
} }
val trustedNode = Preferences.getTrustedNode(context) val trustedNode = Preferences.getTrustedNode(context)
if (trustedNode == null) { if (trustedNode == null) {
LOG.info("Trusted node not available, disabling synchronization") // As Abit tends to get killed by the system, let's leverage the sync mechanism to start it again:
stopSync(context) NetworkUtils.scheduleNodeStart(context)
return return
} }
LOG.info("Synchronization started") LOG.info("Synchronization started")
bmc.synchronize( bmc.synchronize(
trustedNode, trustedNode,
Preferences.getTrustedNodePort(context), Preferences.getTrustedNodePort(context),
Preferences.getTimeoutInSeconds(context), Preferences.getTimeoutInSeconds(context),
true true
) )
LOG.info("Synchronization finished") LOG.info("Synchronization finished")
} }
@ -104,7 +105,8 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
// If the Bitmessage context acts as a full node, synchronization isn't necessary // If the Bitmessage context acts as a full node, synchronization isn't necessary
LOG.info("Looking for completed POW") LOG.info("Looking for completed POW")
val privateKey = identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key") val privateKey =
identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey) val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
val reader = ProofOfWorkRequest.Reader(identity) val reader = ProofOfWorkRequest.Reader(identity)
val powRepo = Singleton.getProofOfWorkRepository(context) val powRepo = Singleton.getProofOfWorkRepository(context)
@ -113,12 +115,13 @@ class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedS
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash) val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes) val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
val cryptoMsg = CryptoCustomMessage( val cryptoMsg = CryptoCustomMessage(
ProofOfWorkRequest(identity, initialHash, CALCULATE, target)) ProofOfWorkRequest(identity, initialHash, CALCULATE, target)
)
cryptoMsg.signAndEncrypt(identity, signingKey) cryptoMsg.signAndEncrypt(identity, signingKey)
val response = bmc.send( val response = bmc.send(
trustedNode, trustedNode,
Preferences.getTrustedNodePort(context), Preferences.getTrustedNodePort(context),
cryptoMsg cryptoMsg
) )
if (response.isError) { if (response.isError) {
LOG.error("Server responded with error: ${String(response.getData())}") LOG.error("Server responded with error: ${String(response.getData())}")

View File

@ -17,8 +17,6 @@
package ch.dissem.apps.abit.util package ch.dissem.apps.abit.util
import android.content.Context import android.content.Context
import android.support.annotation.DrawableRes
import android.support.annotation.StringRes
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import java.io.IOException import java.io.IOException

View File

@ -6,10 +6,7 @@ import android.app.job.JobScheduler
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
import ch.dissem.apps.abit.service.BitmessageService import ch.dissem.apps.abit.service.BitmessageService
import ch.dissem.apps.abit.service.StartupNodeOnWifiService import ch.dissem.apps.abit.service.StartupNodeOnWifiService
@ -20,36 +17,19 @@ object NetworkUtils {
fun enableNode(ctx: Context, ask: Boolean = true) { fun enableNode(ctx: Context, ask: Boolean = true) {
Preferences.setFullNodeActive(ctx, true) Preferences.setFullNodeActive(ctx, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Preferences.isConnectionAllowed(ctx) || !ask) {
if (Preferences.isConnectionAllowed(ctx) || !ask) { scheduleNodeStart(ctx)
scheduleNodeStart(ctx)
} else {
askForConnection(ctx)
}
} else { } else {
if (Preferences.isWifiOnly(ctx)) { // Ask for connection
if (Preferences.isConnectionAllowed(ctx)) { val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
doStartBitmessageService(ctx) if (ctx !is Activity) {
MainActivity.updateNodeSwitch() dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} else if (ask) { ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
askForConnection(ctx)
}
} else {
doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch()
} }
ctx.startActivity(dialogIntent)
} }
} }
private fun askForConnection(ctx: Context) {
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
ctx.startActivity(dialogIntent)
}
fun doStartBitmessageService(ctx: Context) { fun doStartBitmessageService(ctx: Context) {
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java)) ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
} }
@ -59,7 +39,6 @@ object NetworkUtils {
ctx.stopService(Intent(ctx, BitmessageService::class.java)) ctx.stopService(Intent(ctx, BitmessageService::class.java))
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun scheduleNodeStart(ctx: Context) { fun scheduleNodeStart(ctx: Context) {
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java) val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)

View File

@ -1,345 +0,0 @@
/*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will Google be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, as long as the origin is not misrepresented.
*/
package ch.dissem.apps.abit.util;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
/**
* Fixes for the output of the default PRNG having low entropy.
* <p/>
* The fixes need to be applied via {@link #apply()} before any use of Java
* Cryptography Architecture primitives. A good place to invoke them is in the
* application's {@code onCreate}.
*
* @see <a href="http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html">
* http://android-developers.blogspot.ch/2013/08/some-securerandom-thoughts.html</a>
*/
@SuppressWarnings("WeakerAccess")
public final class PRNGFixes {
private static final int VERSION_CODE_JELLY_BEAN = 16;
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
getBuildFingerprintAndDeviceSerial();
/**
* Hidden constructor to prevent instantiation.
*/
private PRNGFixes() {
}
/**
* Applies all fixes.
*
* @throws SecurityException if a fix is needed but could not be applied.
*/
public static void apply() {
applyOpenSSLFix();
installLinuxPRNGSecureRandom();
}
/**
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
* fix is not needed.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void applyOpenSSLFix() throws SecurityException {
if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
// No need to apply the fix
return;
}
try {
// Mix in the device- and invocation-specific seed.
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_seed", byte[].class)
.invoke(null, (Object) generateSeed());
// Mix output of Linux PRNG into OpenSSL's PRNG
int bytesRead = (Integer) Class.forName(
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
.getMethod("RAND_load_file", String.class, long.class)
.invoke(null, "/dev/urandom", 1024);
if (bytesRead != 1024) {
throw new IOException(
"Unexpected number of bytes read from Linux PRNG: "
+ bytesRead);
}
} catch (Exception e) {
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
}
}
/**
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
* default. Does nothing if the implementation is already the default or if
* there is not need to install the implementation.
*
* @throws SecurityException if the fix is needed but could not be applied.
*/
private static void installLinuxPRNGSecureRandom()
throws SecurityException {
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
// No need to apply the fix
return;
}
// Install a Linux PRNG-based SecureRandom implementation as the
// default, if not yet installed.
Provider[] secureRandomProviders =
Security.getProviders("SecureRandom.SHA1PRNG");
if ((secureRandomProviders == null)
|| (secureRandomProviders.length < 1)
|| (!LinuxPRNGSecureRandomProvider.class.equals(
secureRandomProviders[0].getClass()))) {
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
}
// Assert that new SecureRandom() and
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
// by the Linux PRNG-based SecureRandom implementation.
SecureRandom rng1 = new SecureRandom();
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng1.getProvider().getClass())) {
throw new SecurityException(
"new SecureRandom() backed by wrong Provider: "
+ rng1.getProvider().getClass());
}
SecureRandom rng2;
try {
rng2 = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("SHA1PRNG not available", e);
}
if (!LinuxPRNGSecureRandomProvider.class.equals(
rng2.getProvider().getClass())) {
throw new SecurityException(
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ " Provider: " + rng2.getProvider().getClass());
}
}
/**
* {@code Provider} of {@code SecureRandom} engines which pass through
* all requests to the Linux PRNG.
*/
private static class LinuxPRNGSecureRandomProvider extends Provider {
LinuxPRNGSecureRandomProvider() {
super("LinuxPRNG",
1.0,
"A Linux-specific random number provider that uses"
+ " /dev/urandom");
// Although /dev/urandom is not a SHA-1 PRNG, some apps
// explicitly request a SHA1PRNG SecureRandom and we thus need to
// prevent them from getting the default implementation whose output
// may have low entropy.
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
}
}
/**
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
* ({@code /dev/urandom}).
*/
@SuppressWarnings("JavaDoc")
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
/*
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
* are passed through to the Linux PRNG (/dev/urandom). Instances of
* this class seed themselves by mixing in the current time, PID, UID,
* build fingerprint, and hardware serial number (where available) into
* Linux PRNG.
*
* Concurrency: Read requests to the underlying Linux PRNG are
* serialized (on sLock) to ensure that multiple threads do not get
* duplicated PRNG output.
*/
private static final File URANDOM_FILE = new File("/dev/urandom");
private static final Object sLock = new Object();
/**
* Input stream for reading from Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static DataInputStream sUrandomIn;
/**
* Output stream for writing to Linux PRNG or {@code null} if not yet
* opened.
*
* @GuardedBy("sLock")
*/
private static OutputStream sUrandomOut;
/**
* Whether this engine instance has been seeded. This is needed because
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
private boolean mSeeded;
@Override
protected void engineSetSeed(byte[] bytes) {
try {
OutputStream out;
synchronized (sLock) {
out = getUrandomOutputStream();
}
out.write(bytes);
out.flush();
} catch (IOException e) {
// On a small fraction of devices /dev/urandom is not writable.
// Log and ignore.
Log.w(PRNGFixes.class.getSimpleName(),
"Failed to mix seed into " + URANDOM_FILE);
} finally {
mSeeded = true;
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
if (!mSeeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}
try {
DataInputStream in;
synchronized (sLock) {
in = getUrandomInputStream();
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (in) {
in.readFully(bytes);
}
} catch (IOException e) {
throw new SecurityException(
"Failed to read from " + URANDOM_FILE, e);
}
}
@Override
protected byte[] engineGenerateSeed(int size) {
byte[] seed = new byte[size];
engineNextBytes(seed);
return seed;
}
private DataInputStream getUrandomInputStream() {
synchronized (sLock) {
if (sUrandomIn == null) {
// NOTE: Consider inserting a BufferedInputStream between
// DataInputStream and FileInputStream if you need higher
// PRNG output performance and can live with future PRNG
// output being pulled into this process prematurely.
try {
sUrandomIn = new DataInputStream(
new FileInputStream(URANDOM_FILE));
} catch (IOException e) {
throw new SecurityException("Failed to open "
+ URANDOM_FILE + " for reading", e);
}
}
return sUrandomIn;
}
}
private OutputStream getUrandomOutputStream() throws IOException {
synchronized (sLock) {
if (sUrandomOut == null) {
sUrandomOut = new FileOutputStream(URANDOM_FILE);
}
return sUrandomOut;
}
}
}
/**
* Generates a device- and invocation-specific seed to be mixed into the
* Linux PRNG.
*/
private static byte[] generateSeed() {
try {
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
DataOutputStream seedBufferOut =
new DataOutputStream(seedBuffer);
seedBufferOut.writeLong(System.currentTimeMillis());
seedBufferOut.writeLong(System.nanoTime());
seedBufferOut.writeInt(Process.myPid());
seedBufferOut.writeInt(Process.myUid());
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
seedBufferOut.close();
return seedBuffer.toByteArray();
} catch (IOException e) {
throw new SecurityException("Failed to generate seed", e);
}
}
/**
* Gets the hardware serial number of this device.
*
* @return serial number or {@code null} if not available.
*/
private static String getDeviceSerialNumber() {
// We're using the Reflection API because Build.SERIAL is only available
// since API Level 9 (Gingerbread, Android 2.3).
try {
return (String) Build.class.getField("SERIAL").get(null);
} catch (Exception ignored) {
return null;
}
}
private static byte[] getBuildFingerprintAndDeviceSerial() {
StringBuilder result = new StringBuilder();
String fingerprint = Build.FINGERPRINT;
if (fingerprint != null) {
result.append(fingerprint);
}
String serial = getDeviceSerialNumber();
if (serial != null) {
result.append(serial);
}
try {
return result.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported");
}
}
}

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_trash"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_trash"
android:gravity="right|center_vertical"
android:right="16dp"/>
</layer-list>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_archive"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_archive"
android:gravity="left|center_vertical"
android:left="16dp"/>
</layer-list>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#ffffff">
<item
android:id="@android:id/mask"
android:drawable="@android:color/white"/>
</ripple>

View File

@ -18,9 +18,8 @@
<item> <item>
<color android:color="@color/bg_swipe_item_trash"/> <color android:color="@color/bg_swipe_item_trash"/>
</item> </item>
<item android:right="16dp"> <item
<bitmap android:drawable="@drawable/ic_item_swipe_trash"
android:gravity="right|center_vertical" android:gravity="right|center_vertical"
android:src="@drawable/ic_item_swipe_trash"/> android:right="16dp"/>
</item>
</layer-list> </layer-list>

View File

@ -18,9 +18,8 @@
<item> <item>
<color android:color="@color/bg_swipe_item_archive"/> <color android:color="@color/bg_swipe_item_archive"/>
</item> </item>
<item android:left="16dp"> <item
<bitmap android:drawable="@drawable/ic_item_swipe_archive"
android:gravity="left|center_vertical" android:gravity="left|center_vertical"
android:src="@drawable/ic_item_swipe_archive"/> android:left="16dp"/>
</item>
</layer-list> </layer-list>

View File

@ -1,14 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <ripple xmlns:android="http://schemas.android.com/apk/res/android"
<item android:state_pressed="true"> android:color="#ffffff">
<shape android:shape="rectangle">
<solid android:color="#ccffffff" /> <item
</shape> android:id="@android:id/mask"
</item> android:drawable="@android:color/white"/>
<item android:state_focused="true">
<shape android:shape="rectangle"> </ripple>
<stroke android:color="@android:color/white" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/material_showcase_button_bg"
android:text="@string/got_it"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="13sp"
android:textStyle="bold">
</Button>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_pre_api_21">false</bool>
<bool name="is_post_api_21">true</bool>
</resources>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_pre_api_21">true</bool>
<bool name="is_post_api_21">false</bool>
</resources>

View File

@ -34,7 +34,6 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="require_charging" android:key="require_charging"
android:enabled="@bool/is_post_api_21"
android:summary="@string/require_charging_summary" android:summary="@string/require_charging_summary"
android:title="@string/require_charging" /> android:title="@string/require_charging" />
<SwitchPreferenceCompat <SwitchPreferenceCompat