Sensitive media (#162)

* utility functions to censor and decensor a post image

* added Text warning about sensitive content

* adapt layout based on Sensitive attribute, censor and decensor

* try to perform clicks on sensitive image

* small refactor of status for sensitive layout

* testing censor Matrices functions

* perform test on sensitive post

* modified so second post is sensitive

* hide sensitiveWarning from albums for now

* hide totaly the image

* perform visibility check on sensitive warning textView

* deleted tests using activityScenario.onActivity as they return true on assert(false)

* commented dummy test for matrix censoring

* implemented sensitive layout for multiple pictures posts

* remove diplay check before click

* now testing visibility of textView

* deleted faulty lines i hope

* bring back dummy check for matrices

* everything is now sensitive, testing on tab post

* implemented matcher for second item

* implemented tests for tabs and classic sensitive layout using custom matcher Second

* cleaning in JSON values, put sensitive true on posts

* hide sensitive posts behind red triangle

* centered background triangle

* corrected indentation

* extracted sensitive string in string.xml
This commit is contained in:
Samuel Dietz 2020-05-16 23:47:18 +02:00 committed by GitHub
parent 252a192ff3
commit 5fadfd2e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 34 deletions

View File

@ -1,12 +1,21 @@
package com.h.pixeldroid
import android.graphics.ColorMatrix
import android.content.Context
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.TextView
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -16,9 +25,18 @@ import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.second
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.slowSwipeUp
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.typeTextInViewWithId
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.PostUtils.Companion.censorColorMatrix
import com.h.pixeldroid.utils.PostUtils.Companion.uncensorColorMatrix
import kotlinx.android.synthetic.main.fragment_feed.*
import kotlinx.android.synthetic.main.fragment_feed.view.*
import kotlinx.android.synthetic.main.post_fragment.*
import kotlinx.android.synthetic.main.post_fragment.view.*
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -283,7 +301,9 @@ class MockedServerTest {
a -> run {
//Wait for the feed to load
Thread.sleep(1000)
//Pick the second photo
a.findViewById<TextView>(R.id.sensitiveWarning).performClick()
Thread.sleep(1000)
//Pick the second photo
a.findViewById<TabLayout>(R.id.postTabs).getTabAt(1)?.select()
}
}
@ -501,5 +521,86 @@ class MockedServerTest {
onView(first(withId(R.id.commentContainer)))
.check(matches(hasDescendant(withId(R.id.comment))))
}
@Test
fun censorMatrices() {
// Doing these dummy checks as I can not get the matrix property from the ImageView
val array: FloatArray = floatArrayOf(
0.1f, 0f, 0f, 0f, 0f, // red vector
0f, 0.1f, 0f, 0f, 0f, // green vector
0f, 0f, 0.1f, 0f, 0f, // blue vector
0f, 0f, 0f, 1f, 0f ) // alpha vector
assert(censorColorMatrix().equals(array))
assert(uncensorColorMatrix().equals(ColorMatrix()))
}
@Test
fun performClickOnSensitiveWarning() {
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(1))
Thread.sleep(1000)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(1, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(1000)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
@Test
fun performClickOnPostPicture() {
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(1))
Thread.sleep(1000)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(1, clickChildViewWithId(R.id.postPicture)))
Thread.sleep(1000)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
@Test
fun performClickOnSensitiveWarningTabs() {
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(0))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
@Test
fun performClickOnPostPictureTabs() {
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(0))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.postPicture)))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
}

View File

@ -31,6 +31,25 @@ abstract class CustomMatchers {
}
}
fun <T> second(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("second matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return false
} else if (!isFirst && matcher.matches(item))
return true
return false
}
}
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way up

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,6 @@ class MockServer {
private val headerValue = "application/json; charset=utf-8"
}
fun start() {
try {
server.dispatcher = getDispatcher()

View File

@ -32,7 +32,12 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostsRecycler
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = posts[position]
setSquareImageFromURL(holder.postView, post.getPostPreviewURL(), holder.postPreview)
if (post.sensitive)
setSquareImageFromURL(holder.postView, null, holder.postPreview)
else
setSquareImageFromURL(holder.postView, post.getPostPreviewURL(), holder.postPreview)
holder.postPreview.setOnClickListener {
val intent = Intent(holder.postPreview.context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)

View File

@ -2,6 +2,7 @@ package com.h.pixeldroid.objects
import android.Manifest
import android.content.Context
import android.graphics.ColorMatrixColorFilter
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
@ -30,17 +31,20 @@ import com.h.pixeldroid.utils.HtmlUtils.Companion.getDomain
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.ImageUtils.Companion.downloadImage
import com.h.pixeldroid.utils.PostUtils.Companion.censorColorMatrix
import com.h.pixeldroid.utils.PostUtils.Companion.likePostCall
import com.h.pixeldroid.utils.PostUtils.Companion.postComment
import com.h.pixeldroid.utils.PostUtils.Companion.reblogPost
import com.h.pixeldroid.utils.PostUtils.Companion.retrieveComments
import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput
import com.h.pixeldroid.utils.PostUtils.Companion.unLikePostCall
import com.h.pixeldroid.utils.PostUtils.Companion.uncensorColorMatrix
import com.h.pixeldroid.utils.PostUtils.Companion.undoReblogPost
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.android.synthetic.main.post_fragment.view.*
import kotlinx.android.synthetic.main.post_fragment.view.postDate
import kotlinx.android.synthetic.main.post_fragment.view.postDomain
import java.io.Serializable
@ -48,10 +52,6 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.collections.ArrayList
import kotlinx.android.synthetic.main.post_fragment.view.postPager
import kotlinx.android.synthetic.main.post_fragment.view.postPicture
import kotlinx.android.synthetic.main.post_fragment.view.postTabs
import kotlinx.android.synthetic.main.post_fragment.view.profilePic
/*
Represents a status posted by an account.
@ -92,7 +92,7 @@ data class Status(
val muted: Boolean = false,
val bookmarked: Boolean = false,
val pinned: Boolean = false
) : Serializable, FeedContent()
) : Serializable, FeedContent()
{
companion object {
@ -149,7 +149,7 @@ data class Status(
android.text.format.DateUtils.SECOND_IN_MILLIS,
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE)
textView.text = if(isActivity) "Posted on $date"
else "$formattedDate"
else "$formattedDate"
} catch (e: ParseException) {
e.printStackTrace()
}
@ -163,28 +163,47 @@ data class Status(
}
private fun setupPostPics(rootView: View, request: RequestBuilder<Drawable>, homeFragment: Fragment) {
//Check whether or not we need to activate the viewPager
if(media_attachments?.size == 1) {
rootView.postPicture.visibility = VISIBLE
rootView.postPager.visibility = GONE
rootView.postTabs.visibility = GONE
// Standard layout
rootView.postPicture.visibility = VISIBLE
rootView.postPager.visibility = GONE
rootView.postTabs.visibility = GONE
if (sensitive) {
setupSensitiveLayout(rootView, request, homeFragment)
request.load(this.getPostUrl()).into(rootView.postPicture)
} else if(media_attachments?.size!! > 1) {
//Only show the viewPager and tabs
rootView.postPicture.visibility = GONE
rootView.postPager.visibility = VISIBLE
rootView.postTabs.visibility = VISIBLE
val tabs : ArrayList<ImageFragment> = ArrayList()
} else {
rootView.sensitiveWarning.visibility = GONE
//Fill the tabs with each mediaAttachment
for(media in media_attachments) {
tabs.add(ImageFragment.newInstance(media.url))
if(media_attachments?.size == 1) {
request.load(this.getPostUrl()).into(rootView.postPicture)
} else if(media_attachments?.size!! > 1) {
setupTabsLayout(rootView, request, homeFragment)
}
setupTabs(tabs, rootView, homeFragment)
imagePopUpMenu(rootView, homeFragment.requireActivity())
}
}
private fun setupTabsLayout(rootView: View, request: RequestBuilder<Drawable>, homeFragment: Fragment) {
//Only show the viewPager and tabs
rootView.postPicture.visibility = GONE
rootView.postPager.visibility = VISIBLE
rootView.postTabs.visibility = VISIBLE
val tabs : ArrayList<ImageFragment> = ArrayList()
//Fill the tabs with each mediaAttachment
for(media in media_attachments!!) {
tabs.add(ImageFragment.newInstance(media.url))
}
setupTabs(tabs, rootView, homeFragment)
}
private fun setupTabs(tabs: ArrayList<ImageFragment>, rootView: View, homeFragment: Fragment) {
//Attach the given tabs to the view pager
rootView.postPager.adapter = object : FragmentStateAdapter(homeFragment) {
@ -196,6 +215,7 @@ data class Status(
return media_attachments?.size ?: 0
}
}
TabLayoutMediator(rootView.postTabs, rootView.postPager) { tab, _ ->
tab.icon = rootView.context.getDrawable(R.drawable.ic_dot_blue_12dp)
}.attach()
@ -250,9 +270,7 @@ data class Status(
//Set comment initial visibility
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = GONE
imagePopUpMenu(rootView, homeFragment.requireActivity())
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE
}
fun setDescription(rootView: View, api : PixelfedAPI, credential: String) {
@ -411,4 +429,31 @@ data class Status(
}
}
}
private fun setupSensitiveLayout(view: View, request: RequestBuilder<Drawable>, homeFragment: Fragment) {
// Set dark layout and warning message
view.sensitiveWarning.visibility = VISIBLE
view.postPicture.colorFilter = ColorMatrixColorFilter(censorColorMatrix())
fun uncensorPicture(view: View) {
if (!media_attachments.isNullOrEmpty()) {
view.sensitiveWarning.visibility = GONE
view.postPicture.colorFilter = ColorMatrixColorFilter(uncensorColorMatrix())
if (media_attachments.size > 1)
setupTabsLayout(view, request, homeFragment)
}
imagePopUpMenu(view, homeFragment.requireActivity())
}
view.findViewById<TextView>(R.id.sensitiveWarning).setOnClickListener {
uncensorPicture(view)
}
view.findViewById<ImageView>(R.id.postPicture).setOnClickListener {
uncensorPicture(view)
}
}
}

View File

@ -1,12 +1,11 @@
package com.h.pixeldroid.utils
import android.content.SharedPreferences
import android.graphics.ColorMatrix
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.PostViewHolder
@ -226,5 +225,14 @@ abstract class PostUtils {
}
})
}
fun censorColorMatrix(): ColorMatrix {
val array: FloatArray = floatArrayOf( 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f )
return ColorMatrix(array)
}
fun uncensorColorMatrix(): ColorMatrix {
return ColorMatrix()
}
}
}

View File

@ -14,6 +14,10 @@
android:id="@+id/postPreview"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:adjustViewBounds="false"
android:background="@android:drawable/stat_sys_warning"
android:backgroundTint="#780000"
android:contentDescription="TODO" />
</androidx.cardview.widget.CardView>

View File

@ -68,8 +68,7 @@
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.viewpager2.widget.ViewPager2>
app:layout_constraintTop_toTopOf="parent"></androidx.viewpager2.widget.ViewPager2>
<com.google.android.material.tabs.TabLayout
android:id="@+id/postTabs"
@ -78,8 +77,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/postPager"
app:tabMode="auto">
</com.google.android.material.tabs.TabLayout>
app:tabMode="auto"></com.google.android.material.tabs.TabLayout>
<ImageView
android:id="@+id/postPicture"
@ -104,8 +102,23 @@
app:layout_constraintTop_toTopOf="@+id/postPicture"
app:layout_constraintVertical_bias="0.1" />
<TextView
android:id="@+id/sensitiveWarning"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:gravity="center|center_horizontal|center_vertical"
android:longClickable="true"
android:text="@string/cw_nsfw_hidden_media_n_click_to_show"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/ic_launcher_background"
app:layout_constraintBottom_toBottomOf="@+id/postPicture"
app:layout_constraintTop_toBottomOf="@+id/postTabs"
tools:src="@color/browser_actions_bg_grey" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/commenter"
android:layout_width="30dp"

View File

@ -55,5 +55,6 @@
<string name="enter">Enter</string>
<string name="auth_error_toast_msg">Server has responded with an error, try again!</string>
<string name="login_empty_string_error">Instance address cannot be empty!</string>
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Hidden Media \n (click to show)</string>
</resources>