🆕 Adding Notification support

This commit is contained in:
Mylloon 2021-09-04 20:29:22 +02:00
parent ce6dca5363
commit c5fc622d8c
7 changed files with 352 additions and 26 deletions

View file

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mylloon.mobidl" > package="com.mylloon.mobidl" >
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -23,6 +27,16 @@
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" /> android:resource="@array/preloaded_fonts" />
<receiver
android:name="BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,4 @@
{
"tiktok": "musically",
"twitch livestream multiplayer": "twitch"
}

View file

@ -4,6 +4,7 @@ package com.mylloon.mobidl
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -16,20 +17,73 @@ import android.os.Looper
import android.text.Editable import android.text.Editable
import android.text.InputType import android.text.InputType
import android.text.TextWatcher import android.text.TextWatcher
import android.text.method.ScrollingMovementMethod
import android.util.DisplayMetrics
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.*
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
import android.text.method.ScrollingMovementMethod import java.util.concurrent.ExecutionException
import android.util.DisplayMetrics import java.util.concurrent.TimeUnit
private fun isWorkScheduled(context: Context): Boolean {
val instance = WorkManager.getInstance(context)
val statuses = instance.getWorkInfosByTag("com.mylloon.mobidl.BackgroundUpdateCheck")
return try {
var running = false
val workInfoList = statuses.get()
for (workInfo in workInfoList) {
val state = workInfo.state
running = (state == WorkInfo.State.RUNNING) or (state == WorkInfo.State.ENQUEUED)
}
running
} catch (e: ExecutionException) {
e.printStackTrace()
println("No job already running")
false
} catch (e: InterruptedException) {
e.printStackTrace()
println("No job already running")
false
}
}
fun newJob(context: Context) {
val myConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) //checks whether device should have Network Connection
.build()
val job = PeriodicWorkRequestBuilder<BackgroundUpdateCheck>(15, TimeUnit.MINUTES)
.setConstraints(myConstraints)
.build()
WorkManager.getInstance(context).enqueue(job)
}
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!isWorkScheduled(context)) {
println("Create a update check job in background...")
when (intent.action) {
Intent.ACTION_DATE_CHANGED -> {
newJob(context)
}
Intent.ACTION_BOOT_COMPLETED -> {
val sharedPref = context.getSharedPreferences("com.mylloon.MobiDL",
AppCompatActivity.MODE_PRIVATE)
sharedPref.edit().putInt("notifID", 0).apply()
newJob(context)
}
}
}
}
}
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private var settingsButton: Menu? = null // before starting the app there is no settings button private var settingsButton: Menu? = null // before starting the app there is no settings button
@ -37,8 +91,8 @@ class MainActivity : AppCompatActivity() {
private var inSettings: Boolean = false // by default your not in settings page private var inSettings: Boolean = false // by default your not in settings page
private var inAppList: Boolean = false // by default your not in app list page private var inAppList: Boolean = false // by default your not in app list page
private var inAppInfo: Boolean = false // by default your not in app info page private var inAppInfo: Boolean = false // by default your not in app info page
private var prefs: SharedPreferences? = null // first run detection private var sharedPref: SharedPreferences? = null // first run detection
private val sharedPref = "com.mylloon.MobiDL" // shared pref name private val sharedPrefName = "com.mylloon.MobiDL" // shared pref name
private var timeOfLastToast: Long = System.currentTimeMillis() - 2000 private var timeOfLastToast: Long = System.currentTimeMillis() - 2000
private var listInfos: MutableList<Map<String, String?>>? = null private var listInfos: MutableList<Map<String, String?>>? = null
private var appMobilismInfos: Map<String, String?>? = null private var appMobilismInfos: Map<String, String?>? = null
@ -64,15 +118,54 @@ class MainActivity : AppCompatActivity() {
val context = applicationContext() // get app context val context = applicationContext() // get app context
// read apps from the app preference // read apps from the app preference
val sharedPref = context.getSharedPreferences(sharedPref, MODE_PRIVATE) val sharedPref = context.getSharedPreferences(sharedPrefName, MODE_PRIVATE)
val data: Set<String>? = sharedPref.getStringSet("apps", null) val data: Set<String>? = sharedPref.getStringSet("apps", null)
return data?.toMutableList() ?: mutableListOf() return data?.toMutableList() ?: mutableListOf()
} }
private fun getValuesAppNeedToBeUpdated(): MutableMap<String, Boolean> { // list of the apps (from the storage if exists)
val context = applicationContext() // get app context
// read apps from the app preference
val sharedPref = context.getSharedPreferences(sharedPrefName, MODE_PRIVATE)
val dataApp: Set<String>? = sharedPref.getStringSet("appsUpdate", null)
val dataBool: Set<String>? = sharedPref.getStringSet("boolUpdate", null)
val dataAppList = dataApp?.toList()
val dataBoolList = dataBool?.toList()
val finalMap = mutableMapOf<String, Boolean>()
if ((dataAppList != null) and (dataBoolList != null)) {
for (i in dataAppList!!.indices) {
if (i in dataBoolList!!.indices) {
finalMap[dataAppList[i]] =
dataBoolList[i].substring(i.toString().length).toBoolean()
}
}
}
return finalMap
}
private fun storeValuesAppNeedToBeUpdated(updateApps: MutableMap<String, Boolean>) {
val updateAppsName: Set<String> = updateApps.keys
val updateAppsBool: MutableCollection<Boolean> = updateApps.values
val updateAppsBoolList: List<Boolean> = updateAppsBool.toList()
val updateAppsBoolSet: MutableList<String> = mutableListOf()
for (i in updateAppsBoolList.indices) {
updateAppsBoolSet.add(i.toString() + updateAppsBoolList[i].toString())
}
with(applicationContext().getSharedPreferences(sharedPrefName, MODE_PRIVATE).edit()) {
this?.putStringSet("appsUpdate", updateAppsName)
this?.putStringSet("boolUpdate", updateAppsBoolSet.toSet())
this?.apply()
}
}
override fun onCreate(savedInstanceState: Bundle?) { // Main function override fun onCreate(savedInstanceState: Bundle?) { // Main function
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
prefs = getSharedPreferences("com.mylloon.MobiDL", MODE_PRIVATE) sharedPref = getSharedPreferences(sharedPrefName, MODE_PRIVATE)
try { try {
if (Credentials().get(0) == null) { // test if credentials have already been registered if (Credentials().get(0) == null) { // test if credentials have already been registered
@ -128,6 +221,7 @@ class MainActivity : AppCompatActivity() {
private fun mainPage(toDelete: String? = null) { private fun mainPage(toDelete: String? = null) {
if (toDelete == null) { if (toDelete == null) {
setContentView(R.layout.activity_main) // display main page setContentView(R.layout.activity_main) // display main page
if (!isWorkScheduled(applicationContext)) newJob(applicationContext) // run background job if not already running
if (!settingsButtonVisible) { // check if the settings button isn't already showed if (!settingsButtonVisible) { // check if the settings button isn't already showed
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
toggleSettingsButtonVisibility() toggleSettingsButtonVisibility()
@ -137,6 +231,7 @@ class MainActivity : AppCompatActivity() {
val user = Credentials().get(0).toString() val user = Credentials().get(0).toString()
this.title = "${getString(R.string.app_name)} (${getString(R.string.connected_as)} $user)" this.title = "${getString(R.string.app_name)} (${getString(R.string.connected_as)} $user)"
val valuesRecyclerView = getValuesRecyclerView() // list of all the element in the main page val valuesRecyclerView = getValuesRecyclerView() // list of all the element in the main page
val checkedItemListOfApps = getValuesAppNeedToBeUpdated()
class Adapter(private val values: List<String>) : class Adapter(private val values: List<String>) :
RecyclerView.Adapter<Adapter.ViewHolder>() { RecyclerView.Adapter<Adapter.ViewHolder>() {
@ -163,20 +258,30 @@ class MainActivity : AppCompatActivity() {
button?.setOnLongClickListener { button?.setOnLongClickListener {
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =
AlertDialog.Builder(instance) AlertDialog.Builder(instance)
builder.setTitle("${getString(R.string.remove)} ${button?.text} ?") builder.setTitle("${getString(R.string.config)} ${button?.text} ?")
builder.setPositiveButton(R.string.remove) { _, _ -> builder.setPositiveButton(R.string.remove) { _, _ ->
instance?.mainPage( instance?.mainPage(
button?.text.toString() button?.text.toString()
) )
checkedItemListOfApps[button?.text.toString()] = false
storeValuesAppNeedToBeUpdated(checkedItemListOfApps)
} }
val checkedItems = booleanArrayOf( val appInstalled = isItAnInstalledApp(button?.text.toString())
false // get this info from the app files somewhere if (appInstalled != null) {
) val checkedItems = booleanArrayOf(
builder.setMultiChoiceItems( checkedItemListOfApps[button?.text.toString()] == true
arrayOf(getString(R.string.updateCheck)), )
checkedItems builder.setMultiChoiceItems(
) { _, _, isChecked -> arrayOf(getString(R.string.updateCheck)),
println("Update for ${button?.text.toString()}: " + if (isChecked) "enabled" else "disabled" + "") checkedItems
) { _, _, isChecked ->
checkedItemListOfApps[button?.text.toString()] = isChecked
storeValuesAppNeedToBeUpdated(checkedItemListOfApps)
}
builder.setNegativeButton(R.string.forceUpdate) { _, _ ->
Notif().work(applicationContext,
button?.text.toString())
}
} }
builder.setNeutralButton(R.string.cancel) { dialog, _ -> dialog.cancel() } builder.setNeutralButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
builder.show() builder.show()
@ -205,11 +310,8 @@ class MainActivity : AppCompatActivity() {
if (toDelete != null) { if (toDelete != null) {
valuesRecyclerView.remove(toDelete) valuesRecyclerView.remove(toDelete)
val context = applicationContext() // get app context
// read apps from the app preference // read apps from the app preference
val sharedPref = context.getSharedPreferences(sharedPref, MODE_PRIVATE) sharedPref!!.edit().putStringSet("apps", valuesRecyclerView.toSet()).apply()
sharedPref.edit().putStringSet("apps", valuesRecyclerView.toSet()).apply()
recyclerView.adapter = Adapter(valuesRecyclerView) recyclerView.adapter = Adapter(valuesRecyclerView)
} }
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)
@ -219,11 +321,8 @@ class MainActivity : AppCompatActivity() {
fun addApp(app: String) { fun addApp(app: String) {
valuesRecyclerView.add(app) valuesRecyclerView.add(app)
val context = applicationContext() // get app context
// read apps from the app preference // read apps from the app preference
val sharedPref = context.getSharedPreferences(sharedPref, MODE_PRIVATE) sharedPref!!.edit().putStringSet("apps", valuesRecyclerView.toSet()).apply()
sharedPref.edit().putStringSet("apps", valuesRecyclerView.toSet()).apply()
recyclerView.adapter = Adapter(valuesRecyclerView) recyclerView.adapter = Adapter(valuesRecyclerView)
} }
@ -249,6 +348,21 @@ class MainActivity : AppCompatActivity() {
} }
} }
fun isItAnInstalledApp(app: String): String? {
var appName = app
val registeredAnswered: Map<String, String> = Gson().fromJson(
assets.open("appNames.json").bufferedReader()
.use { it.readText() },
object : TypeToken<Map<String, String>>() {}.type
)
for ((key, value) in registeredAnswered) appName =
if (Regex(appName.lowercase()).containsMatchIn(key)) value else appName.lowercase()
for (packageInfo in packageManager.getInstalledPackages(PackageManager.GET_META_DATA)) {
if (Regex(appName).containsMatchIn(packageInfo.packageName)) return packageInfo.versionName
}
return null
}
private fun callScrapper( private fun callScrapper(
user: String, user: String,
password: String, password: String,
@ -470,9 +584,13 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
val title = findViewById<TextView>(R.id.textViewAppName) val title = findViewById<TextView>(R.id.textViewAppName)
title.text = appMobilismInfos!!["title"] title.text = appMobilismInfos!!["title"]
title.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://forum.mobilism.org$link"))) } title.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("https://forum.mobilism.org$link")))
}
findViewById<TextView>(R.id.textViewAppAuthor).text = appMobilismInfos!!["author"] findViewById<TextView>(R.id.textViewAppAuthor).text = appMobilismInfos!!["author"]
findViewById<TextView>(R.id.textViewAppDate).text = appMobilismInfos!!["date"] findViewById<TextView>(R.id.textViewAppDate).text = appMobilismInfos!!["date"]
val changelogs = findViewById<TextView>(R.id.textViewAppChangelogs) val changelogs = findViewById<TextView>(R.id.textViewAppChangelogs)
@ -527,9 +645,9 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (prefs!!.getBoolean("firstrun", true)) { if (sharedPref!!.getBoolean("firstrun", true)) {
Credentials().generateKey() // Generate RSA keys Credentials().generateKey() // Generate RSA keys
prefs!!.edit().putBoolean("firstrun", false) sharedPref!!.edit().putBoolean("firstrun", false)
.apply() // first run done, now the next ones won't be "first". .apply() // first run done, now the next ones won't be "first".
} }
} }

View file

@ -0,0 +1,169 @@
package com.mylloon.mobidl
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.Color
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
var sharedPrefName = "com.mylloon.MobiDL" // shared pref name
class BackgroundUpdateCheck(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
private val ctx = appContext
override fun doWork(): Result {
fun getValuesAppNeedToBeUpdated(): MutableMap<String, Boolean> { // list of the apps (from the storage if exists)
val context = MainActivity.applicationContext() // get app context
// read apps from the app preference
val sharedPref = context.getSharedPreferences(sharedPrefName,
AppCompatActivity.MODE_PRIVATE)
val dataApp: Set<String>? = sharedPref.getStringSet("appsUpdate", null)
val dataBool: Set<String>? = sharedPref.getStringSet("boolUpdate", null)
val dataAppList = dataApp?.toList()
val dataBoolList = dataBool?.toList()
val finalMap = mutableMapOf<String, Boolean>()
if ((dataAppList != null) and (dataBoolList != null)) {
for (i in dataAppList!!.indices) {
if (i in dataBoolList!!.indices) {
finalMap[dataAppList[i]] =
dataBoolList[i].substring(i.toString().length).toBoolean()
}
}
}
return finalMap
}
val apps = getValuesAppNeedToBeUpdated()
for (app in apps.keys) {
if (apps[app] == true) {
println("Checking update for $app...")
Notif().work(ctx, app)
}
}
println("Update check: Done.")
return Result.success()
}
}
class Notif {
private lateinit var notificationManager: NotificationManager
private lateinit var notificationChannel: NotificationChannel
private lateinit var builder: Notification.Builder
private val channelId = "i.apps.notifications"
private val description = R.string.descriptionNotification
@SuppressLint("UnspecifiedImmutableFlag")
fun newNotification(
context: Context,
notificationID: Int,
appName: String,
version: String,
url: String,
) {
notificationManager = context.getSystemService(NotificationManager::class.java)
val pendingIntent = PendingIntent.getActivity(context,
notificationID,
Intent(Intent.ACTION_VIEW, Uri.parse(url)),
(PendingIntent.FLAG_UPDATE_CURRENT))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // checking if android version is greater than oreo(API 26) or not
notificationChannel = NotificationChannel(channelId,
context.getString(description),
NotificationManager.IMPORTANCE_LOW)
notificationChannel.enableLights(true)
notificationChannel.lightColor = Color.GREEN
notificationChannel.enableVibration(false)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(context, channelId)
.setContentTitle("${context.getString(R.string.newUpdateTitleNotification)} $appName")
.setContentText("${context.getString(R.string.newUpdateVersionNotification)} $version ${
context.getString(R.string.newUpdateAvailableNotification)
}")
.setSmallIcon(R.drawable.download)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
} else {
builder = Notification.Builder(context)
.setContentTitle("${context.getString(R.string.newUpdateTitleNotification)} $appName")
.setContentText("${context.getString(R.string.newUpdateVersionNotification)} $version ${
context.getString(R.string.newUpdateAvailableNotification)
}")
.setSmallIcon(R.drawable.download)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
}
notificationManager.notify(notificationID, builder.build())
val sharedPref =
context.getSharedPreferences(sharedPrefName, AppCompatActivity.MODE_PRIVATE)
sharedPref.edit().putInt("notifID", notificationID + 1).apply()
}
fun work(context: Context, app: String) {
var appName = app
val registeredAnswered: Map<String, String> = Gson().fromJson(
context.assets.open("appNames.json").bufferedReader()
.use { it.readText() },
object : TypeToken<Map<String, String>>() {}.type
)
for ((key, value) in registeredAnswered) appName =
if (Regex(appName.lowercase()).containsMatchIn(key)) value else appName.lowercase()
var installedAppVersion: String? = null
for (packageInfo in context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)) {
if (Regex(appName).containsMatchIn(packageInfo.packageName)) installedAppVersion =
packageInfo.versionName
}
if (installedAppVersion == null) return
else {
try {
val res =
Scraper(Credentials().get(0), Credentials().get(1), app).search() ?: return
if (res[0].containsKey("gotResults")) {
var latestVersion = res[1]["title"]!!
latestVersion = Regex("""(?<=v?)\d+\.\d+(\.\d+)?""").findAll(latestVersion)
.map { it.groupValues[0] }.toList()[0]
val arrayInstalledVersion: MutableList<String> =
installedAppVersion.split(".") as MutableList<String>
val arrayLatestVersion: MutableList<String> =
latestVersion.split(".") as MutableList<String>
while (arrayInstalledVersion.size > arrayLatestVersion.size) arrayLatestVersion.add(
"0")
while (arrayInstalledVersion.size < arrayLatestVersion.size) arrayInstalledVersion.add(
"0")
var needUpdate = false
for (i in arrayInstalledVersion.indices) {
if (arrayLatestVersion[i].toInt() > arrayInstalledVersion[i].toInt()) needUpdate =
true
}
if (needUpdate) {
val sharedPref = context.getSharedPreferences(sharedPrefName,
AppCompatActivity.MODE_PRIVATE)
return newNotification(context,
sharedPref.getInt("notifID", 0),
app,
latestVersion,
"https://forum.mobilism.org${res[1]["link"]}")
}
}
} catch (e: Exception) {
return
}
}
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#8E44AD"
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="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
</vector>

View file

@ -32,4 +32,11 @@
<string name="changelogs">Changements</string> <string name="changelogs">Changements</string>
<string name="downloads">Téléchargements</string> <string name="downloads">Téléchargements</string>
<string name="noLinkFound">Aucun lien trouvé</string> <string name="noLinkFound">Aucun lien trouvé</string>
<string name="descriptionNotification">Mise à jour des applications</string>
<string name="newUpdateTitleNotification">Nouvelle version pour</string>
<string name="newUpdateVersionNotification">Version</string>
<string name="newUpdateAvailableNotification">disponible</string>
<string name="forceUpdate">Force recherche de MàJ</string>
<string name="rename">Renommer</string>
<string name="config">Configurer</string>
</resources> </resources>

View file

@ -13,11 +13,20 @@
<string name="newAppDialogTitle">New app name</string> <string name="newAppDialogTitle">New app name</string>
<string name="validate">Validate</string> <string name="validate">Validate</string>
<string name="remove">Remove</string> <string name="remove">Remove</string>
<string name="config">Config</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="forceUpdate">Force update search</string>
<string name="rename">Rename</string>
<string name="updateCheck">Update check</string> <string name="updateCheck">Update check</string>
<string name="notConnected">You are not logged in, redirection to the login page</string> <string name="notConnected">You are not logged in, redirection to the login page</string>
<string name="dropBro">drop bro</string> <string name="dropBro">drop bro</string>
<!-- Notifications -->
<string name="descriptionNotification">Application Update</string>
<string name="newUpdateTitleNotification">New version for</string>
<string name="newUpdateVersionNotification">Version</string>
<string name="newUpdateAvailableNotification">available</string>
<!-- Settings page --> <!-- Settings page -->
<string name="titleSettings">Settings</string> <string name="titleSettings">Settings</string>
<string name="changeCredentials">Change credentials</string> <string name="changeCredentials">Change credentials</string>