diff --git a/app/build.gradle b/app/build.gradle index 16bcc201..886a7eca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,7 +105,6 @@ dependencies { def fragment_version = '1.2.4' debugImplementation "androidx.fragment:fragment-testing:$fragment_version" - } tasks.withType(Test) { diff --git a/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt b/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt new file mode 100644 index 00000000..573a34ba --- /dev/null +++ b/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt @@ -0,0 +1,103 @@ +package com.h.pixeldroid + +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.longClick +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.h.pixeldroid.objects.Account +import com.h.pixeldroid.objects.Attachment +import com.h.pixeldroid.objects.Status +import com.h.pixeldroid.testUtility.MockServer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.Timeout +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class PostTest { + + private lateinit var context: Context + + @get:Rule + var globalTimeout: Timeout = Timeout.seconds(100) + + @Before + fun before(){ + context = InstrumentationRegistry.getInstrumentation().targetContext + val mockServer = MockServer() + mockServer.start() + val baseUrl = mockServer.getUrl() + val preferences = context.getSharedPreferences( + "com.h.pixeldroid.pref", + Context.MODE_PRIVATE) + preferences.edit().putString("accessToken", "azerty").apply() + preferences.edit().putString("domain", baseUrl.toString()).apply() + } + + @Test + fun saveToGalleryTestSimplePost() { + val attachment = Attachment( + id = "12", + url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + ) + val post = Status( + id = "12", + account = Account( + id = "12", + username = "douze" + ), + media_attachments = listOf(attachment) + ) + val intent = Intent(context, PostActivity::class.java) + intent.putExtra(Status.POST_TAG, post) + ActivityScenario.launch(intent) + onView(withId(R.id.postPicture)).perform(longClick()) + onView(withText(R.string.save_to_gallery)).inRoot(RootMatchers.isPlatformPopup()).perform(click()) + Thread.sleep(300) + onView(withText(R.string.image_download_downloading)).inRoot( + RootMatchers.hasWindowLayoutParams() + ).check(matches(isDisplayed())) + Thread.sleep(5000) + } + + @Test + fun saveToGalleryTestAlbum() { + val attachment1 = Attachment( + id = "12", + url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + ) + val attachment2 = Attachment( + id = "13", + url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + ) + val post = Status( + id = "12", + account = Account( + id = "12", + username = "douze" + ), + media_attachments = listOf(attachment1, attachment2) + ) + val intent = Intent(context, PostActivity::class.java) + intent.putExtra(Status.POST_TAG, post) + ActivityScenario.launch(intent) + onView(withId(R.id.imageImageView)).perform(longClick()) + onView(withText(R.string.save_to_gallery)).inRoot(RootMatchers.isPlatformPopup()).perform(click()) + Thread.sleep(300) + onView(withText(R.string.image_download_downloading)).inRoot( + RootMatchers.hasWindowLayoutParams() + ).check(matches(isDisplayed())) + Thread.sleep(5000) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/ImageFragment.kt b/app/src/main/java/com/h/pixeldroid/ImageFragment.kt index a991c57f..1a31ab49 100644 --- a/app/src/main/java/com/h/pixeldroid/ImageFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/ImageFragment.kt @@ -8,10 +8,13 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView +import android.widget.PopupMenu import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.h.pixeldroid.utils.ImageConverter +import com.h.pixeldroid.utils.ImageUtils import kotlinx.android.synthetic.main.post_fragment.view.* import java.io.Serializable @@ -38,9 +41,26 @@ class ImageFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + val view = inflater.inflate(R.layout.fragment_image, container, false) + view.findViewById(R.id.imageImageView).setOnLongClickListener { + PopupMenu(view.context, it).apply { + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.image_popup_menu_save_to_gallery -> { + ImageUtils.downloadImage(requireActivity(), view.context, imgUrl) + true + } + else -> false + } + } + inflate(R.menu.image_popup_menu) + show() + } + true + } // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_image, container, false) + return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -52,6 +72,7 @@ class ImageFragment : Fragment() { .placeholder(ColorDrawable(Color.GRAY)) picRequest.load(imgUrl).into(imageView) + } companion object { diff --git a/app/src/main/java/com/h/pixeldroid/MainActivity.kt b/app/src/main/java/com/h/pixeldroid/MainActivity.kt index 4023766e..97e5f370 100644 --- a/app/src/main/java/com/h/pixeldroid/MainActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/MainActivity.kt @@ -59,6 +59,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte setupTabs(tabs) } + + } private fun setupTabs(tabs: Array){ diff --git a/app/src/main/java/com/h/pixeldroid/PostActivity.kt b/app/src/main/java/com/h/pixeldroid/PostActivity.kt index 5d797d8e..11f600c0 100644 --- a/app/src/main/java/com/h/pixeldroid/PostActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/PostActivity.kt @@ -6,8 +6,6 @@ import com.h.pixeldroid.fragments.PostFragment import com.h.pixeldroid.objects.Status import com.h.pixeldroid.objects.Status.Companion.POST_TAG - - class PostActivity : AppCompatActivity() { lateinit var postFragment : PostFragment diff --git a/app/src/main/java/com/h/pixeldroid/fragments/ProfilePostsFragment.kt b/app/src/main/java/com/h/pixeldroid/fragments/ProfilePostsFragment.kt index 16f62f23..e90a8d92 100644 --- a/app/src/main/java/com/h/pixeldroid/fragments/ProfilePostsFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/fragments/ProfilePostsFragment.kt @@ -27,11 +27,6 @@ class ProfilePostsFragment : Fragment() { private var columnCount = 3 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/java/com/h/pixeldroid/objects/Account.kt b/app/src/main/java/com/h/pixeldroid/objects/Account.kt index df65f383..4e7b262b 100644 --- a/app/src/main/java/com/h/pixeldroid/objects/Account.kt +++ b/app/src/main/java/com/h/pixeldroid/objects/Account.kt @@ -28,23 +28,23 @@ data class Account( //Base attributes override val id: String, val username: String, - val acct: String, - val url: String, //HTTPS URL + val acct: String = "", + val url: String = "", //HTTPS URL //Display attributes - val display_name: String?, - val note: String, //HTML - val avatar: String, //URL - val avatar_static: String, //URL - val header: String, //URL - val header_static: String, //URL - val locked: Boolean, - val emojis: List, - val discoverable: Boolean, + val display_name: String? = null, + val note: String = "", //HTML + val avatar: String = "", //URL + val avatar_static: String = "", //URL + val header: String = "", //URL + val header_static: String = "", //URL + val locked: Boolean = false, + val emojis: List? = null, + val discoverable: Boolean = true, //Statistical attributes - val created_at: String, //ISO 8601 Datetime (maybe can use a date type) - val statuses_count: Int, - val followers_count: Int, - val following_count: Int, + val created_at: String = "", //ISO 8601 Datetime (maybe can use a date type) + val statuses_count: Int = 0, + val followers_count: Int = 0, + val following_count: Int = 0, //Optional attributes val moved: Account? = null, val fields: List? = emptyList(), diff --git a/app/src/main/java/com/h/pixeldroid/objects/Attachment.kt b/app/src/main/java/com/h/pixeldroid/objects/Attachment.kt index dea5ba30..d90d81ef 100644 --- a/app/src/main/java/com/h/pixeldroid/objects/Attachment.kt +++ b/app/src/main/java/com/h/pixeldroid/objects/Attachment.kt @@ -5,9 +5,9 @@ import java.io.Serializable data class Attachment( //Required attributes val id: String, - val type: AttachmentType, + val type: AttachmentType = AttachmentType.image, val url: String, //URL - val preview_url: String, //URL + val preview_url: String = "", //URL //Optional attributes val remote_url: String? = null, //URL val text_url: String? = null, //URL diff --git a/app/src/main/java/com/h/pixeldroid/objects/Status.kt b/app/src/main/java/com/h/pixeldroid/objects/Status.kt index ea11ab9e..bffd04a4 100644 --- a/app/src/main/java/com/h/pixeldroid/objects/Status.kt +++ b/app/src/main/java/com/h/pixeldroid/objects/Status.kt @@ -1,5 +1,6 @@ package com.h.pixeldroid.objects +import android.app.Activity import android.content.Context import android.graphics.Typeface import android.graphics.drawable.Drawable @@ -9,20 +10,27 @@ import android.util.Log import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.widget.FrameLayout +import android.widget.ImageView import android.widget.LinearLayout +import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.core.text.toSpanned import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 import com.bumptech.glide.RequestBuilder import com.google.android.material.tabs.TabLayoutMediator import com.h.pixeldroid.ImageFragment +import com.h.pixeldroid.MainActivity import com.h.pixeldroid.R import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.fragments.feeds.PostViewHolder 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.likePostCall import com.h.pixeldroid.utils.PostUtils.Companion.postComment import com.h.pixeldroid.utils.PostUtils.Companion.reblogPost @@ -30,9 +38,10 @@ 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.undoReblogPost - -import kotlinx.android.synthetic.main.post_fragment.view.* - +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 import java.io.Serializable /* @@ -42,38 +51,38 @@ https://docs.joinmastodon.org/entities/status/ data class Status( //Base attributes override val id: String, - val uri: String, - val created_at: String, //ISO 8601 Datetime (maybe can use a date type) + val uri: String = "", + val created_at: String = "", //ISO 8601 Datetime (maybe can use a date type) val account: Account, - val content: String, //HTML - val visibility: Visibility, - val sensitive: Boolean, - val spoiler_text: String, - val media_attachments: List?, - val application: Application, + val content: String = "", //HTML + val visibility: Visibility = Visibility.public, + val sensitive: Boolean = false, + val spoiler_text: String = "", + val media_attachments: List? = null, + val application: Application? = null, //Rendering attributes - val mentions: List, - val tags: List, - val emojis: List, + val mentions: List? = null, + val tags: List? = null, + val emojis: List? = null, //Informational attributes - val reblogs_count: Int, - val favourites_count: Int, - val replies_count: Int, + val reblogs_count: Int = 0, + val favourites_count: Int = 0, + val replies_count: Int = 0, //Nullable attributes - val url: String?, //URL - val in_reply_to_id: String?, - val in_reply_to_account: String?, - val reblog: Status?, - val poll: Poll?, - val card: Card?, - val language: String?, //ISO 639 Part 1 two-letter language code - val text: String?, + val url: String? = null, //URL + val in_reply_to_id: String? = null, + val in_reply_to_account: String? = null, + val reblog: Status? = null, + val poll: Poll? = null, + val card: Card? = null, + val language: String? = null, //ISO 639 Part 1 two-letter language code + val text: String? = null, //Authorized user attributes - val favourited: Boolean, - val reblogged: Boolean, - val muted: Boolean, - val bookmarked: Boolean, - val pinned: Boolean + val favourited: Boolean = false, + val reblogged: Boolean = false, + val muted: Boolean = false, + val bookmarked: Boolean = false, + val pinned: Boolean = false ) : Serializable, FeedContent() { @@ -194,6 +203,8 @@ data class Status( //Set comment initial visibility rootView.findViewById(R.id.commentIn).visibility = View.GONE + + imagePopUpMenu(rootView, homeFragment.requireActivity()) } fun setDescription(rootView: View, api : PixelfedAPI, credential: String) { @@ -292,4 +303,27 @@ data class Status( enum class Visibility : Serializable { public, unlisted, private, direct } + + + fun imagePopUpMenu(view: View, activity: FragmentActivity) { + val anchor = view.findViewById(R.id.post_fragment_image_popup_menu_anchor) + if (!media_attachments.isNullOrEmpty() && media_attachments.size == 1) { + view.findViewById(R.id.postPicture).setOnLongClickListener { + PopupMenu(view.context, anchor).apply { + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.image_popup_menu_save_to_gallery -> { + downloadImage(activity, view.context, getPostUrl()!!) + true + } + else -> false + } + } + inflate(R.menu.image_popup_menu) + show() + } + true + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/ImageUtils.kt b/app/src/main/java/com/h/pixeldroid/utils/ImageUtils.kt new file mode 100644 index 00000000..48b8274f --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/utils/ImageUtils.kt @@ -0,0 +1,77 @@ +package com.h.pixeldroid.utils + +import android.app.DownloadManager +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import android.provider.Settings.Global.getString +import android.widget.Toast +import androidx.fragment.app.FragmentActivity +import com.h.pixeldroid.R +import java.io.File + +class ImageUtils { + companion object { + fun downloadImage(activity: FragmentActivity, context: Context, url: String) { + var msg = "" + var lastMsg = "" + val directory = File(Environment.DIRECTORY_PICTURES) + if (!directory.exists()) { + directory.mkdirs() + } + val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) + as DownloadManager + val downloadUri = Uri.parse(url) + val title = url.substring(url.lastIndexOf("/") + 1) + val request = DownloadManager.Request(downloadUri).apply { + setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI + or DownloadManager.Request.NETWORK_MOBILE) + setTitle(title) + setDestinationInExternalPublicDir(directory.toString(), title) + } + val downloadId = downloadManager.enqueue(request) + val query = DownloadManager.Query().setFilterById(downloadId) + + Thread(Runnable { + var downloading = true + while (downloading) { + val cursor: Cursor = downloadManager.query(query) + cursor.moveToFirst() + if (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) + == DownloadManager.STATUS_SUCCESSFUL) { + downloading = false + } + val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) + msg = when (status) { + DownloadManager.STATUS_FAILED -> + context.getString(R.string.image_download_failed) + DownloadManager.STATUS_RUNNING -> + context.getString(R.string.image_download_downloading) + DownloadManager.STATUS_SUCCESSFUL -> + context.getString(R.string.image_download_success) + else -> "" + } + if (msg != lastMsg && msg != "") { + activity.runOnUiThread { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + lastMsg = msg + } + cursor.close() + } + }).start() + } + + private fun statusMessage(url: String, directory: File, status: Int, context: Context): String { + return when (status) { + DownloadManager.STATUS_FAILED -> context.getString(R.string.image_download_failed) + DownloadManager.STATUS_RUNNING -> "Downloading..." + DownloadManager.STATUS_SUCCESSFUL -> "Image downloaded successfully in $directory" + File.separator + url.substring( + url.lastIndexOf("/") + 1 + ) + else -> "" + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_from_left.xml b/app/src/main/res/anim/slide_from_left.xml deleted file mode 100644 index d8ee8270..00000000 --- a/app/src/main/res/anim/slide_from_left.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_from_right.xml b/app/src/main/res/anim/slide_from_right.xml deleted file mode 100644 index 9b2bfa12..00000000 --- a/app/src/main/res/anim/slide_from_right.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_to_left.xml b/app/src/main/res/anim/slide_to_left.xml deleted file mode 100644 index 4eceda0a..00000000 --- a/app/src/main/res/anim/slide_to_left.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/slide_to_right.xml b/app/src/main/res/anim/slide_to_right.xml deleted file mode 100644 index cb41f785..00000000 --- a/app/src/main/res/anim/slide_to_right.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml deleted file mode 100644 index 0258249c..00000000 --- a/app/src/main/res/drawable/ic_add_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_bluetooth.xml b/app/src/main/res/drawable/ic_bluetooth.xml deleted file mode 100644 index 1094756b..00000000 --- a/app/src/main/res/drawable/ic_bluetooth.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_comment_full.xml b/app/src/main/res/drawable/ic_comment_full.xml deleted file mode 100644 index 3eeab828..00000000 --- a/app/src/main/res/drawable/ic_comment_full.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_dot_blue_24dp.xml b/app/src/main/res/drawable/ic_dot_blue_24dp.xml deleted file mode 100644 index 0b49ec37..00000000 --- a/app/src/main/res/drawable/ic_dot_blue_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_font_size.xml b/app/src/main/res/drawable/ic_font_size.xml deleted file mode 100644 index dd81ddfd..00000000 --- a/app/src/main/res/drawable/ic_font_size.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml deleted file mode 100644 index be9f8368..00000000 --- a/app/src/main/res/drawable/ic_notifications.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_star_white_24dp.xml b/app/src/main/res/drawable/ic_star_white_24dp.xml deleted file mode 100644 index 711b2e33..00000000 --- a/app/src/main/res/drawable/ic_star_white_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_time.xml b/app/src/main/res/drawable/ic_time.xml deleted file mode 100644 index 2239a4f4..00000000 --- a/app/src/main/res/drawable/ic_time.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml deleted file mode 100644 index 10841511..00000000 --- a/app/src/main/res/drawable/ic_translate.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml deleted file mode 100644 index e4b83cf5..00000000 --- a/app/src/main/res/layout/activity_camera.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - -