change: Display "About" information in tabs (#420)

Previously, `AboutActivity` had buttons and links to show the privacy
policy and licenses of dependencies.

Change this to a selection of fragments in tabs, one tab each for:

- General "About" information
- Licenses
- Privacy Policy

The information shown hasn't changed, but this lays the groundwork for
including additional tabs in the future for information like server
rules, detected capabilities, or troubleshooting information.
This commit is contained in:
Nik Clayton 2024-02-06 00:43:26 +01:00 committed by GitHub
parent 7c5181d5c2
commit 41c702fc1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 524 additions and 452 deletions

View File

@ -144,8 +144,6 @@
android:resource="@xml/searchable" />
</activity>
<activity android:name=".ListsActivity" />
<activity android:name=".feature.about.LicenseActivity" />
<activity android:name=".feature.about.PrivacyPolicyActivity" />
<activity android:name=".components.filters.FiltersActivity" />
<activity android:name=".components.trending.TrendingActivity" />
<activity android:name=".components.followedtags.FollowedTagsActivity" />

View File

@ -97,6 +97,7 @@ import app.pachli.core.navigation.TrendingActivityIntent
import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Notification
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.ui.reduceSwipeSensitivity
import app.pachli.databinding.ActivityMainBinding
import app.pachli.db.DraftsAlert
import app.pachli.interfaces.ActionButtonActivity
@ -110,7 +111,6 @@ import app.pachli.usecase.LogoutUsecase
import app.pachli.util.await
import app.pachli.util.deleteStaleCachedMedia
import app.pachli.util.getDimension
import app.pachli.util.reduceSwipeSensitivity
import app.pachli.util.unsafeLazy
import app.pachli.util.updateShortcut
import at.connyduck.calladapter.networkresult.fold

View File

@ -77,6 +77,7 @@ import app.pachli.core.network.model.Relationship
import app.pachli.core.network.parseAsMastodonHtml
import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.ui.reduceSwipeSensitivity
import app.pachli.databinding.ActivityAccountBinding
import app.pachli.db.DraftsAlert
import app.pachli.interfaces.ActionButtonActivity
@ -86,7 +87,6 @@ import app.pachli.util.Error
import app.pachli.util.Loading
import app.pachli.util.Success
import app.pachli.util.getDomain
import app.pachli.util.reduceSwipeSensitivity
import app.pachli.util.setClickableText
import app.pachli.view.showMuteAccountDialog
import com.bumptech.glide.Glide

View File

@ -31,8 +31,8 @@ import app.pachli.components.search.adapter.SearchPagerAdapter
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.ui.reduceSwipeSensitivity
import app.pachli.databinding.ActivitySearchBinding
import app.pachli.util.reduceSwipeSensitivity
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint

View File

@ -32,9 +32,9 @@ import app.pachli.components.timeline.TimelineFragment
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.ui.reduceSwipeSensitivity
import app.pachli.databinding.ActivityTrendingBinding
import app.pachli.interfaces.AppBarLayoutHost
import app.pachli.util.reduceSwipeSensitivity
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint

View File

@ -17,43 +17,6 @@
package app.pachli.util
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import timber.log.Timber
/**
* Reduce ViewPager2's sensitivity to horizontal swipes.
*/
fun ViewPager2.reduceSwipeSensitivity() {
// ViewPager2 is very sensitive to horizontal motion when swiping vertically, and will
// trigger a page transition if the user's swipe is only a few tens of degrees off from
// vertical. This is a problem if the underlying content is a list that the user wants
// to scroll vertically -- it's far too easy to trigger an accidental horizontal swipe.
//
// One way to stop this is to reach in to ViewPager2's RecyclerView and adjust the amount
// of touch slop it has.
//
// See https://issuetracker.google.com/issues/139867645 and
// https://bladecoder.medium.com/fixing-recyclerview-nested-scrolling-in-opposite-direction-f587be5c1a04
// for more (the approach in that Medium article works, but is still quite sensitive to
// horizontal movement while scrolling).
try {
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(this) as RecyclerView
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
touchSlopField.isAccessible = true
val touchSlop = touchSlopField.get(recyclerView) as Int
// Experimentally, 2 seems to be a sweet-spot, requiring a downward swipe that's at least
// 45 degrees off the vertical to trigger a change. This is consistent with maximum angle
// supported to open the nav. drawer.
val scaleFactor = 2
touchSlopField.set(recyclerView, touchSlop * scaleFactor)
} catch (e: Exception) {
Timber.tag("reduceSwipeSensitibity").w(e)
}
}
/**
* TextViews with an ancestor RecyclerView can forget that they are selectable. Toggling

View File

@ -549,12 +549,6 @@ class InstanceListActivityIntent(context: Context) : Intent() {
}
}
class LicenseActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.LICENSE_ACTIVITY)
}
}
class ListActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.LISTS_ACTIVITY)
@ -567,12 +561,6 @@ class LoginWebViewActivityIntent(context: Context) : Intent() {
}
}
class PrivacyPolicyActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.PRIVACY_POLICY_ACTIVITY)
}
}
class ScheduledStatusActivityIntent(context: Context) : Intent() {
init {
setClassName(context, QuadrantConstants.SCHEDULED_STATUS_ACTIVITY)

View File

@ -0,0 +1,56 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.ui
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import timber.log.Timber
/**
* Reduce ViewPager2's sensitivity to horizontal swipes.
*/
fun ViewPager2.reduceSwipeSensitivity() {
// ViewPager2 is very sensitive to horizontal motion when swiping vertically, and will
// trigger a page transition if the user's swipe is only a few tens of degrees off from
// vertical. This is a problem if the underlying content is a list that the user wants
// to scroll vertically -- it's far too easy to trigger an accidental horizontal swipe.
//
// One way to stop this is to reach in to ViewPager2's RecyclerView and adjust the amount
// of touch slop it has.
//
// See https://issuetracker.google.com/issues/139867645 and
// https://bladecoder.medium.com/fixing-recyclerview-nested-scrolling-in-opposite-direction-f587be5c1a04
// for more (the approach in that Medium article works, but is still quite sensitive to
// horizontal movement while scrolling).
try {
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(this) as RecyclerView
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
touchSlopField.isAccessible = true
val touchSlop = touchSlopField.get(recyclerView) as Int
// Experimentally, 2 seems to be a sweet-spot, requiring a downward swipe that's at least
// 45 degrees off the vertical to trigger a change. This is consistent with maximum angle
// supported to open the nav. drawer.
val scaleFactor = 2
touchSlopField.set(recyclerView, touchSlop * scaleFactor)
} catch (e: Exception) {
Timber.tag("reduceSwipeSensitibity").w(e)
}
}

View File

@ -47,6 +47,7 @@ dependencies {
implementation(projects.core.activity)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.designsystem)
implementation(projects.core.navigation)
implementation(projects.core.ui)
@ -57,4 +58,7 @@ dependencies {
implementation(libs.bundles.androidx)
implementation(libs.bundles.aboutlibraries)
// For FixedSizeDrawable
implementation(libs.glide.core)
}

View File

@ -17,137 +17,83 @@
package app.pachli.feature.about
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.common.util.versionName
import app.pachli.core.data.repository.InstanceInfoRepository
import app.pachli.core.navigation.LicenseActivityIntent
import app.pachli.core.navigation.PrivacyPolicyActivityIntent
import app.pachli.core.ui.NoUnderlineURLSpan
import app.pachli.core.designsystem.R as DR
import app.pachli.core.ui.reduceSwipeSensitivity
import app.pachli.feature.about.databinding.ActivityAboutBinding
import com.bumptech.glide.request.target.FixedSizeDrawable
import com.google.android.material.tabs.TabLayoutMediator
import com.mikepenz.aboutlibraries.LibsBuilder
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
@AndroidEntryPoint
class AboutActivity : BottomSheetActivity() {
@Inject
lateinit var instanceInfoRepository: InstanceInfoRepository
class AboutActivity : BottomSheetActivity(), MenuProvider {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
setSupportActionBar(binding.toolbar)
binding.toolbar.run {
val navIconSize = resources.getDimensionPixelSize(DR.dimen.avatar_toolbar_nav_icon_size)
navigationIcon = FixedSizeDrawable(
AppCompatResources.getDrawable(this@AboutActivity, DR.mipmap.ic_launcher),
navIconSize,
navIconSize,
)
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.app_name)
setDisplayShowHomeEnabled(true)
}
setTitle(R.string.about_title_activity)
val adapter = AboutFragmentAdapter(this)
binding.pager.adapter = adapter
binding.pager.reduceSwipeSensitivity()
binding.versionTextView.text = getString(
R.string.about_app_version,
getString(
R.string.app_name,
),
versionName(this),
TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
tab.text = adapter.title(position)
}.attach()
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
@SuppressLint("SyntheticAccessor")
override fun handleOnBackPressed() {
if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish()
}
},
)
binding.deviceInfo.text = getString(
R.string.about_device_info,
Build.MANUFACTURER,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
)
lifecycleScope.launch {
accountManager.activeAccount?.let { account ->
val instanceInfo = instanceInfoRepository.getInstanceInfo()
binding.accountInfo.text = getString(
R.string.about_account_info,
account.username,
account.domain,
instanceInfo.version,
)
binding.accountInfoTitle.show()
binding.accountInfo.show()
}
}
if (BuildConfig.CUSTOM_INSTANCE.isBlank()) {
binding.aboutPoweredBy.hide()
}
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_pachli_license)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
binding.aboutPrivacyPolicyTextView.setOnClickListener {
startActivity(PrivacyPolicyActivityIntent(this))
}
binding.appProfileButton.setOnClickListener {
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
}
binding.aboutLicensesButton.setOnClickListener {
startActivityWithSlideInAnimation(LicenseActivityIntent(this))
}
binding.copyDeviceInfo.setOnClickListener {
val text = "${binding.versionTextView.text}\n\nDevice:\n\n${binding.deviceInfo.text}\n\nAccount:\n\n${binding.accountInfo.text}"
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Pachli version information", text)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
this,
getString(R.string.about_copied),
Toast.LENGTH_SHORT,
).show()
}
}
}
}
internal fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
val text = SpannableString(context.getText(textId))
class AboutFragmentAdapter(val activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 3
Linkify.addLinks(text, Linkify.WEB_URLS)
val builder = SpannableStringBuilder(text)
val urlSpans = text.getSpans(0, text.length, URLSpan::class.java)
for (span in urlSpans) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
val flags = builder.getSpanFlags(span)
val customSpan = NoUnderlineURLSpan(span.url)
builder.removeSpan(span)
builder.setSpan(customSpan, start, end, flags)
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> AboutFragment.newInstance()
1 -> LibsBuilder().supportFragment()
2 -> PrivacyPolicyFragment.newInstance()
else -> throw IllegalStateException()
}
}
setText(builder)
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
fun title(position: Int): CharSequence {
return when (position) {
0 -> "About"
1 -> activity.getString(R.string.title_licenses)
2 -> activity.getString(R.string.about_privacy_policy)
else -> throw IllegalStateException()
}
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.feature.about
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Build
import android.os.Bundle
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.util.versionName
import app.pachli.core.ui.NoUnderlineURLSpan
import app.pachli.feature.about.databinding.FragmentAboutBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class AboutFragment : Fragment(R.layout.fragment_about) {
private val viewModel: AboutFragmentViewModel by viewModels()
private val binding by viewBinding(FragmentAboutBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val version = getString(
R.string.about_app_version,
getString(
R.string.app_name,
),
versionName(requireContext()),
)
binding.versionTextView.text = version
val deviceInfo = getString(
R.string.about_device_info,
Build.MANUFACTURER,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
)
binding.deviceInfo.text = deviceInfo
lifecycleScope.launch {
viewModel.accountInfo.collect {
binding.accountInfo.text = it
binding.accountInfoTitle.show()
binding.accountInfo.show()
binding.copyDeviceInfo.show()
}
}
if (BuildConfig.CUSTOM_INSTANCE.isBlank()) {
binding.aboutPoweredBy.hide()
}
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_pachli_license)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
binding.appProfileButton.setOnClickListener {
(activity as? BottomSheetActivity)?.viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
}
binding.copyDeviceInfo.setOnClickListener {
val text = "$version\n\nDevice:\n\n$deviceInfo\n\nAccount:\n\n${binding.accountInfo.text}"
val clipboard = getSystemService(requireContext(), ClipboardManager::class.java) as ClipboardManager
val clip = ClipData.newPlainText("Pachli version information", text)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
requireContext(),
getString(R.string.about_copied),
Toast.LENGTH_SHORT,
).show()
}
}
}
companion object {
fun newInstance() = AboutFragment()
}
}
internal fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
val text = SpannableString(context.getText(textId))
Linkify.addLinks(text, Linkify.WEB_URLS)
val builder = SpannableStringBuilder(text)
val urlSpans = text.getSpans(0, text.length, URLSpan::class.java)
for (span in urlSpans) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
val flags = builder.getSpanFlags(span)
val customSpan = NoUnderlineURLSpan(span.url)
builder.removeSpan(span)
builder.setSpan(customSpan, start, end, flags)
}
setText(builder)
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.feature.about
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.pachli.core.accounts.AccountManager
import app.pachli.core.data.repository.InstanceInfoRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
@HiltViewModel
class AboutFragmentViewModel @Inject constructor(
private val application: Application,
private val accountManager: AccountManager,
private val instanceInfoRepository: InstanceInfoRepository,
) : AndroidViewModel(application) {
private val _accountInfo = MutableSharedFlow<String>()
val accountInfo: Flow<String> = _accountInfo.asSharedFlow()
init {
viewModelScope.launch {
accountManager.activeAccount?.let { account ->
val instanceInfo = instanceInfoRepository.getInstanceInfo()
_accountInfo.emit(
application.getString(
R.string.about_account_info,
account.username,
account.domain,
instanceInfo.version,
),
)
}
}
}
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.feature.about
import android.os.Bundle
import androidx.fragment.app.commit
import app.pachli.core.activity.BaseActivity
import app.pachli.feature.about.databinding.ActivityLicenseBinding
import com.mikepenz.aboutlibraries.LibsBuilder
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class LicenseActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityLicenseBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
setTitle(R.string.title_licenses)
val fragment = LibsBuilder().supportFragment()
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.fragment_licenses, fragment)
}
}
}
}

View File

@ -19,17 +19,23 @@ package app.pachli.feature.about
import android.os.Bundle
import android.util.Base64
import app.pachli.core.activity.BaseActivity
import app.pachli.feature.about.databinding.ActivityPrivacyPolicyBinding
import android.view.View
import androidx.fragment.app.Fragment
import app.pachli.core.common.extensions.viewBinding
import app.pachli.feature.about.databinding.FragmentPrivacyPolicyBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PrivacyPolicyActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityPrivacyPolicyBinding.inflate(layoutInflater)
setContentView(binding.root)
class PrivacyPolicyFragment : Fragment(R.layout.fragment_privacy_policy) {
private val binding by viewBinding(FragmentPrivacyPolicyBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val encoded = Base64.encodeToString(markdownR.html.PRIVACY_md.toByteArray(), Base64.NO_PADDING)
binding.policy.loadData(encoded, "text/html", "base64")
}
companion object {
fun newInstance() = PrivacyPolicyFragment()
}
}

View File

@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -6,215 +23,32 @@
android:layout_height="match_parent"
tools:context="app.pachli.feature.about.AboutActivity">
<include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<FrameLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation"
app:elevationOverlayEnabled="false">
<androidx.core.widget.NestedScrollView
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textDirection="anyRtl">
app:tabGravity="fill"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<ImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/versionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="?colorOnSurface"
android:textIsSelectable="true"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo"
tools:text="Pachli Test" />
<TextView
android:id="@+id/deviceInfoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginStart="@dimen/text_content_margin"
android:layout_marginEnd="@dimen/text_content_margin"
android:lineSpacingMultiplier="1.1"
android:text="@string/about_device_info_title"
android:textIsSelectable="true"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/versionTextView"
tools:text="Your device" />
<TextView
android:id="@+id/deviceInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingMultiplier="1.1"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/deviceInfoTitle"
app:layout_constraintStart_toStartOf="@+id/deviceInfoTitle"
app:layout_constraintTop_toBottomOf="@id/deviceInfoTitle" />
<TextView
android:id="@+id/accountInfoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingMultiplier="1.1"
android:text="@string/about_account_info_title"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/deviceInfo"
app:layout_constraintTop_toBottomOf="@id/deviceInfo"
tools:visibility="visible" />
<TextView
android:id="@+id/accountInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingMultiplier="1.1"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/accountInfoTitle"
app:layout_constraintStart_toStartOf="@+id/accountInfoTitle"
app:layout_constraintTop_toBottomOf="@id/accountInfoTitle"
tools:text="\@Pachli@mastodon.social\nVersion: xxx"
tools:visibility="visible" />
<ImageButton
android:id="@+id/copyDeviceInfo"
style="@style/AppImageButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/about_copy"
android:layout_marginEnd="@dimen/text_content_margin"
app:layout_constraintBottom_toBottomOf="@+id/accountInfo"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_content_copy_24" />
<TextView
android:id="@+id/about_powered_by"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/about_powered_by_pachli"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/deviceInfo"
app:layout_constraintTop_toBottomOf="@+id/accountInfo" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutLicenseInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/accountInfo"
app:layout_constraintTop_toBottomOf="@id/about_powered_by"
tools:text="@string/about_pachli_license" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutWebsiteInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/aboutLicenseInfoTextView"
app:layout_constraintStart_toStartOf="@+id/aboutLicenseInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutLicenseInfoTextView"
tools:text="@string/about_project_site" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutBugsFeaturesInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/about_bug_feature_request_site"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintStart_toStartOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutWebsiteInfoTextView" />
<TextView
android:id="@+id/aboutPrivacyPolicyTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/about_privacy_policy"
android:textColor="?android:attr/textColorLink"
app:layout_constraintEnd_toEndOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintStart_toStartOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutBugsFeaturesInfoTextView" />
<Button
android:id="@+id/appProfileButton"
style="@style/AppButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:maxWidth="320dp"
android:text="@string/about_pachli_account"
android:textAllCaps="false"
android:textSize="16sp"
android:layout_marginEnd="@dimen/text_content_margin"
app:layout_constraintEnd_toStartOf="@+id/aboutLicensesButton"
app:layout_constraintStart_toStartOf="@+id/aboutBugsFeaturesInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutPrivacyPolicyTextView" />
<Button
android:id="@+id/aboutLicensesButton"
style="@style/AppButton.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
android:maxWidth="320dp"
android:text="@string/title_licenses"
android:textAlignment="center"
android:textAllCaps="false"
android:textSize="16sp"
android:layout_marginEnd="@dimen/text_content_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/appProfileButton"
app:layout_constraintTop_toTopOf="@+id/appProfileButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<include layout="@layout/item_status_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="app.pachli.feature.about.LicenseActivity">
<include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center_vertical"
android:lineSpacingMultiplier="1.1"
android:text="@string/license_description" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textDirection="anyRtl"
tools:context="app.pachli.feature.about.AboutActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
android:orientation="vertical">
<TextView
android:id="@+id/versionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Pachli Test" />
<TextView
android:id="@+id/deviceInfoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="@dimen/text_content_margin"
android:layout_marginEnd="@dimen/text_content_margin"
android:lineSpacingMultiplier="1.1"
android:text="@string/about_device_info_title"
android:textIsSelectable="true"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/versionTextView"
tools:text="Your device" />
<TextView
android:id="@+id/deviceInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingMultiplier="1.1"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/deviceInfoTitle"
app:layout_constraintStart_toStartOf="@+id/deviceInfoTitle"
app:layout_constraintTop_toBottomOf="@id/deviceInfoTitle" />
<TextView
android:id="@+id/accountInfoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingMultiplier="1.1"
android:text="@string/about_account_info_title"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/deviceInfo"
app:layout_constraintTop_toBottomOf="@id/deviceInfo"
tools:visibility="visible" />
<TextView
android:id="@+id/accountInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingMultiplier="1.1"
android:textIsSelectable="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/accountInfoTitle"
app:layout_constraintStart_toStartOf="@+id/accountInfoTitle"
app:layout_constraintTop_toBottomOf="@id/accountInfoTitle"
tools:text="\@Pachli@mastodon.social\nVersion: xxx"
tools:visibility="visible" />
<ImageButton
android:id="@+id/copyDeviceInfo"
style="@style/AppImageButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/about_copy"
android:layout_marginEnd="@dimen/text_content_margin"
app:layout_constraintBottom_toBottomOf="@+id/accountInfo"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_content_copy_24" />
<TextView
android:id="@+id/about_powered_by"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/about_powered_by_pachli"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/deviceInfo"
app:layout_constraintTop_toBottomOf="@+id/accountInfo" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutLicenseInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo"
app:layout_constraintStart_toStartOf="@+id/accountInfo"
app:layout_constraintTop_toBottomOf="@id/about_powered_by"
tools:text="@string/about_pachli_license" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutWebsiteInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/aboutLicenseInfoTextView"
app:layout_constraintStart_toStartOf="@+id/aboutLicenseInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutLicenseInfoTextView"
tools:text="@string/about_project_site" />
<app.pachli.core.ui.ClickableSpanTextView
android:id="@+id/aboutBugsFeaturesInfoTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/about_bug_feature_request_site"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintStart_toStartOf="@+id/aboutWebsiteInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutWebsiteInfoTextView" />
<Button
android:id="@+id/appProfileButton"
style="@style/AppButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:lineSpacingMultiplier="1.2"
android:maxWidth="320dp"
android:text="@string/about_pachli_account"
android:textAllCaps="false"
android:textSize="16sp"
android:layout_marginEnd="@dimen/text_content_margin"
app:layout_constraintStart_toStartOf="@+id/aboutBugsFeaturesInfoTextView"
app:layout_constraintTop_toBottomOf="@id/aboutBugsFeaturesInfoTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -18,4 +18,6 @@
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/policy"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:layout_marginStart="@dimen/text_content_margin"
android:layout_marginEnd="@dimen/text_content_margin" />