Skip to content

Commit fd105b6

Browse files
committed
Implemented privacy settings
1 parent 574cfae commit fd105b6

26 files changed

+1288
-15
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.nmc.android.ui
2+
3+
import android.text.Spannable
4+
import android.text.style.ClickableSpan
5+
import android.view.View
6+
import android.widget.TextView
7+
import androidx.test.espresso.UiController
8+
import androidx.test.espresso.ViewAction
9+
import androidx.test.espresso.ViewInteraction
10+
import androidx.test.espresso.assertion.ViewAssertions.matches
11+
import androidx.test.espresso.matcher.BoundedMatcher
12+
import androidx.test.espresso.matcher.ViewMatchers
13+
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
14+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
15+
import org.hamcrest.Description
16+
import org.hamcrest.Matcher
17+
18+
object ClickableSpanTestHelper {
19+
20+
/**
21+
* method to get clickable span form a text view
22+
* example: val clickableSpan = getClickableSpan("Link text", onView(withId(R.id.text_id)))
23+
*/
24+
fun getClickableSpan(spanText: String, matcher: ViewInteraction?): ClickableSpan? {
25+
val clickableSpans = arrayOf<ClickableSpan?>(null)
26+
27+
// Get the SpannableString from the TextView
28+
matcher?.check(matches(isDisplayed()))
29+
matcher?.perform(object : ViewAction {
30+
override fun getConstraints(): Matcher<View> {
31+
return isAssignableFrom(TextView::class.java)
32+
}
33+
34+
override fun getDescription(): String {
35+
return "get text from TextView"
36+
}
37+
38+
override fun perform(uiController: UiController, view: View) {
39+
val textView = view as TextView
40+
val text = textView.text
41+
if (text is Spannable) {
42+
val spans = text.getSpans(
43+
0, text.length,
44+
ClickableSpan::class.java
45+
)
46+
for (span in spans) {
47+
val start = text.getSpanStart(span)
48+
val end = text.getSpanEnd(span)
49+
val spanString = text.subSequence(start, end).toString()
50+
if (spanString == spanText) {
51+
clickableSpans[0] = span
52+
return
53+
}
54+
}
55+
}
56+
throw java.lang.RuntimeException("ClickableSpan not found")
57+
}
58+
})
59+
return clickableSpans[0]
60+
}
61+
62+
/**
63+
* perform click on the spanned string
64+
* @link getClickableSpan() method to get clickable span
65+
*/
66+
fun performClickSpan(clickableSpan: ClickableSpan?): ViewAction {
67+
return object : ViewAction {
68+
override fun getConstraints(): Matcher<View> {
69+
return ViewMatchers.isAssignableFrom(TextView::class.java)
70+
}
71+
72+
override fun getDescription(): String {
73+
return "clicking on a span"
74+
}
75+
76+
override fun perform(uiController: UiController, view: View) {
77+
val textView = view as TextView
78+
val spannable = textView.text as Spannable
79+
val spans = spannable.getSpans(
80+
0, spannable.length,
81+
ClickableSpan::class.java
82+
)
83+
for (span in spans) {
84+
if (span == clickableSpan) {
85+
span.onClick(textView)
86+
return
87+
}
88+
}
89+
throw RuntimeException("ClickableSpan not found")
90+
}
91+
}
92+
}
93+
94+
fun verifyClickSpan(clickableSpan: ClickableSpan?): Matcher<View?> {
95+
return object : BoundedMatcher<View?, TextView>(TextView::class.java) {
96+
override fun describeTo(description: Description) {
97+
description.appendText("clickable span")
98+
}
99+
100+
override fun matchesSafely(textView: TextView): Boolean {
101+
val spannable = textView.text as Spannable
102+
val spans = spannable.getSpans(
103+
0, spannable.length,
104+
ClickableSpan::class.java
105+
)
106+
for (span in spans) {
107+
if (span == clickableSpan) {
108+
return true
109+
}
110+
}
111+
return false
112+
}
113+
}
114+
}
115+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.nmc.android.ui
2+
3+
import androidx.test.espresso.Espresso.onView
4+
import androidx.test.espresso.Espresso.pressBack
5+
import androidx.test.espresso.action.ViewActions.click
6+
import androidx.test.espresso.assertion.ViewAssertions.matches
7+
import androidx.test.espresso.intent.Intents
8+
import androidx.test.espresso.intent.Intents.intended
9+
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
10+
import androidx.test.espresso.matcher.ViewMatchers.isClickable
11+
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
12+
import androidx.test.espresso.matcher.ViewMatchers.withId
13+
import androidx.test.ext.junit.rules.ActivityScenarioRule
14+
import androidx.test.ext.junit.runners.AndroidJUnit4
15+
import com.nextcloud.client.preferences.AppPreferencesImpl
16+
import com.nmc.android.ui.ClickableSpanTestHelper.getClickableSpan
17+
import com.owncloud.android.AbstractIT
18+
import com.owncloud.android.R
19+
import com.owncloud.android.ui.activity.ExternalSiteWebView
20+
import com.owncloud.android.ui.activity.FileDisplayActivity
21+
import org.junit.Assert.*
22+
import org.junit.Rule
23+
import org.junit.Test
24+
import org.junit.runner.RunWith
25+
26+
@RunWith(AndroidJUnit4::class)
27+
class LoginPrivacySettingsActivityIT : AbstractIT() {
28+
29+
@get:Rule
30+
val activityRule = ActivityScenarioRule(LoginPrivacySettingsActivity::class.java)
31+
32+
@Test
33+
fun verifyNothingHappensOnBackPress() {
34+
pressBack()
35+
shortSleep()
36+
37+
//check any one view to check the activity is not destroyed
38+
onView(withId(R.id.tv_privacy_setting_title)).check(matches(isCompletelyDisplayed()))
39+
}
40+
41+
@Test
42+
fun verifyUIElements() {
43+
onView(withId(R.id.ic_privacy)).check(matches(isCompletelyDisplayed()))
44+
45+
onView(withId(R.id.tv_privacy_setting_title)).check(matches(isCompletelyDisplayed()))
46+
47+
onView(withId(R.id.tv_login_privacy_intro_text)).check(matches(isCompletelyDisplayed()))
48+
49+
onView(withId(R.id.privacy_accept_btn)).check(matches(isCompletelyDisplayed()))
50+
onView(withId(R.id.privacy_accept_btn)).check(matches(isClickable()))
51+
}
52+
53+
@Test
54+
fun verifyAcceptButtonRedirection() {
55+
Intents.init()
56+
onView(withId(R.id.privacy_accept_btn)).perform(click())
57+
58+
//check if the policy action saved correct --> 2 for Accept action
59+
assertEquals(2, AppPreferencesImpl.fromContext(targetContext).privacyPolicyAction)
60+
61+
intended(hasComponent(FileDisplayActivity::class.java.canonicalName))
62+
Intents.release()
63+
}
64+
65+
@Test
66+
fun verifySettingsTextClick() {
67+
Intents.init()
68+
val settingsClickableSpan = getClickableSpan("Settings", onView(withId(R.id.tv_login_privacy_intro_text)))
69+
onView(withId(R.id.tv_login_privacy_intro_text)).perform(
70+
ClickableSpanTestHelper.performClickSpan(
71+
settingsClickableSpan
72+
)
73+
)
74+
intended(hasComponent(PrivacySettingsActivity::class.java.canonicalName))
75+
Intents.release()
76+
}
77+
78+
@Test
79+
fun verifyPrivacyPolicyTextClick() {
80+
Intents.init()
81+
val privacyPolicyClickableSpan =
82+
getClickableSpan("Privacy Policy", onView(withId(R.id.tv_login_privacy_intro_text)))
83+
onView(withId(R.id.tv_login_privacy_intro_text)).perform(
84+
ClickableSpanTestHelper.performClickSpan(
85+
privacyPolicyClickableSpan
86+
)
87+
)
88+
intended(hasComponent(ExternalSiteWebView::class.java.canonicalName))
89+
Intents.release()
90+
}
91+
92+
@Test
93+
fun verifyRejectTextClick() {
94+
Intents.init()
95+
val rejectClickableSpan =
96+
getClickableSpan("reject", onView(withId(R.id.tv_login_privacy_intro_text)))
97+
onView(withId(R.id.tv_login_privacy_intro_text)).perform(
98+
ClickableSpanTestHelper.performClickSpan(
99+
rejectClickableSpan
100+
)
101+
)
102+
103+
//check if the policy action saved correct --> 1 for Reject action
104+
assertEquals(1, AppPreferencesImpl.fromContext(targetContext).privacyPolicyAction)
105+
106+
intended(hasComponent(FileDisplayActivity::class.java.canonicalName))
107+
Intents.release()
108+
}
109+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.nmc.android.ui
2+
3+
import android.content.Intent
4+
import androidx.test.core.app.ActivityScenario
5+
import androidx.test.core.app.launchActivity
6+
import androidx.test.espresso.Espresso.onView
7+
import androidx.test.espresso.action.ViewActions.click
8+
import androidx.test.espresso.assertion.ViewAssertions.matches
9+
import androidx.test.espresso.matcher.ViewMatchers.isChecked
10+
import androidx.test.espresso.matcher.ViewMatchers.isClickable
11+
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
12+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
13+
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
14+
import androidx.test.espresso.matcher.ViewMatchers.withId
15+
import androidx.test.ext.junit.runners.AndroidJUnit4
16+
import com.owncloud.android.AbstractIT
17+
import com.owncloud.android.R
18+
import org.hamcrest.CoreMatchers.not
19+
import org.junit.After
20+
import org.junit.Assert.*
21+
import org.junit.Before
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
25+
@RunWith(AndroidJUnit4::class)
26+
class PrivacySettingsActivityIT : AbstractIT() {
27+
28+
private fun getIntent(showSettingsButton: Boolean): Intent =
29+
Intent(targetContext, PrivacySettingsActivity::class.java)
30+
.putExtra("show_settings_button", showSettingsButton)
31+
32+
lateinit var activityRule: ActivityScenario<PrivacySettingsActivity>
33+
34+
@Before
35+
fun setUp() {
36+
activityRule = launchActivity(getIntent(false))
37+
}
38+
39+
@Test
40+
fun verifyUIElements() {
41+
onView(withId(R.id.tv_privacy_intro_text)).check(matches(isCompletelyDisplayed()))
42+
43+
onView(withId(R.id.switch_data_collection)).check(matches(isCompletelyDisplayed()))
44+
onView(withId(R.id.switch_data_collection)).check(matches(not(isEnabled())))
45+
onView(withId(R.id.switch_data_collection)).check(matches(isChecked()))
46+
47+
onView(withId(R.id.switch_data_analysis)).check(matches(isCompletelyDisplayed()))
48+
onView(withId(R.id.switch_data_analysis)).check(matches(isEnabled()))
49+
//by-default the analysis switch will be checked as per #AppPreferences.isDataAnalysisEnabled will return true
50+
onView(withId(R.id.switch_data_analysis)).check(matches(isChecked()))
51+
onView(withId(R.id.switch_data_analysis)).check(matches(isClickable()))
52+
53+
onView(withId(R.id.privacy_save_settings_btn)).check(matches(not(isDisplayed())))
54+
}
55+
56+
@Test
57+
fun verifyDataCollectionSwitchToggle() {
58+
//since this button is disabled performing click operation should do nothing
59+
//and switch will be in checked state only
60+
onView(withId(R.id.switch_data_collection)).perform(click())
61+
onView(withId(R.id.switch_data_collection)).check(matches(isChecked()))
62+
63+
onView(withId(R.id.switch_data_collection)).perform(click())
64+
onView(withId(R.id.switch_data_collection)).check(matches(isChecked()))
65+
}
66+
67+
@Test
68+
fun verifyDataAnalysisSwitchToggle() {
69+
onView(withId(R.id.switch_data_analysis)).perform(click())
70+
onView(withId(R.id.switch_data_analysis)).check(matches(not(isChecked())))
71+
72+
onView(withId(R.id.switch_data_analysis)).perform(click())
73+
onView(withId(R.id.switch_data_analysis)).check(matches(isChecked()))
74+
}
75+
76+
@Test
77+
fun verifySaveSettingsButton() {
78+
//button not shown on the basis of extras passed to intent
79+
onView(withId(R.id.privacy_save_settings_btn)).check(matches(not(isDisplayed())))
80+
//close the activity already open
81+
activityRule.close()
82+
83+
//launch activity with extras as true
84+
activityRule = launchActivity(getIntent(true))
85+
//button will be shown if extras is true
86+
onView(withId(R.id.privacy_save_settings_btn)).check(matches(isDisplayed()))
87+
}
88+
89+
@After
90+
fun tearDown() {
91+
activityRule.close()
92+
}
93+
}

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,15 @@
493493
android:name=".ui.preview.PreviewBitmapActivity"
494494
android:exported="false"
495495
android:theme="@style/Theme.ownCloud.OverlayGrey" />
496+
<activity
497+
android:name="com.nmc.android.ui.PrivacySettingsActivity"
498+
android:exported="false"
499+
android:windowSoftInputMode="stateAlwaysHidden" />
500+
501+
<activity
502+
android:name="com.nmc.android.ui.LoginPrivacySettingsActivity"
503+
android:exported="false"
504+
android:windowSoftInputMode="stateAlwaysHidden" />
496505
<activity
497506
android:name="com.nextcloud.client.documentscan.DocumentScanActivity"
498507
android:exported="false"

app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import com.nextcloud.ui.SetStatusDialogFragment;
4040
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
4141
import com.nmc.android.ui.LauncherActivity;
42+
import com.nmc.android.ui.LoginPrivacySettingsActivity;
43+
import com.nmc.android.ui.PrivacySettingsActivity;
4244
import com.owncloud.android.MainApp;
4345
import com.owncloud.android.authentication.AuthenticatorActivity;
4446
import com.owncloud.android.authentication.DeepLinkLoginActivity;
@@ -467,6 +469,12 @@ abstract class ComponentsModule {
467469
@ContributesAndroidInjector
468470
abstract DocumentScanActivity documentScanActivity();
469471

472+
@ContributesAndroidInjector
473+
abstract PrivacySettingsActivity privacySettingsActivity();
474+
475+
@ContributesAndroidInjector
476+
abstract LoginPrivacySettingsActivity loginPrivacySettingsActivity();
477+
470478
@ContributesAndroidInjector
471479
abstract GroupfolderListFragment groupfolderListFragment();
472480

app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,24 @@ default void onDarkThemeModeChanged(DarkMode mode) {
347347

348348
void setCurrentAccountName(String accountName);
349349

350+
/**
351+
* Saves the data analysis from privacy settings
352+
* on disabling it we should disable Adjust SDK tracking
353+
*
354+
* @param enableDataAnalysis to enable/disable data analysis
355+
*/
356+
void setDataAnalysis(boolean enableDataAnalysis);
357+
boolean isDataAnalysisEnabled();
358+
359+
/**
360+
* Saves the privacy policy action taken by user
361+
* this will maintain the state of current privacy policy action taken
362+
* @see com.nmc.android.ui.LoginPrivacySettingsActivity for actions
363+
* @param userAction taken by user
364+
*/
365+
void setPrivacyPolicyAction(int userAction);
366+
int getPrivacyPolicyAction();
367+
350368
/**
351369
* Gets status of migration to user id, default false
352370
*

0 commit comments

Comments
 (0)