From 0aad3dc37e9611c0fdfdd3612fd86baed386e1e4 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Wed, 7 Jul 2021 17:21:49 +0530 Subject: [PATCH] Implemented privacy settings --- .../nmc/android/ui/ClickableSpanTestHelper.kt | 115 ++++++++++++++++ .../ui/LoginPrivacySettingsActivityIT.kt | 109 +++++++++++++++ .../android/ui/PrivacySettingsActivityIT.kt | 93 +++++++++++++ app/src/main/AndroidManifest.xml | 9 ++ .../nextcloud/client/di/ComponentsModule.java | 8 ++ .../client/preferences/AppPreferences.java | 18 +++ .../preferences/AppPreferencesImpl.java | 25 ++++ .../com/nmc/android/ui/LauncherActivity.kt | 47 +++++-- .../ui/LoginPrivacySettingsActivity.kt | 107 +++++++++++++++ .../nmc/android/ui/PrivacySettingsActivity.kt | 87 ++++++++++++ .../android/ui/PrivacySettingsInterface.kt | 13 ++ .../ui/PrivacySettingsInterfaceImpl.kt | 14 ++ .../com/nmc/android/ui/PrivacyUserAction.kt | 9 ++ .../nmc/android/utils/CheckableThemeUtils.kt | 117 ++++++++++++++++ .../java/com/nmc/android/utils/Extensions.kt | 42 ++++++ .../authentication/AuthenticatorActivity.java | 9 +- .../android/ui/activity/SettingsActivity.java | 15 ++ .../main/res/drawable/ic_privacy_settings.xml | 12 ++ .../activity_login_privacy_settings.xml | 84 ++++++++++++ .../res/layout/activity_privacy_settings.xml | 128 ++++++++++++++++++ app/src/main/res/values-de/strings.xml | 21 +++ app/src/main/res/values-night/colors.xml | 64 +++++++++ app/src/main/res/values/colors.xml | 89 ++++++++++++ app/src/main/res/values/dimens.xml | 31 +++++ app/src/main/res/values/setup.xml | 2 +- app/src/main/res/values/strings.xml | 21 +++ 26 files changed, 1274 insertions(+), 15 deletions(-) create mode 100644 app/src/androidTest/java/com/nmc/android/ui/ClickableSpanTestHelper.kt create mode 100644 app/src/androidTest/java/com/nmc/android/ui/LoginPrivacySettingsActivityIT.kt create mode 100644 app/src/androidTest/java/com/nmc/android/ui/PrivacySettingsActivityIT.kt create mode 100644 app/src/main/java/com/nmc/android/ui/LoginPrivacySettingsActivity.kt create mode 100644 app/src/main/java/com/nmc/android/ui/PrivacySettingsActivity.kt create mode 100644 app/src/main/java/com/nmc/android/ui/PrivacySettingsInterface.kt create mode 100644 app/src/main/java/com/nmc/android/ui/PrivacySettingsInterfaceImpl.kt create mode 100644 app/src/main/java/com/nmc/android/ui/PrivacyUserAction.kt create mode 100644 app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt create mode 100644 app/src/main/java/com/nmc/android/utils/Extensions.kt create mode 100644 app/src/main/res/drawable/ic_privacy_settings.xml create mode 100644 app/src/main/res/layout/activity_login_privacy_settings.xml create mode 100644 app/src/main/res/layout/activity_privacy_settings.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/src/androidTest/java/com/nmc/android/ui/ClickableSpanTestHelper.kt b/app/src/androidTest/java/com/nmc/android/ui/ClickableSpanTestHelper.kt new file mode 100644 index 000000000000..59c91cd42dbf --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/ClickableSpanTestHelper.kt @@ -0,0 +1,115 @@ +package com.nmc.android.ui + +import android.text.Spannable +import android.text.style.ClickableSpan +import android.view.View +import android.widget.TextView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.BoundedMatcher +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import org.hamcrest.Description +import org.hamcrest.Matcher + +object ClickableSpanTestHelper { + + /** + * method to get clickable span form a text view + * example: val clickableSpan = getClickableSpan("Link text", onView(withId(R.id.text_id))) + */ + fun getClickableSpan(spanText: String, matcher: ViewInteraction?): ClickableSpan? { + val clickableSpans = arrayOf(null) + + // Get the SpannableString from the TextView + matcher?.check(matches(isDisplayed())) + matcher?.perform(object : ViewAction { + override fun getConstraints(): Matcher { + return isAssignableFrom(TextView::class.java) + } + + override fun getDescription(): String { + return "get text from TextView" + } + + override fun perform(uiController: UiController, view: View) { + val textView = view as TextView + val text = textView.text + if (text is Spannable) { + val spans = text.getSpans( + 0, text.length, + ClickableSpan::class.java + ) + for (span in spans) { + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + val spanString = text.subSequence(start, end).toString() + if (spanString == spanText) { + clickableSpans[0] = span + return + } + } + } + throw java.lang.RuntimeException("ClickableSpan not found") + } + }) + return clickableSpans[0] + } + + /** + * perform click on the spanned string + * @link getClickableSpan() method to get clickable span + */ + fun performClickSpan(clickableSpan: ClickableSpan?): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher { + return ViewMatchers.isAssignableFrom(TextView::class.java) + } + + override fun getDescription(): String { + return "clicking on a span" + } + + override fun perform(uiController: UiController, view: View) { + val textView = view as TextView + val spannable = textView.text as Spannable + val spans = spannable.getSpans( + 0, spannable.length, + ClickableSpan::class.java + ) + for (span in spans) { + if (span == clickableSpan) { + span.onClick(textView) + return + } + } + throw RuntimeException("ClickableSpan not found") + } + } + } + + fun verifyClickSpan(clickableSpan: ClickableSpan?): Matcher { + return object : BoundedMatcher(TextView::class.java) { + override fun describeTo(description: Description) { + description.appendText("clickable span") + } + + override fun matchesSafely(textView: TextView): Boolean { + val spannable = textView.text as Spannable + val spans = spannable.getSpans( + 0, spannable.length, + ClickableSpan::class.java + ) + for (span in spans) { + if (span == clickableSpan) { + return true + } + } + return false + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nmc/android/ui/LoginPrivacySettingsActivityIT.kt b/app/src/androidTest/java/com/nmc/android/ui/LoginPrivacySettingsActivityIT.kt new file mode 100644 index 000000000000..3232d588769e --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/LoginPrivacySettingsActivityIT.kt @@ -0,0 +1,109 @@ +package com.nmc.android.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nextcloud.client.preferences.AppPreferencesImpl +import com.nmc.android.ui.ClickableSpanTestHelper.getClickableSpan +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.ui.activity.ExternalSiteWebView +import com.owncloud.android.ui.activity.FileDisplayActivity +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LoginPrivacySettingsActivityIT : AbstractIT() { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginPrivacySettingsActivity::class.java) + + @Test + fun verifyNothingHappensOnBackPress() { + pressBack() + shortSleep() + + //check any one view to check the activity is not destroyed + onView(withId(R.id.tv_privacy_setting_title)).check(matches(isCompletelyDisplayed())) + } + + @Test + fun verifyUIElements() { + onView(withId(R.id.ic_privacy)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.tv_privacy_setting_title)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.tv_login_privacy_intro_text)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.privacy_accept_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.privacy_accept_btn)).check(matches(isClickable())) + } + + @Test + fun verifyAcceptButtonRedirection() { + Intents.init() + onView(withId(R.id.privacy_accept_btn)).perform(click()) + + //check if the policy action saved correct --> 2 for Accept action + assertEquals(2, AppPreferencesImpl.fromContext(targetContext).privacyPolicyAction) + + intended(hasComponent(FileDisplayActivity::class.java.canonicalName)) + Intents.release() + } + + @Test + fun verifySettingsTextClick() { + Intents.init() + val settingsClickableSpan = getClickableSpan("Settings", onView(withId(R.id.tv_login_privacy_intro_text))) + onView(withId(R.id.tv_login_privacy_intro_text)).perform( + ClickableSpanTestHelper.performClickSpan( + settingsClickableSpan + ) + ) + intended(hasComponent(PrivacySettingsActivity::class.java.canonicalName)) + Intents.release() + } + + @Test + fun verifyPrivacyPolicyTextClick() { + Intents.init() + val privacyPolicyClickableSpan = + getClickableSpan("Privacy Policy", onView(withId(R.id.tv_login_privacy_intro_text))) + onView(withId(R.id.tv_login_privacy_intro_text)).perform( + ClickableSpanTestHelper.performClickSpan( + privacyPolicyClickableSpan + ) + ) + intended(hasComponent(ExternalSiteWebView::class.java.canonicalName)) + Intents.release() + } + + @Test + fun verifyRejectTextClick() { + Intents.init() + val rejectClickableSpan = + getClickableSpan("reject", onView(withId(R.id.tv_login_privacy_intro_text))) + onView(withId(R.id.tv_login_privacy_intro_text)).perform( + ClickableSpanTestHelper.performClickSpan( + rejectClickableSpan + ) + ) + + //check if the policy action saved correct --> 1 for Reject action + assertEquals(1, AppPreferencesImpl.fromContext(targetContext).privacyPolicyAction) + + intended(hasComponent(FileDisplayActivity::class.java.canonicalName)) + Intents.release() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nmc/android/ui/PrivacySettingsActivityIT.kt b/app/src/androidTest/java/com/nmc/android/ui/PrivacySettingsActivityIT.kt new file mode 100644 index 000000000000..5de5d075991b --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/PrivacySettingsActivityIT.kt @@ -0,0 +1,93 @@ +package com.nmc.android.ui + +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import org.hamcrest.CoreMatchers.not +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PrivacySettingsActivityIT : AbstractIT() { + + private fun getIntent(showSettingsButton: Boolean): Intent = + Intent(targetContext, PrivacySettingsActivity::class.java) + .putExtra("show_settings_button", showSettingsButton) + + lateinit var activityRule: ActivityScenario + + @Before + fun setUp() { + activityRule = launchActivity(getIntent(false)) + } + + @Test + fun verifyUIElements() { + onView(withId(R.id.tv_privacy_intro_text)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.switch_data_collection)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.switch_data_collection)).check(matches(not(isEnabled()))) + onView(withId(R.id.switch_data_collection)).check(matches(isChecked())) + + onView(withId(R.id.switch_data_analysis)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.switch_data_analysis)).check(matches(isEnabled())) + //by-default the analysis switch will be checked as per #AppPreferences.isDataAnalysisEnabled will return true + onView(withId(R.id.switch_data_analysis)).check(matches(isChecked())) + onView(withId(R.id.switch_data_analysis)).check(matches(isClickable())) + + onView(withId(R.id.privacy_save_settings_btn)).check(matches(not(isDisplayed()))) + } + + @Test + fun verifyDataCollectionSwitchToggle() { + //since this button is disabled performing click operation should do nothing + //and switch will be in checked state only + onView(withId(R.id.switch_data_collection)).perform(click()) + onView(withId(R.id.switch_data_collection)).check(matches(isChecked())) + + onView(withId(R.id.switch_data_collection)).perform(click()) + onView(withId(R.id.switch_data_collection)).check(matches(isChecked())) + } + + @Test + fun verifyDataAnalysisSwitchToggle() { + onView(withId(R.id.switch_data_analysis)).perform(click()) + onView(withId(R.id.switch_data_analysis)).check(matches(not(isChecked()))) + + onView(withId(R.id.switch_data_analysis)).perform(click()) + onView(withId(R.id.switch_data_analysis)).check(matches(isChecked())) + } + + @Test + fun verifySaveSettingsButton() { + //button not shown on the basis of extras passed to intent + onView(withId(R.id.privacy_save_settings_btn)).check(matches(not(isDisplayed()))) + //close the activity already open + activityRule.close() + + //launch activity with extras as true + activityRule = launchActivity(getIntent(true)) + //button will be shown if extras is true + onView(withId(R.id.privacy_save_settings_btn)).check(matches(isDisplayed())) + } + + @After + fun tearDown() { + activityRule.close() + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0494a90d4c0..1c37affe82db 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -506,6 +506,15 @@ android:name=".ui.preview.PreviewBitmapActivity" android:exported="false" android:theme="@style/Theme.ownCloud.OverlayGrey" /> + + + Note: use userAccountManager.currentAccount for user validation + // because setting enableAccountHandling false will not set user or account values under SessionMixin class + + // if user is null then go to authenticator activity + if (userAccountManager.currentAccount == null) { + startActivity(Intent(this, AuthenticatorActivity::class.java)) + } + //if user is logged in but did not accepted the privacy policy then take him there + //show him the privacy policy screen again + //check if app has been updated, if yes then also we have to show the privacy policy screen + else if (userAccountManager.currentAccount != null && (appPreferences.privacyPolicyAction == PrivacyUserAction.NO_ACTION + || appPreferences.lastSeenVersionCode < BuildConfig.VERSION_CODE) + ) { + LoginPrivacySettingsActivity.openPrivacySettingsActivity(this) + } else { + startActivity(Intent(this, FileDisplayActivity::class.java)) + } + finish() + } + override fun onCreate(savedInstanceState: Bundle?) { // Mandatory to call this before super method to show system launch screen for api level 31+ installSplashScreen() + //Fix of NMC-2464 + //this is mandatory to call before super() function + //setting false to show launcher screen properly if user is not logged in + enableAccountHandling = false + super.onCreate(savedInstanceState) binding = ActivitySplashBinding.inflate(layoutInflater) @@ -75,17 +103,18 @@ class LauncherActivity : BaseActivity() { } private fun scheduleSplashScreen() { - Handler(Looper.getMainLooper()).postDelayed({ - if (!user.isPresent) { - startActivity(Intent(this, AuthenticatorActivity::class.java)) - } else { - startActivity(Intent(this, FileDisplayActivity::class.java)) - } - finish() - }, SPLASH_DURATION) + handler.postDelayed( + runnable, + SPLASH_DURATION + ) + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(runnable) } companion object { const val SPLASH_DURATION = 1500L } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/ui/LoginPrivacySettingsActivity.kt b/app/src/main/java/com/nmc/android/ui/LoginPrivacySettingsActivity.kt new file mode 100644 index 000000000000..b9c9c203d736 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/LoginPrivacySettingsActivity.kt @@ -0,0 +1,107 @@ +package com.nmc.android.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import com.nextcloud.client.preferences.AppPreferences +import com.nmc.android.utils.makeLinks +import com.owncloud.android.BuildConfig +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityLoginPrivacySettingsBinding +import com.owncloud.android.ui.activity.ExternalSiteWebView +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.ToolbarActivity +import javax.inject.Inject + +class LoginPrivacySettingsActivity : ToolbarActivity() { + + companion object { + @JvmStatic + fun openPrivacySettingsActivity(context: Context) { + val intent = Intent(context, LoginPrivacySettingsActivity::class.java) + context.startActivity(intent) + } + } + + private lateinit var binding: ActivityLoginPrivacySettingsBinding + + @Inject + lateinit var preferences: AppPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLoginPrivacySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + setupToolbar() + resetPreferenceForPrivacy() + //don't show back button + supportActionBar?.setDisplayHomeAsUpEnabled(false) + updateActionBarTitleAndHomeButtonByString(resources.getString(R.string.privacy_settings)) + setUpPrivacyText() + binding.privacyAcceptBtn.setOnClickListener { + //on accept finish the activity + //update the accept privacy action to preferences + preferences.privacyPolicyAction = PrivacyUserAction.ACCEPT_ACTION + openFileDisplayActivity() + } + } + + private fun resetPreferenceForPrivacy() { + preferences.setDataAnalysis(false) + preferences.privacyPolicyAction = PrivacyUserAction.NO_ACTION + } + + private fun setUpPrivacyText() { + val privacyText = String.format( + resources.getString(R.string.login_privacy_settings_intro_text), resources + .getString(R.string.login_privacy_policy), resources + .getString(R.string.login_privacy_reject), resources + .getString(R.string.login_privacy_settings) + ) + binding.tvLoginPrivacyIntroText.text = privacyText + + //make links clickable + binding.tvLoginPrivacyIntroText.makeLinks( + Pair(resources.getString(R.string.login_privacy_policy), View.OnClickListener { + //open privacy policy url + val intent = Intent(this, ExternalSiteWebView::class.java) + intent.putExtra( + ExternalSiteWebView.EXTRA_TITLE, + resources.getString(R.string.privacy_policy) + ) + intent.putExtra(ExternalSiteWebView.EXTRA_URL, resources.getString(R.string.privacy_url)) + intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + intent.putExtra(ExternalSiteWebView.EXTRA_MENU_ITEM_ID, -1) + startActivity(intent) + }), Pair(resources + .getString(R.string.login_privacy_reject), View.OnClickListener { + //disable data analysis option and close the activity + preferences.setDataAnalysis(false) + //update the reject privacy action to preferences + preferences.privacyPolicyAction = PrivacyUserAction.REJECT_ACTION + openFileDisplayActivity() + }), Pair(resources + .getString(R.string.login_privacy_settings), View.OnClickListener { + //open privacy settings screen + PrivacySettingsActivity.openPrivacySettingsActivity(this, true) + }) + ) + } + + private fun openFileDisplayActivity() { + //update the version code when user has accepted or rejected privacy policy + //this will be used to help to check app up-gradation + preferences.lastSeenVersionCode = BuildConfig.VERSION_CODE + + val i = Intent(this, FileDisplayActivity::class.java) + i.action = FileDisplayActivity.RESTART + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(i) + finish() + } + + override fun onBackPressed() { + //user cannot close this screen without accepting or rejecting the privacy policy + } +} diff --git a/app/src/main/java/com/nmc/android/ui/PrivacySettingsActivity.kt b/app/src/main/java/com/nmc/android/ui/PrivacySettingsActivity.kt new file mode 100644 index 000000000000..e3572062d287 --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/PrivacySettingsActivity.kt @@ -0,0 +1,87 @@ +package com.nmc.android.ui + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import com.nextcloud.client.preferences.AppPreferences +import com.nmc.android.utils.CheckableThemeUtils +import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityPrivacySettingsBinding +import com.owncloud.android.ui.activity.ToolbarActivity +import javax.inject.Inject + +class PrivacySettingsActivity : ToolbarActivity() { + + companion object { + private const val EXTRA_SHOW_SETTINGS = "show_settings_button" + + @JvmStatic + fun openPrivacySettingsActivity(context: Context, isShowSettings: Boolean) { + val intent = Intent(context, PrivacySettingsActivity::class.java) + intent.putExtra(EXTRA_SHOW_SETTINGS, isShowSettings) + context.startActivity(intent) + } + } + + private lateinit var binding: ActivityPrivacySettingsBinding + + /** + * variable to check if save settings button needs to be shown or not + * currently we are showing only when user opens this activity from LoginPrivacySettingsActivity + */ + private var isShowSettingsButton = false + + @Inject + lateinit var preferences: AppPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPrivacySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + setupToolbar() + setupActionBar() + setUpViews() + showHideSettingsButton() + } + + private fun setupActionBar() { + supportActionBar?.let { + viewThemeUtils.platform.themeStatusBar(this) + it.setDisplayHomeAsUpEnabled(true) + it.setDisplayShowTitleEnabled(true) + //custom color for back arrow for NMC + viewThemeUtils.files.themeActionBar(this, it, resources.getString(R.string.privacy_settings)) + it.setBackgroundDrawable(ColorDrawable(resources.getColor(R.color.bg_default, null))) + } + } + + private fun showHideSettingsButton() { + isShowSettingsButton = intent.getBooleanExtra(EXTRA_SHOW_SETTINGS, false) + binding.privacySaveSettingsBtn.visibility = if (isShowSettingsButton) View.VISIBLE else View.GONE + } + + private fun setUpViews() { + CheckableThemeUtils.tintSwitch(binding.switchDataCollection) + CheckableThemeUtils.tintSwitch(binding.switchDataAnalysis) + binding.switchDataAnalysis.isChecked = preferences.isDataAnalysisEnabled + binding.switchDataAnalysis.setOnCheckedChangeListener { _, isChecked -> + preferences.setDataAnalysis(isChecked) + } + binding.privacySaveSettingsBtn.setOnClickListener { + //finish the activity as we are changing the setting on switch check change + finish() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + } + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterface.kt b/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterface.kt new file mode 100644 index 000000000000..860ffada08ab --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterface.kt @@ -0,0 +1,13 @@ +package com.nmc.android.ui + +import android.content.Context + +/** + * interface to open privacy settings activity from nmc/1921-settings branch + * for implementation look nmc/1878-privacy branch + * this class will have the declaration for it since it has the PrivacySettingsActivity.java in place + * since we don't have privacy settings functionality in this branch so to handle the redirection we have used interface + */ +interface PrivacySettingsInterface { + fun openPrivacySettingsActivity(context: Context) +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterfaceImpl.kt b/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterfaceImpl.kt new file mode 100644 index 000000000000..e087bc888e6f --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/PrivacySettingsInterfaceImpl.kt @@ -0,0 +1,14 @@ +package com.nmc.android.ui + +import android.content.Context + +/** + * interface impl to launch PrivacySettings Activity + * this class will have the implementation for it since it has the PrivacySettingsActivity in place + * calling of this method will be done from nmc/1921-settings + */ +class PrivacySettingsInterfaceImpl : PrivacySettingsInterface { + override fun openPrivacySettingsActivity(context: Context) { + PrivacySettingsActivity.openPrivacySettingsActivity(context, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/ui/PrivacyUserAction.kt b/app/src/main/java/com/nmc/android/ui/PrivacyUserAction.kt new file mode 100644 index 000000000000..6dca412cc29b --- /dev/null +++ b/app/src/main/java/com/nmc/android/ui/PrivacyUserAction.kt @@ -0,0 +1,9 @@ +package com.nmc.android.ui + +//class to handle user action for privacy +object PrivacyUserAction { + //privacy user action to maintain the state of privacy policy + const val NO_ACTION = 0 //user has taken no action + const val REJECT_ACTION = 1 //user rejected the privacy policy + const val ACCEPT_ACTION = 2 //user has accepted the privacy policy +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt new file mode 100644 index 000000000000..a3b8a1149948 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt @@ -0,0 +1,117 @@ +package com.nmc.android.utils + +import android.content.res.ColorStateList +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.res.ResourcesCompat +import com.owncloud.android.R + +object CheckableThemeUtils { + @JvmStatic + fun tintCheckbox(vararg checkBoxes: AppCompatCheckBox) { + for (checkBox in checkBoxes) { + val checkEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_enabled, + checkBox.context.theme + ) + val checkDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_disabled, + checkBox.context.theme + ) + val uncheckEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_enabled, + checkBox.context.theme + ) + val uncheckDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_disabled, + checkBox.context.theme + ) + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked) + ) + val colors = intArrayOf( + checkEnabled, + checkDisabled, + uncheckEnabled, + uncheckDisabled + ) + checkBox.buttonTintList = ColorStateList(states, colors) + } + } + + @JvmStatic + @JvmOverloads + fun tintSwitch(switchView: SwitchCompat, color: Int = 0, colorText: Boolean = false) { + if (colorText) { + switchView.setTextColor(color) + } + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled) + ) + + val thumbColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_checked_enabled, + switchView.context.theme + ) + val thumbColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_unchecked_enabled, + switchView.context.theme + ) + val thumbColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_disabled, + switchView.context.theme + ) + + val thumbColors = intArrayOf( + thumbColorCheckedEnabled, + thumbColorUncheckedEnabled, + thumbColorDisabled + ) + val thumbColorStateList = ColorStateList(states, thumbColors) + + val trackColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_checked_enabled, + switchView.context.theme + ) + val trackColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_unchecked_enabled, + switchView.context.theme + ) + val trackColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_disabled, + switchView.context.theme + ) + + val trackColors = intArrayOf( + trackColorCheckedEnabled, + trackColorUncheckedEnabled, + trackColorDisabled + ) + + val trackColorStateList = ColorStateList(states, trackColors) + + switchView.thumbTintList = thumbColorStateList + switchView.trackTintList = trackColorStateList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/Extensions.kt b/app/src/main/java/com/nmc/android/utils/Extensions.kt new file mode 100644 index 000000000000..035690e7af04 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/Extensions.kt @@ -0,0 +1,42 @@ +package com.nmc.android.utils + +import android.text.Selection +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.View +import android.widget.TextView + +fun TextView.makeLinks(vararg links: Pair) { + val spannableString = SpannableString(this.text) + var startIndexOfLink = -1 + for (link in links) { + val clickableSpan = object : ClickableSpan() { + override fun updateDrawState(textPaint: TextPaint) { + // use this to change the link color + textPaint.color = textPaint.linkColor + // toggle below value to enable/disable + // the underline shown below the clickable text + //textPaint.isUnderlineText = true + } + + override fun onClick(view: View) { + Selection.setSelection((view as TextView).text as Spannable, 0) + view.invalidate() + link.second.onClick(view) + } + } + startIndexOfLink = this.text.toString().indexOf(link.first, startIndexOfLink + 1) + spannableString.setSpan( + clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + this.movementMethod = + LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click + this.setText(spannableString, TextView.BufferType.SPANNABLE) +} + diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index 5e5f1f897acb..2d2fca3dcc42 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -84,6 +84,7 @@ import com.nextcloud.client.device.DeviceInfo; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.onboarding.FirstRunActivity; +import com.nmc.android.ui.LoginPrivacySettingsActivity; import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.java.util.Optional; @@ -120,7 +121,6 @@ import com.owncloud.android.services.OperationsService; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; import com.owncloud.android.ui.NextcloudWebViewClient; -import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.dialog.IndeterminateProgressDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; @@ -1222,10 +1222,9 @@ private void endSuccess() { if (onlyAdd) { finish(); } else { - Intent i = new Intent(this, FileDisplayActivity.class); - i.setAction(FileDisplayActivity.RESTART); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); + //login privacy settings to accept/reject by the user after login + LoginPrivacySettingsActivity.openPrivacySettingsActivity(this); + finish(); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 3017ff43b8b4..1d5446402437 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -57,11 +57,13 @@ import com.nextcloud.client.logger.ui.LogsActivity; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ConnectivityService; +import com.nmc.android.ui.PrivacySettingsInterface; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.preferences.DarkMode; import com.owncloud.android.BuildConfig; import com.owncloud.android.MainApp; +import com.nmc.android.ui.PrivacySettingsInterfaceImpl; import com.owncloud.android.R; import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -152,6 +154,16 @@ public class SettingsActivity extends PreferenceActivity @Inject ViewThemeUtils viewThemeUtils; @Inject ConnectivityService connectivityService; + /** + * Things to note about both the branches. + * 1. nmc/1921-settings branch: + * --> interface won't be initialised + * --> calling of interface method will be done here + * 2. nmc/1878-privacy + * --> interface will be initialised + * --> calling of interface method won't be done here + */ + private PrivacySettingsInterface privacySettingsInterface; @SuppressWarnings("deprecation") @Override @@ -164,6 +176,9 @@ public void onCreate(Bundle savedInstanceState) { setupActionBar(); + //NMC customization will be initialised in nmc/1878-privacy + privacySettingsInterface = new PrivacySettingsInterfaceImpl(); + // Register context menu for list of preferences. registerForContextMenu(getListView()); diff --git a/app/src/main/res/drawable/ic_privacy_settings.xml b/app/src/main/res/drawable/ic_privacy_settings.xml new file mode 100644 index 000000000000..c3bb3fb01899 --- /dev/null +++ b/app/src/main/res/drawable/ic_privacy_settings.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/layout/activity_login_privacy_settings.xml b/app/src/main/res/layout/activity_login_privacy_settings.xml new file mode 100644 index 000000000000..3a51e9457b3d --- /dev/null +++ b/app/src/main/res/layout/activity_login_privacy_settings.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_privacy_settings.xml b/app/src/main/res/layout/activity_privacy_settings.xml new file mode 100644 index 000000000000..33ef22c67720 --- /dev/null +++ b/app/src/main/res/layout/activity_privacy_settings.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 05928ea9fbe2..6d4247b203e2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -624,6 +624,27 @@ Dunkel Hell Systemvorgaben verwenden + + + Zur Optimierung unserer App erfassen wir anonymisierte Daten. Hierzu nutzen wir Software Lösungen verschiedener Partner. Wir möchten Ihnen volle Transparenz und Entscheidungsgewalt über die Verarbeitung und Erfassung Ihrer anonymisierten Nutzungsdaten geben. Ihre Einstellungen können Sie auch später jederzeit in den Einstellungen unter Datenschutz ändern. Bitte beachten Sie jedoch, dass die Datenerfassungen einen erheblichen Beitrag zur Optimierung dieser App leisten und Sie diese Optimierungen durch die Unterbindung der Datenübermittlung verhindern. + Erfoderliche Datenerfassung + Die Erfassung dieser Daten ist notwendig, um wesentliche Funktionen der App nutzen zu können. + Analyse-Datenerfassung zur bedarfsgerechten Gestaltung + Diese Daten helfen uns, die App Nutzung für Sie zu optimieren und Systemabstürze und Fehler schneller zu identifizieren. + Diese App verwendet Cookies und ähnliche Technologien (Tools). + Mit einem Klick auf Zustimmen akzeptieren Sie die Verarbeitung und auch die Weitergabe Ihrer Daten an + Drittanbieter. Die Daten werden für Analysen, Retargeting und zur Ausspielung von personalisierten Inhalten + und Werbung auf Seiten der Telekom, sowie auf Drittanbieterseiten genutzt. Weitere Informationen, auch zur + Datenverarbeitung durch Drittanbieter, finden Sie in den Einstellungen sowie in unseren %s. Sie können die + Verwendung der Tools %s oder jederzeit über ihre %s anpassen. + Datenschutz-Einstellungen + Akzeptieren + Einstellungen speichern + Datenschutzhinweise + ablehnen + Einstellungen + + Bildvorschau Keine lokale Datei für die Vorschau vorhanden Bild kann nicht angezeigt werden diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 0c23e15f1550..cb09bcace39c 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -49,4 +49,68 @@ #1E1E1E @android:color/white + + + #FFFFFF + @color/grey_30 + @color/grey_30 + #CCCCCC + @color/grey_70 + @color/grey_80 + #2D2D2D + @color/grey_70 + @color/grey_70 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_60 + @color/grey_0 + @color/grey_0 + @color/grey_30 + #FFFFFF + @color/grey_30 + @color/grey_80 + #FFFFFF + + + @color/grey_80 + @color/grey_30 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + @color/grey_80 + + + @color/grey_70 + @color/grey_60 + + + @color/grey_70 + @color/grey_70 + + + #FFFFFF + @color/grey_30 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_60 + @color/grey_0 + #FFFFFF + + + #121212 + @color/grey_0 + @color/grey_80 + @color/grey_80 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b18023e7d3df..b9de9c86b30e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -83,4 +83,93 @@ @android:color/white #666666 #A5A5A5 + + + #191919 + @color/primary + #191919 + #191919 + @color/grey_30 + @android:color/white + #FFFFFF + @color/grey_0 + #CCCCCC + #77c4ff + #B3FFFFFF + @color/grey_10 + + + #101010 + #F2F2F2 + #E5E5E5 + #B2B2B2 + #666666 + #4C4C4C + #333333 + + + @color/design_snackbar_background_color + @color/white + + + #FFFFFF + #191919 + + + @color/grey_0 + #191919 + @color/primary + #191919 + @color/primary + @color/grey_30 + @color/white + #191919 + + + #FFFFFF + #191919 + #191919 + + + #FFFFFF + #191919 + #FFFFFF + + + @color/primary + #F399C7 + #FFFFFF + @color/grey_30 + @color/grey_10 + @color/grey_0 + + + @color/primary + @color/grey_30 + @color/grey_30 + #CCCCCC + + + #191919 + @color/grey_30 + #191919 + #191919 + #191919 + #191919 + @color/grey_30 + #191919 + #000000 + #191919 + #F6E5EB + #C16F81 + #0D39DF + #0099ff + + + @color/grey_0 + #191919 + @color/grey_0 + @color/grey_30 + #77b6bb + #5077b6bb diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..cc9e25255a10 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + \ No newline at end of file diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml index 5eaf8e92d387..578e63d23ea7 100644 --- a/app/src/main/res/values/setup.xml +++ b/app/src/main/res/values/setup.xml @@ -78,7 +78,7 @@ true false true - https://github.com/nextcloud/android + https://static.magentacloud.de/licences/android.html true https://www.gnu.org/licenses/gpl-2.0.html diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fd30bc9360f..bac91622a308 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -208,6 +208,27 @@ Created %1$d fresh UID Created %1$d fresh UIDs + + To optimize your app, we collect anonymous data. For this we use + software solutions of different partners. We would like to give you full transparency and decision-making power over the processing and collection of your anonymized usage data. You can also change your settings at any time later in the app settings under data protection. Please note, however, that data collection makes a considerable contribution to the optimization of this app and you prevent this optimization by preventing data transmission. + Required data collection + The collection of this data is necessary to be able to use essential + functions of the app. + Analysis-data acquisition for the design + This data helps us to optimize the app usage for you and to identify system crashes and errors more quickly. + This app uses Cookies and similar technologies (tools). By + clicking Accept, you accept the processing and also the Transfer of your data to third parties. The data will + be used for Analysis, retargeting and to Display personalized Content and Advertising on sites and + third-party sites. You can find further information, including Information on data processing by third-party + Providers, in the Settings and in our %s. You can %s the use of the Tools or customize them at any time in the + %s. + Privacy Settings + Accept + Save Settings + Privacy Policy + reject + Settings + Processed %d entry. Processed %d entries.