Start at boot feature

This commit is contained in:
Matéo Duparc 2021-08-14 14:50:32 +02:00
parent fc46326deb
commit eac227085d
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
15 changed files with 176 additions and 75 deletions

View File

@ -5,6 +5,7 @@
android:installLocation="auto">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>
@ -27,6 +28,12 @@
</intent-filter>
</receiver>
<receiver android:name=".background_service.SystemBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<activity android:name=".ChatActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
<activity android:name=".MainActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
<activity

View File

@ -3,7 +3,9 @@ package sushi.hardcore.aira
import sushi.hardcore.aira.background_service.Contact
object AIRADatabase {
external fun initLogging(): Boolean
external fun isIdentityProtected(databaseFolder: String): Boolean
external fun getIdentityName(databaseFolder: String): String?
external fun loadIdentity(databaseFolder: String, password: ByteArray?): Boolean
external fun addContact(name: String, avatarUuid: String?, publicKey: ByteArray): Contact?
external fun removeContact(uuid: String): Boolean
@ -30,6 +32,11 @@ object AIRADatabase {
external fun removeIdentityAvatar(databaseFolder: String): Boolean
external fun getIdentityAvatar(databaseFolder: String): ByteArray?
fun init() {
System.loadLibrary("aira")
initLogging()
}
fun loadAvatar(avatarUuid: String?): ByteArray? {
return avatarUuid?.let {
getAvatar(it)

View File

@ -80,7 +80,7 @@ class CreateIdentityFragment(private val activity: AppCompatActivity) : Fragment
bundle.getBinder(LoginActivity.BINDER_ARG)?.let { binder ->
val databaseFolder = Constants.getDatabaseFolder(requireContext())
if (createNewIdentity(databaseFolder, identityName, password)) {
(binder as LoginActivity.ActivityLauncher).launch(identityName)
(binder as LoginActivity.ActivityLauncher).launch()
success = true
}
}

View File

@ -9,21 +9,18 @@ import sushi.hardcore.aira.background_service.AIRAService
import java.io.File
class LoginActivity : AppCompatActivity() {
private external fun getIdentityName(databaseFolder: String): String?
companion object {
const val NAME_ARG = "identityName"
const val BINDER_ARG = "binder"
private external fun initLogging()
init {
System.loadLibrary("aira")
initLogging()
}
}
init {
AIRADatabase.init()
}
inner class ActivityLauncher: Binder() {
fun launch(identityName: String) {
startMainActivity(identityName)
fun launch() {
startMainActivity()
}
}
@ -38,13 +35,13 @@ class LoginActivity : AppCompatActivity() {
}
}
val isProtected = AIRADatabase.isIdentityProtected(databaseFolder)
val name = getIdentityName(databaseFolder)
val name = AIRADatabase.getIdentityName(databaseFolder)
if (AIRAService.isServiceRunning) {
startMainActivity(null)
startMainActivity()
} else if (name != null && !isProtected) {
if (AIRADatabase.loadIdentity(databaseFolder, null)) {
AIRADatabase.clearCache()
startMainActivity(name)
startMainActivity()
} else {
Toast.makeText(this, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
}
@ -62,11 +59,10 @@ class LoginActivity : AppCompatActivity() {
}
}
private fun startMainActivity(identityName: String?) {
private fun startMainActivity() {
val mainActivityIntent = Intent(this, MainActivity::class.java)
mainActivityIntent.action = intent.action
mainActivityIntent.putExtras(intent)
mainActivityIntent.putExtra(NAME_ARG, identityName)
startActivity(mainActivityIntent)
finish()
}

View File

@ -42,7 +42,7 @@ class LoginFragment : Fragment() {
binding.buttonLogin.setOnClickListener {
if (AIRADatabase.loadIdentity(databaseFolder, binding.editPassword.text.toString().toByteArray())) {
AIRADatabase.clearCache()
(binder as LoginActivity.ActivityLauncher).launch(name)
(binder as LoginActivity.ActivityLauncher).launch()
} else {
Toast.makeText(activity, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
}

View File

@ -95,11 +95,6 @@ class MainActivity : ServiceBoundActivity() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar.toolbar)
val identityName = intent.getStringExtra(LoginActivity.NAME_ARG)
identityName?.let {
initToolbar(it)
}
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE
onlineSessionAdapter = SessionAdapter(this)
@ -157,12 +152,10 @@ class MainActivity : ServiceBoundActivity() {
airaService.uiCallbacks = uiCallbacks
airaService.isAppInBackground = false
refreshSessions()
if (AIRAService.isServiceRunning) {
airaService.identityName?.let { initToolbar(it) }
} else {
airaService.identityName = identityName
if (!AIRAService.isServiceRunning) {
startService(serviceIntent)
}
initToolbar(airaService.identityName)
}
override fun onServiceDisconnected(name: ComponentName?) {}
}

View File

@ -27,6 +27,7 @@ import sushi.hardcore.aira.utils.StringUtils
class SettingsActivity: AppCompatActivity() {
class MySettingsFragment(private val activity: AppCompatActivity): PreferenceFragmentCompat() {
private lateinit var databaseFolder: String
private lateinit var airaService: AIRAService
private val avatarPicker = AvatarPicker(activity) { picker, avatar ->
if (::airaService.isInitialized) {
@ -37,6 +38,7 @@ class SettingsActivity: AppCompatActivity() {
displayAvatar(avatar)
}
private lateinit var identityAvatarPreference: Preference
private lateinit var startAtBootSwitch: SwitchPreferenceCompat
override fun onAttach(context: Context) {
super.onAttach(context)
@ -45,10 +47,13 @@ class SettingsActivity: AppCompatActivity() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
databaseFolder = Constants.getDatabaseFolder(activity)
findPreference<Preference>("identityAvatar")?.let { identityAvatarPreference = it }
startAtBootSwitch = findPreference("startAtBoot")!!
updateStartAtBootSwitch(AIRADatabase.isIdentityProtected(databaseFolder))
val paddingPreference = findPreference<SwitchPreferenceCompat>("psecPadding")
paddingPreference?.isPersistent = false
AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))?.let { avatar ->
AIRADatabase.getIdentityAvatar(databaseFolder)?.let { avatar ->
displayAvatar(avatar)
}
Intent(activity, AIRAService::class.java).also { serviceIntent ->
@ -68,9 +73,9 @@ class SettingsActivity: AppCompatActivity() {
avatarPicker.launch()
}
val dialogBinding = ChangeAvatarDialogBinding.inflate(layoutInflater)
val avatar = AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))
val avatar = AIRADatabase.getIdentityAvatar(databaseFolder)
if (avatar == null) {
dialogBinding.avatar.setTextAvatar(airaService.identityName!!)
dialogBinding.avatar.setTextAvatar(airaService.identityName)
} else {
dialogBinding.avatar.setImageAvatar(avatar)
dialogBuilder.setNegativeButton(R.string.remove) { _, _ ->
@ -112,7 +117,7 @@ class SettingsActivity: AppCompatActivity() {
findPreference<Preference>("identityPassword")?.setOnPreferenceClickListener {
val dialogView = layoutInflater.inflate(R.layout.dialog_password, null)
val oldPasswordEditText = dialogView.findViewById<EditText>(R.id.old_password)
val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity))
val isIdentityProtected = AIRADatabase.isIdentityProtected(databaseFolder)
if (!isIdentityProtected) {
oldPasswordEditText.visibility = View.GONE
}
@ -123,24 +128,24 @@ class SettingsActivity: AppCompatActivity() {
.setTitle(R.string.change_password)
.setPositiveButton(R.string.ok) { _, _ ->
val newPassword = newPasswordEditText.text.toString().toByteArray()
if (newPassword.isEmpty()) {
if (isIdentityProtected) { //don't change password if identity is not protected and new password is blank
changePassword(isIdentityProtected, oldPasswordEditText, null)
val newPasswordConfirm = newPasswordConfirmEditText.text.toString().toByteArray()
if (newPassword.contentEquals(newPasswordConfirm)) {
if (newPassword.isEmpty()) {
if (isIdentityProtected) { //don't change password if identity is not protected and new password is blank
changePassword(isIdentityProtected, oldPasswordEditText, null)
}
} else {
changePassword(isIdentityProtected, oldPasswordEditText, newPassword)
}
} else {
val newPasswordConfirm = newPasswordConfirmEditText.text.toString().toByteArray()
if (newPassword.contentEquals(newPasswordConfirm)) {
changePassword(isIdentityProtected, oldPasswordEditText, newPassword)
} else {
AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setMessage(R.string.password_mismatch)
.setTitle(R.string.error)
.setPositiveButton(R.string.ok, null)
.show()
}
newPassword.fill(0)
newPasswordConfirm.fill(0)
AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setMessage(R.string.password_mismatch)
.setTitle(R.string.error)
.setPositiveButton(R.string.ok, null)
.show()
}
newPassword.fill(0)
newPasswordConfirm.fill(0)
}
.setNegativeButton(R.string.cancel, null)
.show()
@ -188,7 +193,13 @@ class SettingsActivity: AppCompatActivity() {
} else {
null
}
if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(activity), oldPassword, newPassword)) {
if (AIRADatabase.changePassword(databaseFolder, oldPassword, newPassword)) {
val isNowIdentityProtected = newPassword != null
updateStartAtBootSwitch(isNowIdentityProtected)
if (isIdentityProtected && !isNowIdentityProtected ) {
startAtBootSwitch.isChecked = true
}
} else {
AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setMessage(R.string.change_password_failed)
.setTitle(R.string.error)
@ -197,6 +208,15 @@ class SettingsActivity: AppCompatActivity() {
}
oldPassword?.fill(0)
}
private fun updateStartAtBootSwitch(isIdentityProtected: Boolean) {
startAtBootSwitch.isEnabled = !isIdentityProtected
startAtBootSwitch.summary = getString(if (isIdentityProtected) {
R.string.start_at_boot_summary_identity_protected
} else {
R.string.start_at_boot_summary
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@ -89,6 +89,7 @@ class AIRAService : Service() {
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {}
}
lateinit var identityName: String
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
val pendingMsgs = mutableMapOf<Int, MutableList<ByteArray>>()
val savedNames = mutableMapOf<Int, String>()
@ -96,7 +97,6 @@ class AIRAService : Service() {
val notSeen = mutableListOf<Int>()
var uiCallbacks: UiCallbacks? = null
var isAppInBackground = true
var identityName: String? = null
inner class AIRABinder : Binder() {
fun getService(): AIRAService = this@AIRAService
@ -500,14 +500,12 @@ class AIRAService : Service() {
}
}
MESSAGE_SEND_NAME -> {
identityName?.let {
val tellingName = Protocol.name(it)
for (session in sessions.values) {
try {
session.encryptAndSend(tellingName, usePadding)
} catch (e: SocketException) {
e.printStackTrace()
}
val tellingName = Protocol.name(identityName)
for (session in sessions.values) {
try {
session.encryptAndSend(tellingName, usePadding)
} catch (e: SocketException) {
e.printStackTrace()
}
}
}
@ -540,6 +538,7 @@ class AIRAService : Service() {
}
}
}
identityName = AIRADatabase.getIdentityName(Constants.getDatabaseFolder(this))!!
val contactList = AIRADatabase.loadContacts()
if (contactList == null) {
contacts = HashMap(0)
@ -773,9 +772,7 @@ class AIRAService : Service() {
}
}
Protocol.ASK_PROFILE_INFO -> {
identityName?.let { name ->
session.encryptAndSend(Protocol.name(name), usePadding)
}
session.encryptAndSend(Protocol.name(identityName), usePadding)
AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(this))?.let { avatar ->
session.encryptAndSend(Protocol.avatar(avatar), usePadding)
}

View File

@ -0,0 +1,36 @@
package sushi.hardcore.aira.background_service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.preference.PreferenceManager
import sushi.hardcore.aira.AIRADatabase
import sushi.hardcore.aira.Constants
class SystemBroadcastReceiver: BroadcastReceiver() {
init {
AIRADatabase.init()
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("startAtBoot", true) && !AIRAService.isServiceRunning) {
val databaseFolder = Constants.getDatabaseFolder(context)
val isProtected = AIRADatabase.isIdentityProtected(databaseFolder)
val name = AIRADatabase.getIdentityName(databaseFolder)
if (name != null && !isProtected) {
if (AIRADatabase.loadIdentity(databaseFolder, null)) {
AIRADatabase.clearCache()
val serviceIntent = Intent(context, AIRAService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}
}
}
}

View File

@ -11,21 +11,21 @@ jni = { version = "0.19", default-features = false }
crate-type = ["dylib"]
[dependencies]
rand = "0.8.3"
rand-7 = {package = "rand", version = "0.7.3"}
lazy_static = "1.4.0"
rusqlite = { version = "0.25.1", features = ["bundled"] }
rand = "0.8"
rand-7 = {package = "rand", version = "0.7"}
lazy_static = "1.4"
rusqlite = { version = "0.25", features = ["bundled"] }
ed25519-dalek = "1" #for singing
x25519-dalek = "1.1" #for shared secret
sha2 = "0.9.3"
hkdf = "0.11.0"
aes-gcm = "0.9.0" #PSEC
aes-gcm-siv = "0.10.0" #Database
hmac = "0.11.0"
hex = "0.4.3"
strum_macros = "0.20.1" #display enums
sha2 = "0.9"
hkdf = "0.11"
aes-gcm = "0.9" #PSEC
aes-gcm-siv = "0.10" #Database
hmac = "0.11"
hex = "0.4"
strum_macros = "0.21" #display enums
uuid = { version = "0.8", features = ["v4"] }
scrypt = "0.7.0"
zeroize = "1.2.0"
log = "0.4.14"
android_log = "0.1.3"
scrypt = "0.7"
zeroize = "1.4"
log = "0.4"
android_log = "0.1"

View File

@ -53,7 +53,7 @@ fn slice_to_jvalue<'a>(env: JNIEnv, input: &'a [u8]) -> JValue<'a> {
}
#[no_mangle]
pub extern fn Java_sushi_hardcore_aira_LoginActivity_00024Companion_initLogging(_: JNIEnv, _: JClass) -> jboolean {
pub extern fn Java_sushi_hardcore_aira_AIRADatabase_initLogging(_: JNIEnv, _: JClass) -> jboolean {
bool_to_jboolean(android_log::init("AIRA Native").is_ok())
}
@ -76,7 +76,7 @@ pub extern fn Java_sushi_hardcore_aira_CreateIdentityFragment_createNewIdentity(
#[no_mangle]
pub extern fn Java_sushi_hardcore_aira_LoginActivity_getIdentityName(env: JNIEnv, _: JClass, database_folder: JString) -> jobject {
pub extern fn Java_sushi_hardcore_aira_AIRADatabase_getIdentityName(env: JNIEnv, _: JClass, database_folder: JString) -> jobject {
*match Identity::get_identity_name(&jstring_to_string(env, database_folder)) {
Ok(name) => *env.new_string(name).unwrap(),
Err(e) => {

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM3,9.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM6,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,10.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM14,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM14,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM3,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM10,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM10,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM18,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM14,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM10,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM14,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5z"/>
<path android:fillColor="#ffffff" android:pathData="M6,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM3,9.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM6,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,10.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM14,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM14,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM3,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM10,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM10,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM18,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM14,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM10,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM14,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5z"/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512.004"
android:viewportHeight="512.004">
<path
android:fillColor="#FFF"
android:pathData="m130.239,138.268 l-44.358,3.427c-12.343,0.954 -23.336,7.423 -30.162,17.748l-51.157,77.372c-5.177,7.83 -6,17.629 -2.203,26.213 3.798,8.584 11.603,14.566 20.878,16.003l40.615,6.29c9.501,-50.42 32.245,-100.716 66.387,-147.053z"/>
<path
android:fillColor="#FFF"
android:pathData="m226.682,448.151 l6.291,40.615c1.437,9.275 7.419,17.08 16.002,20.877 3.571,1.58 7.351,2.36 11.112,2.36 5.283,0 10.529,-1.539 15.102,-4.563l77.374,-51.156c10.325,-6.827 16.794,-17.821 17.746,-30.162l3.427,-44.358c-46.338,34.143 -96.633,56.887 -147.054,66.387z"/>
<path
android:fillColor="#FFF"
android:pathData="m211.407,420c1.41,0 2.828,-0.116 4.243,-0.352 21.124,-3.532 41.484,-9.482 60.906,-17.27l-166.93,-166.93c-7.788,19.421 -13.738,39.781 -17.27,60.906 -1.392,8.327 1.401,16.81 7.37,22.78l93.144,93.144c4.956,4.955 11.645,7.722 18.537,7.722z"/>
<path
android:fillColor="#FFF"
android:pathData="m471.178,227.003c40.849,-78.974 42.362,-162.43 40.227,-201.57 -0.731,-13.411 -11.423,-24.103 -24.835,-24.834 -6.373,-0.348 -13.926,-0.599 -22.439,-0.599 -43.766,0 -113.017,6.629 -179.131,40.826 -52.542,27.177 -121.439,87.018 -162.087,165.66 0.48,0.375 0.949,0.773 1.391,1.215l180,180c0.442,0.442 0.839,0.91 1.214,1.39 78.642,-40.649 138.483,-109.546 165.66,-162.088zM297.698,108.24c29.241,-29.241 76.822,-29.244 106.065,0 14.166,14.165 21.967,33 21.967,53.033s-7.801,38.868 -21.967,53.033c-14.619,14.619 -33.829,21.93 -53.032,21.932 -19.209,0.001 -38.41,-7.309 -53.033,-21.932 -14.166,-14.165 -21.968,-33 -21.968,-53.033s7.802,-38.868 21.968,-53.033z"/>
<path
android:fillColor="#FFF"
android:pathData="m318.911,193.092c17.545,17.545 46.095,17.546 63.64,0 8.499,-8.5 13.18,-19.8 13.18,-31.82s-4.681,-23.32 -13.18,-31.819c-8.772,-8.773 -20.296,-13.159 -31.82,-13.159 -11.523,0 -23.047,4.386 -31.819,13.159 -8.499,8.499 -13.181,19.799 -13.181,31.819s4.681,23.321 13.18,31.82z"/>
<path
android:fillColor="#FFF"
android:pathData="m15.305,421.938c3.839,0 7.678,-1.464 10.606,-4.394l48.973,-48.973c5.858,-5.858 5.858,-15.355 0,-21.213 -5.857,-5.858 -15.355,-5.858 -21.213,0l-48.973,48.973c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.394 10.607,4.394z"/>
<path
android:fillColor="#FFF"
android:pathData="m119.765,392.239c-5.857,-5.858 -15.355,-5.858 -21.213,0l-94.155,94.155c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.393 10.607,4.393s7.678,-1.464 10.606,-4.394l94.154,-94.154c5.859,-5.858 5.859,-15.355 0.001,-21.213z"/>
<path
android:fillColor="#FFF"
android:pathData="m143.432,437.12 l-48.972,48.973c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.394 10.606,4.394s7.678,-1.464 10.606,-4.394l48.973,-48.973c5.858,-5.858 5.858,-15.355 0,-21.213 -5.857,-5.858 -15.355,-5.858 -21.213,0z"/>
</vector>

View File

@ -104,6 +104,10 @@
<string name="pending_messages">Pending messages:</string>
<string name="sending_pending_messages">Sending pending messages…</string>
<string name="stop">Stop</string>
<string name="app">App</string>
<string name="start_at_boot">Start AIRA service at boot</string>
<string name="start_at_boot_summary">If disabled, you won\'t receive messages until you open the app manually.</string>
<string name="start_at_boot_summary_identity_protected">Only available if identity is not protected by a password.</string>
<!--accessibility strings-->
<string name="send_file">Send file</string>

View File

@ -34,6 +34,17 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/app">
<SwitchPreferenceCompat
android:key="startAtBoot"
android:defaultValue="true"
android:title="@string/start_at_boot"
android:summary="@string/start_at_boot_summary"
android:icon="@drawable/ic_shuttle"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/security">
<SwitchPreferenceCompat