Skip to content

Commit 0378f5f

Browse files
committed
Customized Settings as per MC with test cases.
Added delete account menu under settings. NMC-3041 NMC-4681 -- enable logs for debug builds NMC-4850 -- use appcompat text appearance instead of material for separating headings NMC-4888 -- add option to save logs
1 parent 2a0b507 commit 0378f5f

File tree

25 files changed

+1352
-243
lines changed

25 files changed

+1352
-243
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package com.nmc.android.ui
2+
3+
import android.preference.ListPreference
4+
import android.preference.Preference
5+
import androidx.test.espresso.Espresso.onData
6+
import androidx.test.espresso.assertion.ViewAssertions.matches
7+
import androidx.test.espresso.matcher.PreferenceMatchers
8+
import androidx.test.espresso.matcher.PreferenceMatchers.withKey
9+
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
10+
import androidx.test.ext.junit.rules.ActivityScenarioRule
11+
import com.owncloud.android.AbstractIT
12+
import com.owncloud.android.R
13+
import com.owncloud.android.ui.AppVersionPreference
14+
import com.owncloud.android.ui.PreferenceCustomCategory
15+
import com.owncloud.android.ui.ThemeableSwitchPreference
16+
import com.owncloud.android.ui.activity.SettingsActivity
17+
import org.hamcrest.Matchers.allOf
18+
import org.hamcrest.Matchers.instanceOf
19+
import org.hamcrest.Matchers.`is`
20+
import org.junit.Assert.assertEquals
21+
import org.junit.Rule
22+
import org.junit.Test
23+
24+
class SettingsPreferenceIT : AbstractIT() {
25+
26+
@get:Rule
27+
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
28+
29+
@Test
30+
fun verifyPreferenceSectionCustomClass() {
31+
activityRule.scenario.onActivity {
32+
val preferenceAccountInfo = it.findPreference("account_info")
33+
val preferenceGeneral = it.findPreference("general")
34+
val preferenceDetails = it.findPreference("details")
35+
val preferenceMore = it.findPreference("more")
36+
val preferenceDataProtection = it.findPreference("data_protection")
37+
val preferenceInfo = it.findPreference("info")
38+
39+
val preferenceCategoryList = listOf(
40+
preferenceAccountInfo,
41+
preferenceGeneral,
42+
preferenceDetails,
43+
preferenceMore,
44+
preferenceDataProtection,
45+
preferenceInfo
46+
)
47+
48+
for (preference in preferenceCategoryList) {
49+
assertEquals(PreferenceCustomCategory::class.java, preference.javaClass)
50+
}
51+
}
52+
}
53+
54+
@Test
55+
fun verifySwitchPreferenceCustomClass() {
56+
activityRule.scenario.onActivity {
57+
val preferenceShowHiddenFiles = it.findPreference("show_hidden_files")
58+
assertEquals(ThemeableSwitchPreference::class.java, preferenceShowHiddenFiles.javaClass)
59+
}
60+
}
61+
62+
@Test
63+
fun verifyAppVersionPreferenceCustomClass() {
64+
activityRule.scenario.onActivity {
65+
val preferenceAboutApp = it.findPreference("about_app")
66+
assertEquals(AppVersionPreference::class.java, preferenceAboutApp.javaClass)
67+
}
68+
}
69+
70+
@Test
71+
fun verifyPreferenceChildCustomLayout() {
72+
activityRule.scenario.onActivity {
73+
val userName = it.findPreference("user_name")
74+
val storagePath = it.findPreference("storage_path")
75+
val lock = it.findPreference("lock")
76+
val showHiddenFiles = it.findPreference("show_hidden_files")
77+
val syncedFolders = it.findPreference("syncedFolders")
78+
val backup = it.findPreference("backup")
79+
val mnemonic = it.findPreference("mnemonic")
80+
val privacySettings = it.findPreference("privacy_settings")
81+
val privacyPolicy = it.findPreference("privacy_policy")
82+
val sourceCode = it.findPreference("sourcecode")
83+
val help = it.findPreference("help")
84+
val imprint = it.findPreference("imprint")
85+
86+
val preferenceList = listOf(
87+
userName,
88+
storagePath,
89+
lock,
90+
showHiddenFiles,
91+
syncedFolders,
92+
backup,
93+
mnemonic,
94+
privacySettings,
95+
privacyPolicy,
96+
sourceCode,
97+
help,
98+
imprint
99+
)
100+
101+
for (preference in preferenceList) {
102+
assertEquals(R.layout.custom_preference_layout, preference.layoutResource)
103+
}
104+
105+
val aboutApp = it.findPreference("about_app")
106+
assertEquals(R.layout.custom_app_preference_layout, aboutApp.layoutResource)
107+
108+
}
109+
}
110+
111+
@Test
112+
fun verifyPreferencesTitleText() {
113+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("account_info"),
114+
PreferenceMatchers.withTitleText("Account Information")))
115+
.check(matches(isCompletelyDisplayed()))
116+
117+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("user_name"),
118+
PreferenceMatchers.withTitleText("test")))
119+
.check(matches(isCompletelyDisplayed()))
120+
121+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("general"),
122+
PreferenceMatchers.withTitleText("General")))
123+
.check(matches(isCompletelyDisplayed()))
124+
125+
onData(allOf(`is`(instanceOf(ListPreference::class.java)), withKey("storage_path"),
126+
PreferenceMatchers.withTitleText("Data storage folder")))
127+
.check(matches(isCompletelyDisplayed()))
128+
129+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("details"),
130+
PreferenceMatchers.withTitleText("Details")))
131+
.check(matches(isCompletelyDisplayed()))
132+
133+
onData(allOf(`is`(instanceOf(ListPreference::class.java)), withKey("lock"),
134+
PreferenceMatchers.withTitleText("App passcode")))
135+
.check(matches(isCompletelyDisplayed()))
136+
137+
onData(allOf(`is`(instanceOf(ThemeableSwitchPreference::class.java)), withKey("show_hidden_files"),
138+
PreferenceMatchers.withTitleText("Show hidden files")))
139+
.check(matches(isCompletelyDisplayed()))
140+
141+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("more"),
142+
PreferenceMatchers.withTitleText("More")))
143+
.check(matches(isCompletelyDisplayed()))
144+
145+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("syncedFolders"),
146+
PreferenceMatchers.withTitleText("Auto upload")))
147+
.check(matches(isCompletelyDisplayed()))
148+
149+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("backup"),
150+
PreferenceMatchers.withTitleText("Back up contacts")))
151+
.check(matches(isCompletelyDisplayed()))
152+
153+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("mnemonic"),
154+
PreferenceMatchers.withTitleText("E2E mnemonic")))
155+
.check(matches(isCompletelyDisplayed()))
156+
157+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("logger"),
158+
PreferenceMatchers.withTitleText("Logs")))
159+
.check(matches(isCompletelyDisplayed()))
160+
161+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("data_protection"),
162+
PreferenceMatchers.withTitleText("Data Privacy")))
163+
.check(matches(isCompletelyDisplayed()))
164+
165+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("privacy_settings"),
166+
PreferenceMatchers.withTitleText("Privacy Settings")))
167+
.check(matches(isCompletelyDisplayed()))
168+
169+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("privacy_policy"),
170+
PreferenceMatchers.withTitleText("Privacy Policy")))
171+
.check(matches(isCompletelyDisplayed()))
172+
173+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("sourcecode"),
174+
PreferenceMatchers.withTitleText("Used OpenSource Software")))
175+
.check(matches(isCompletelyDisplayed()))
176+
177+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("service"),
178+
PreferenceMatchers.withTitleText("Service")))
179+
.check(matches(isCompletelyDisplayed()))
180+
181+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("help"),
182+
PreferenceMatchers.withTitleText("Help")))
183+
.check(matches(isCompletelyDisplayed()))
184+
185+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("imprint"),
186+
PreferenceMatchers.withTitleText("Imprint")))
187+
.check(matches(isCompletelyDisplayed()))
188+
189+
onData(allOf(`is`(instanceOf(PreferenceCustomCategory::class.java)), withKey("info"),
190+
PreferenceMatchers.withTitleText("Info")))
191+
.check(matches(isCompletelyDisplayed()))
192+
}
193+
194+
@Test
195+
fun verifyPreferencesSummaryText() {
196+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("lock"),
197+
PreferenceMatchers.withSummaryText("None")))
198+
.check(matches(isCompletelyDisplayed()))
199+
200+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("syncedFolders"),
201+
PreferenceMatchers.withSummaryText("Manage folders for auto upload")))
202+
.check(matches(isCompletelyDisplayed()))
203+
204+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("backup"),
205+
PreferenceMatchers.withSummaryText("Daily backup of your calendar & contacts")))
206+
.check(matches(isCompletelyDisplayed()))
207+
208+
onData(allOf(`is`(instanceOf(Preference::class.java)), withKey("mnemonic"),
209+
PreferenceMatchers.withSummaryText("To show mnemonic please enable device credentials.")))
210+
.check(matches(isCompletelyDisplayed()))
211+
}
212+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Nextcloud - Android Client
3+
~
4+
~ SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
5+
~ SPDX-License-Identifier: AGPL-3.0-or-later
6+
-->
7+
8+
<resources>
9+
<!-- enable logs in debug builds -->
10+
<bool name="logger_enabled">true</bool>
11+
</resources>

app/src/main/java/com/nextcloud/client/logger/ui/LogsActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class LogsActivity : ToolbarActivity() {
8585
android.R.id.home -> finish()
8686
R.id.action_delete_logs -> vm.deleteAll()
8787
R.id.action_send_logs -> vm.send()
88+
R.id.action_save_logs -> vm.save()
8889
R.id.action_refresh_logs -> vm.load()
8990
else -> retval = super.onOptionsItemSelected(item)
9091
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
6+
*/
7+
package com.nextcloud.client.logger.ui
8+
9+
import android.app.DownloadManager
10+
import android.app.NotificationManager
11+
import android.app.PendingIntent
12+
import android.content.Context
13+
import android.content.Intent
14+
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
15+
import androidx.core.app.NotificationCompat
16+
import com.nextcloud.client.core.AsyncRunner
17+
import com.nextcloud.client.core.Cancellable
18+
import com.nextcloud.client.core.Clock
19+
import com.nextcloud.client.logger.LogEntry
20+
import com.owncloud.android.R
21+
import com.owncloud.android.lib.common.utils.Log_OC
22+
import com.owncloud.android.ui.notifications.NotificationUtils
23+
import com.owncloud.android.utils.DisplayUtils
24+
import com.owncloud.android.utils.FileExportUtils
25+
import java.io.File
26+
import java.io.FileWriter
27+
import java.security.SecureRandom
28+
import java.util.TimeZone
29+
30+
// NMC-4888 task
31+
class LogsSaveHandler(private val context: Context, private val clock: Clock, private val runner: AsyncRunner) {
32+
33+
private companion object {
34+
private const val LOGS_MIME_TYPE = "text/plain"
35+
private const val LOGS_DATE_FORMAT = "yyyyMMdd_HHmmssZ"
36+
private val notificationId = SecureRandom().nextInt()
37+
}
38+
39+
private class Task(
40+
private val logs: List<LogEntry>,
41+
private val file: File,
42+
private val tz: TimeZone
43+
) : Function0<File> {
44+
45+
override fun invoke(): File {
46+
file.parentFile?.mkdirs()
47+
val fo = FileWriter(file, false)
48+
logs.forEach {
49+
fo.write(it.toString(tz))
50+
fo.write("\n")
51+
}
52+
fo.close()
53+
return file
54+
}
55+
}
56+
57+
private var task: Cancellable? = null
58+
59+
fun save(logs: List<LogEntry>) {
60+
if (task == null) {
61+
val timestamp = DisplayUtils.getDateByPattern(System.currentTimeMillis(), context, LOGS_DATE_FORMAT)
62+
val logFileName = "logs_${context.resources.getString(R.string.app_name)}_${timestamp}.txt"
63+
val outFile = File(context.cacheDir, logFileName)
64+
task = runner.postQuickTask(Task(logs, outFile, clock.tz), onResult = {
65+
task = null
66+
export(it)
67+
})
68+
}
69+
}
70+
71+
fun stop() {
72+
if (task != null) {
73+
task?.cancel()
74+
task = null
75+
}
76+
}
77+
78+
private fun export(file: File) {
79+
task = null
80+
try {
81+
FileExportUtils().exportFile(
82+
file.name,
83+
LOGS_MIME_TYPE,
84+
context.contentResolver,
85+
null,
86+
file
87+
)
88+
showSuccessNotification()
89+
} catch (e: IllegalStateException) {
90+
Log_OC.e("LogsSaveHandler", "Error saving logs to file", e)
91+
showErrorNotification()
92+
}
93+
}
94+
95+
private fun showErrorNotification() {
96+
showNotification(false, context.resources.getString(R.string.logs_export_failed))
97+
}
98+
99+
private fun showSuccessNotification() {
100+
showNotification(true, context.resources.getString(R.string.logs_export_success))
101+
}
102+
103+
private fun showNotification(isSuccess: Boolean, message: String) {
104+
val notificationBuilder = NotificationCompat.Builder(
105+
context,
106+
NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD
107+
)
108+
.setSmallIcon(R.drawable.notification_icon)
109+
.setContentTitle(message)
110+
.setAutoCancel(true)
111+
112+
// NMC Customization
113+
notificationBuilder.color = context.resources.getColor(R.color.primary, null)
114+
115+
if (isSuccess) {
116+
val actionIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
117+
flags = FLAG_ACTIVITY_NEW_TASK
118+
}
119+
val actionPendingIntent = PendingIntent.getActivity(
120+
context,
121+
notificationId,
122+
actionIntent,
123+
PendingIntent.FLAG_CANCEL_CURRENT or
124+
PendingIntent.FLAG_IMMUTABLE
125+
)
126+
notificationBuilder.addAction(
127+
NotificationCompat.Action(
128+
null,
129+
context.getString(R.string.locate_folder),
130+
actionPendingIntent
131+
)
132+
)
133+
}
134+
135+
val notificationManager = context
136+
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
137+
notificationManager.notify(notificationId, notificationBuilder.build())
138+
}
139+
}

app/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class LogsViewModel @Inject constructor(
3232

3333
private val asyncFilter = AsyncFilter(asyncRunner)
3434
private val sender = LogsEmailSender(context, clock, asyncRunner)
35+
private val logsSaver = LogsSaveHandler(context, clock, asyncRunner)
3536
private var allEntries = emptyList<LogEntry>()
3637
private var logsSize = -1L
3738
private var filterDurationMs = 0L
@@ -48,6 +49,12 @@ class LogsViewModel @Inject constructor(
4849
}
4950
}
5051

52+
fun save() {
53+
entries.value?.let {
54+
logsSaver.save(it)
55+
}
56+
}
57+
5158
fun load() {
5259
if (isLoading.value != true) {
5360
logsRepository.load(this::onLoaded)

0 commit comments

Comments
 (0)