Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions app/src/androidTest/java/com/nmc/android/ui/SettingsPreferenceIT.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package com.nmc.android.ui

import android.preference.ListPreference
import android.preference.Preference
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.PreferenceMatchers
import androidx.test.espresso.matcher.PreferenceMatchers.withKey
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import com.owncloud.android.ui.AppVersionPreference
import com.owncloud.android.ui.PreferenceCustomCategory
import com.owncloud.android.ui.ThemeableSwitchPreference
import com.owncloud.android.ui.activity.SettingsActivity
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class SettingsPreferenceIT : AbstractIT() {

@get:Rule
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)

@Test
fun verifyPreferenceSectionCustomClass() {
activityRule.scenario.onActivity {
val preferenceAccountInfo = it.findPreference("account_info")
val preferenceGeneral = it.findPreference("general")
val preferenceDetails = it.findPreference("details")
val preferenceMore = it.findPreference("more")
val preferenceDataProtection = it.findPreference("data_protection")
val preferenceInfo = it.findPreference("info")

val preferenceCategoryList = listOf(
preferenceAccountInfo,
preferenceGeneral,
preferenceDetails,
preferenceMore,
preferenceDataProtection,
preferenceInfo
)

for (preference in preferenceCategoryList) {
assertEquals(PreferenceCustomCategory::class.java, preference.javaClass)
}
}
}

@Test
fun verifySwitchPreferenceCustomClass() {
activityRule.scenario.onActivity {
val preferenceShowHiddenFiles = it.findPreference("show_hidden_files")
assertEquals(ThemeableSwitchPreference::class.java, preferenceShowHiddenFiles.javaClass)
}
}

@Test
fun verifyAppVersionPreferenceCustomClass() {
activityRule.scenario.onActivity {
val preferenceAboutApp = it.findPreference("about_app")
assertEquals(AppVersionPreference::class.java, preferenceAboutApp.javaClass)
}
}

@Test
fun verifyPreferenceChildCustomLayout() {
activityRule.scenario.onActivity {
val userName = it.findPreference("user_name")
val storagePath = it.findPreference("storage_path")
val lock = it.findPreference("lock")
val showHiddenFiles = it.findPreference("show_hidden_files")
val syncedFolders = it.findPreference("syncedFolders")
val backup = it.findPreference("backup")
val mnemonic = it.findPreference("mnemonic")
val privacySettings = it.findPreference("privacy_settings")
val privacyPolicy = it.findPreference("privacy_policy")
val sourceCode = it.findPreference("sourcecode")
val help = it.findPreference("help")
val imprint = it.findPreference("imprint")

val preferenceList = listOf(
userName,
storagePath,
lock,
showHiddenFiles,
syncedFolders,
backup,
mnemonic,
privacySettings,
privacyPolicy,
sourceCode,
help,
imprint
)

for (preference in preferenceList) {
assertEquals(R.layout.custom_preference_layout, preference.layoutResource)
}

val aboutApp = it.findPreference("about_app")
assertEquals(R.layout.custom_app_preference_layout, aboutApp.layoutResource)

}
}

@Test
fun verifyPreferencesTitleText() {
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("account_info"),
PreferenceMatchers.withTitleText("Account Information")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("user_name"),
PreferenceMatchers.withTitleText("test")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("general"),
PreferenceMatchers.withTitleText("General")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(ListPreference::class.java)), withKey("storage_path"),
PreferenceMatchers.withTitleText("Data storage folder")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("details"),
PreferenceMatchers.withTitleText("Details")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(ListPreference::class.java)), withKey("lock"),
PreferenceMatchers.withTitleText("App passcode")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(ThemeableSwitchPreference::class.java)), withKey("show_hidden_files"),
PreferenceMatchers.withTitleText("Show hidden files")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("more"),
PreferenceMatchers.withTitleText("More")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("syncedFolders"),
PreferenceMatchers.withTitleText("Auto upload")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("backup"),
PreferenceMatchers.withTitleText("Back up contacts")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("mnemonic"),
PreferenceMatchers.withTitleText("E2E mnemonic")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("logger"),
PreferenceMatchers.withTitleText("Logs")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("data_protection"),
PreferenceMatchers.withTitleText("Data Privacy")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("privacy_settings"),
PreferenceMatchers.withTitleText("Privacy Settings")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("privacy_policy"),
PreferenceMatchers.withTitleText("Privacy Policy")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("sourcecode"),
PreferenceMatchers.withTitleText("Used OpenSource Software")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("service"),
PreferenceMatchers.withTitleText("Service")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("help"),
PreferenceMatchers.withTitleText("Help")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("imprint"),
PreferenceMatchers.withTitleText("Imprint")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("info"),
PreferenceMatchers.withTitleText("Info")))
.check(matches(isCompletelyDisplayed()))
}

@Test
fun verifyPreferencesSummaryText() {
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("lock"),
PreferenceMatchers.withSummaryText("None")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("syncedFolders"),
PreferenceMatchers.withSummaryText("Manage folders for auto upload")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("backup"),
PreferenceMatchers.withSummaryText("Daily backup of your calendar & contacts")))
.check(matches(isCompletelyDisplayed()))

onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("mnemonic"),
PreferenceMatchers.withSummaryText("To show mnemonic please enable device credentials.")))
.check(matches(isCompletelyDisplayed()))
}
}
11 changes: 11 additions & 0 deletions app/src/debug/res/values/log_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->

<resources>
<!-- enable logs in debug builds -->
<bool name="logger_enabled">true</bool>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class LogsActivity : ToolbarActivity() {
android.R.id.home -> finish()
R.id.action_delete_logs -> vm.deleteAll()
R.id.action_send_logs -> vm.send()
R.id.action_save_logs -> vm.save()
R.id.action_refresh_logs -> vm.load()
else -> retval = super.onOptionsItemSelected(item)
}
Expand Down
139 changes: 139 additions & 0 deletions app/src/main/java/com/nextcloud/client/logger/ui/LogsSaveHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.logger.ui

import android.app.DownloadManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.core.app.NotificationCompat
import com.nextcloud.client.core.AsyncRunner
import com.nextcloud.client.core.Cancellable
import com.nextcloud.client.core.Clock
import com.nextcloud.client.logger.LogEntry
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileExportUtils
import java.io.File
import java.io.FileWriter
import java.security.SecureRandom
import java.util.TimeZone

// NMC-4888 task
class LogsSaveHandler(private val context: Context, private val clock: Clock, private val runner: AsyncRunner) {

private companion object {
private const val LOGS_MIME_TYPE = "text/plain"
private const val LOGS_DATE_FORMAT = "yyyyMMdd_HHmmssZ"
private val notificationId = SecureRandom().nextInt()
}

private class Task(
private val logs: List<LogEntry>,
private val file: File,
private val tz: TimeZone
) : Function0<File> {

override fun invoke(): File {
file.parentFile?.mkdirs()
val fo = FileWriter(file, false)
logs.forEach {
fo.write(it.toString(tz))
fo.write("\n")
}
fo.close()
return file
}
}

private var task: Cancellable? = null

fun save(logs: List<LogEntry>) {
if (task == null) {
val timestamp = DisplayUtils.getDateByPattern(System.currentTimeMillis(), context, LOGS_DATE_FORMAT)
val logFileName = "logs_${context.resources.getString(R.string.app_name)}_${timestamp}.txt"
val outFile = File(context.cacheDir, logFileName)
task = runner.postQuickTask(Task(logs, outFile, clock.tz), onResult = {
task = null
export(it)
})
}
}

fun stop() {
if (task != null) {
task?.cancel()
task = null
}
}

private fun export(file: File) {
task = null
try {
FileExportUtils().exportFile(
file.name,
LOGS_MIME_TYPE,
context.contentResolver,
null,
file
)
showSuccessNotification()
} catch (e: IllegalStateException) {
Log_OC.e("LogsSaveHandler", "Error saving logs to file", e)
showErrorNotification()
}
}

private fun showErrorNotification() {
showNotification(false, context.resources.getString(R.string.logs_export_failed))
}

private fun showSuccessNotification() {
showNotification(true, context.resources.getString(R.string.logs_export_success))
}

private fun showNotification(isSuccess: Boolean, message: String) {
val notificationBuilder = NotificationCompat.Builder(
context,
NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(message)
.setAutoCancel(true)

// NMC Customization
notificationBuilder.color = context.resources.getColor(R.color.primary, null)

if (isSuccess) {
val actionIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
flags = FLAG_ACTIVITY_NEW_TASK
}
val actionPendingIntent = PendingIntent.getActivity(
context,
notificationId,
actionIntent,
PendingIntent.FLAG_CANCEL_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder.addAction(
NotificationCompat.Action(
null,
context.getString(R.string.locate_folder),
actionPendingIntent
)
)
}

val notificationManager = context
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(notificationId, notificationBuilder.build())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class LogsViewModel @Inject constructor(

private val asyncFilter = AsyncFilter(asyncRunner)
private val sender = LogsEmailSender(context, clock, asyncRunner)
private val logsSaver = LogsSaveHandler(context, clock, asyncRunner)
private var allEntries = emptyList<LogEntry>()
private var logsSize = -1L
private var filterDurationMs = 0L
Expand All @@ -46,6 +47,12 @@ class LogsViewModel @Inject constructor(
}
}

fun save() {
entries.value?.let {
logsSaver.save(it)
}
}

fun load() {
if (isLoading.value != true) {
logsRepository.load(this::onLoaded)
Expand Down
Loading