Merge with master

This commit is contained in:
mjaillot 2020-05-21 20:08:42 +02:00
commit 556091a587
164 changed files with 4213 additions and 1573 deletions

14
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,14 @@
---
name: Bug
about: Report a bug you found in the app
title: "[BUG]"
labels: bug
assignees: ''
---
* [ ] I checked the release notes for known issues
* [ ] I checked the existing issues
* [ ] I checked that the issue is not related to my instance configuration, and that my instance has the latest version of pixelfed
Describe step by step how to reproduce the bug, include screenshots if possible.

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
.idea
app/release

View File

@ -2,4 +2,15 @@
![Pixeldroid project logo](pixeldroid_logo.png)
Free (as in freedom) Android client for Pixelfed, the federated image sharing platform.
[![Build Status](https://gitlab.com/Matttter/PixelDroid/badges/master/pipeline.svg)](https://gitlab.com/Matttter/PixelDroid/pipelines) [![Maintainability](https://api.codeclimate.com/v1/badges/a4f1747dc60b96eb74df/maintainability)](https://codeclimate.com/github/H-PixelDroid/PixelDroid/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/a4f1747dc60b96eb74df/test_coverage)](https://codeclimate.com/github/H-PixelDroid/PixelDroid/test_coverage) [![Translation status](https://weblate.pixeldroid.org/widgets/pixeldroid/-/pixeldroid/svg-badge.svg)](https://weblate.pixeldroid.org/engage/pixeldroid/?utm_source=widget)
[![Build Status](https://gitlab.com/Matttter/PixelDroid/badges/master/pipeline.svg)](https://gitlab.com/Matttter/PixelDroid/pipelines) [![Maintainability](https://api.codeclimate.com/v1/badges/a4f1747dc60b96eb74df/maintainability)](https://codeclimate.com/github/H-PixelDroid/PixelDroid/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/a4f1747dc60b96eb74df/test_coverage)](https://codeclimate.com/github/H-PixelDroid/PixelDroid/test_coverage) [![Translation status](https://weblate.pixeldroid.org/widgets/pixeldroid/-/pixeldroid/svg-badge.svg)](https://weblate.pixeldroid.org/engage/pixeldroid/?utm_source=widget)
<a href=https://apt.izzysoft.de/fdroid/index/apk/com.h.pixeldroid><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="170"></a>
## Compiling the code yourself
If you want to try out PixelDroid on your own device, you can try to compile the source code yourself. To do that you will need to install [Android Studio](https://developer.android.com/studio/).
- Open the ___gradle___ project inside of Android Studio. Then you should plug your Android device into your computer (make sure that your device is in [developer mode](https://developer.android.com/studio/debug/dev-options)) and select ___share files___ on it.
- You should see that Android studio has detected your device and its name should appear next to a small play button on the top right corner of Android Studio. If that is the case, then you can click said play button and, after Android studio will have built the project, you'll be able to use PixelDroid on your device!
At this point PixelDroid will be installed on your phone, so it won't have to be plugged in anymore!

View File

@ -19,11 +19,14 @@ android {
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"
versionName "1.0.alpha1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions{
disable 'MissingTranslation'
}
sourceSets {
main.java.srcDirs += 'src/main/java'
test.java.srcDirs += 'src/test/java'
@ -62,22 +65,25 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.8.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.16'
implementation 'io.reactivex.rxjava2:rxjava:2.2.17'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "androidx.browser:browser:1.2.0"
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.connyduck:sparkbutton:3.0.0'
implementation 'com.github.connyduck:sparkbutton:4.0.0'
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.github.yalantis:ucrop:2.2.5-native'
implementation("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"
@ -88,7 +94,7 @@ dependencies {
// Excludes the support library because it's already included by Glide.
transitive = false
}
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation "com.github.tomakehurst:wiremock-jre8:2.26.3"
@ -104,6 +110,21 @@ dependencies {
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "com.mikepenz:materialdrawer:8.0.3"
//required support lib modules
implementation "androidx.annotation:annotation:1.1.0"
// Add for NavController support
implementation "com.mikepenz:materialdrawer-nav:8.0.3"
//iconics
implementation "com.mikepenz:materialdrawer-iconics:8.0.3"
implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
@ -111,7 +132,21 @@ dependencies {
def fragment_version = '1.2.4'
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
implementation 'com.karumi:dexter:6.1.0'
// Use the most recent version of CameraX
def camerax_version = '1.0.0-beta03'
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha10'
implementation 'com.karumi:dexter:6.1.2'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
tasks.withType(Test) {

View File

@ -1,70 +0,0 @@
package com.h.pixeldroid
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.PostDao
import com.h.pixeldroid.db.PostEntity
import com.h.pixeldroid.utils.*
import org.junit.*
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.util.Calendar
@RunWith(AndroidJUnit4::class)
class AppDatabaseTest {
private var postDao: PostDao? = null
private var db: AppDatabase? = null
private var postTest = PostEntity(1, "test", date= Calendar.getInstance().time)
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@Before
fun setup() {
AppDatabase.TEST_MODE = true
db = AppDatabase.getDatabase(ApplicationProvider.getApplicationContext())
postDao = db?.postDao()
postDao?.insertAll(postTest)
}
@After
fun tearDown() {
}
@Test
fun testInsertPostItem() {
Assert.assertEquals(postTest.domain, postDao?.getById(postTest.uid)!!.domain)
}
@Test
fun testDeleteAll(){
postDao?.deleteAll()
Assert.assertEquals(postDao?.getPostsCount(), 0)
}
@Test
fun testUtilsInsertAll() {
val postTest2 = PostEntity(2, "test", date= Calendar.getInstance().time)
DatabaseUtils.insertAllPosts(db!!, postTest, postTest2)
Assert.assertEquals(postTest.domain, postDao?.getById(postTest.uid)!!.domain)
Assert.assertEquals(postTest2.domain, postDao?.getById(postTest2.uid)!!.domain)
}
@Test
fun testUtilsLRU() {
for(i in 1..db!!.MAX_NUMBER_OF_POSTS) {
//sleep a bit to not have the weird concurrency bugs?
Thread.sleep(10)
DatabaseUtils.insertAllPosts(db!!, PostEntity(i, i.toString(), date= Calendar.getInstance().time))
}
Assert.assertEquals("1", postDao?.getById(1)!!.domain)
Assert.assertEquals(db?.MAX_NUMBER_OF_POSTS, postDao?.getPostsCount())
DatabaseUtils.insertAllPosts(db!!, PostEntity(0, "0", date= Calendar.getInstance().time))
Assert.assertEquals(db?.MAX_NUMBER_OF_POSTS, postDao?.getPostsCount())
val eldestPost = postDao?.getById(1)
Assert.assertEquals(null, eldestPost)
}
}

View File

@ -0,0 +1,113 @@
package com.h.pixeldroid
import android.Manifest
import android.content.Intent
import android.content.Intent.ACTION_CHOOSER
import android.graphics.Bitmap
import android.graphics.Color
import android.media.MediaScannerConnection
import android.os.Environment
import android.webkit.MimeTypeMap
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.h.pixeldroid.fragments.CameraFragment
import kotlinx.android.synthetic.main.camera_ui_container.*
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File
class CameraTest {
@get:Rule
val mRuntimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
out.flush()
}
}
@Before
fun before(){
Intents.init()
}
@After
fun after(){
Intents.release()
}
@Test
fun takePictureButton() {
var scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment {
val image = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
image.eraseColor(Color.GREEN)
val folder =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
if (!folder.exists()) {
folder.mkdir()
}
val file = File.createTempFile("temp_img", ".png", folder)
file.writeBitmap(image)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(file.extension)
MediaScannerConnection.scanFile(
context,
arrayOf(file.absolutePath),
arrayOf(mimeType)){_, _ ->
}
}
scenario = launchFragmentInContainer<CameraFragment>()
Thread.sleep(2000)
scenario.onFragment { fragment ->
fragment.camera_capture_button.performClick()
}
scenario.onFragment { fragment ->
assert(fragment.isHidden)
}
}
@Test
fun uploadButton() {
val expectedIntent: Matcher<Intent> = CoreMatchers.allOf(
IntentMatchers.hasAction(ACTION_CHOOSER)
)
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.photo_view_button.performClick()
}
Thread.sleep(1000)
Intents.intended(expectedIntent)
}
@Test
fun switchButton() {
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.camera_switch_button.performClick()
}
Thread.sleep(1000)
scenario.onFragment { fragment ->
assert(!fragment.isHidden)
}
}
}

View File

@ -1,22 +1,26 @@
package com.h.pixeldroid
import android.content.Context
import android.view.Gravity
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
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.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.testUtility.CustomMatchers
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -27,40 +31,81 @@ import org.junit.runner.RunWith
class DrawerMenuTest {
private val mockServer = MockServer()
private lateinit var db: AppDatabase
private lateinit var context: Context
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(30)
@get:Rule
var activityRule: ActivityScenarioRule<MainActivity>
= ActivityScenarioRule(MainActivity::class.java)
@Before
fun before(){
mockServer.start()
val baseUrl = mockServer.getUrl()
val preferences = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply()
context = ApplicationProvider.getApplicationContext()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
// Open Drawer to click on navigation.
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.drawer_layout))
.check(matches(DrawerMatchers.isClosed(Gravity.LEFT))) // Left Drawer should be closed.
.check(matches(DrawerMatchers.isClosed())) // Left Drawer should be closed.
.perform(DrawerActions.open()) // Open Drawer
}
@Test
@Test
fun testDrawerSettingsButton() {
// Start the screen of your activity.
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_settings))
onView(withText(R.string.menu_settings)).perform(click())
// Check that settings activity was opened.
onView(withText(R.string.signature_title)).check(matches(isDisplayed()))
onView(withText(R.string.theme_title)).check(matches(isDisplayed()))
}
@Test
fun testThemeSettings() {
// Start the screen of your activity.
onView(withText(R.string.menu_settings)).perform(click())
val themes = getInstrumentation().targetContext.resources.getStringArray(R.array.theme_entries)
//select theme modes
onView(withText(R.string.theme_title)).perform(click())
onView(withText(themes[2])).perform(click())
//Select an other theme
onView(withText(R.string.theme_title)).perform(click())
onView(withText(themes[0])).perform(click())
//Select the last theme
onView(withText(R.string.theme_title)).perform(click())
onView(withText(themes[1])).perform(click())
//Check that we are back in the settings page
onView(withText(R.string.theme_header)).check(matches(isDisplayed()))
}
@Test
fun testDrawerLogoutButton() {
// Start the screen of your activity.
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_logout))
onView(withText(R.string.logout)).perform(click())
// Check that settings activity was opened.
onView(withId(R.id.connect_instance_button)).check(matches(isDisplayed()))
}
@ -68,38 +113,38 @@ class DrawerMenuTest {
@Test
fun testDrawerProfileButton() {
// Start the screen of your activity.
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_account))
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.profilePictureImageView)).check(matches(isDisplayed()))
}
@Test
/*@Test
fun testDrawerAvatarClick() {
// Start the screen of your activity.
onView(withId(R.id.drawer_avatar)).perform(ViewActions.click())
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.profilePictureImageView)).check(matches(isDisplayed()))
}
}*/
@Test
/*@Test
fun testDrawerAccountNameClick() {
// Start the screen of your activity.
onView(withId(R.id.drawer_account_name)).perform(ViewActions.click())
onView(withText("Testi")).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.profilePictureImageView)).check(matches(isDisplayed()))
}
onView(withText("Add Account")).check(matches(isDisplayed()))
}*/
@Test
fun clickFollowers() {
// Open My Profile from drawer
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_account))
onView(withText(R.string.menu_account)).perform(click())
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform(ViewActions.click())
onView(withId(R.id.nbFollowersTextView)).perform(click())
Thread.sleep(1000)
// Open follower's profile
onView(withText("ete2")).perform(ViewActions.click())
onView(withText("ete2")).perform(click())
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Christian")))
@ -108,13 +153,13 @@ class DrawerMenuTest {
@Test
fun clickFollowing() {
// Open My Profile from drawer
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_account))
onView(withText(R.string.menu_account)).perform(click())
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowingTextView)).perform(ViewActions.click())
onView(withId(R.id.nbFollowingTextView)).perform(click())
Thread.sleep(1000)
// Open following's profile
onView(withText("Dobios")).perform(ViewActions.click())
onView(withText("Dobios")).perform(click())
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Andrew Dobis")))
@ -123,7 +168,7 @@ class DrawerMenuTest {
@Test
fun showBookmarkedPosts() {
// Open My Profile from drawer
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_account))
onView(withText(R.string.menu_account)).perform(click())
Thread.sleep(100)
// Open bookmarks tab
onView(withId(R.id.profile_view_pager))
@ -133,9 +178,17 @@ class DrawerMenuTest {
// Open first post
onView(withId(R.id.profilePostsRecyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<ProfilePostsRecyclerViewAdapter.ViewHolder>
(0, CustomMatchers.clickChildViewWithId(R.id.postPreview)))
.perform(
RecyclerViewActions.actionOnItemAtPosition<ProfilePostsRecyclerViewAdapter.ViewHolder>
(0, CustomMatchers.clickChildViewWithId(R.id.postPreview))
)
onView(withId(R.id.nlikes)).check(matches(withText("5 Likes")))
}
fun onBackPressedClosesDrawer() {
UiDevice.getInstance(getInstrumentation()).pressBack()
Thread.sleep(1000)
onView(withId(R.id.drawer_layout)).check(matches(DrawerMatchers.isClosed()))
}
}

View File

@ -1,6 +1,5 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
@ -20,7 +19,6 @@ import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.MockServer
import kotlinx.android.synthetic.main.fragment_edit_image.*
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert
@ -33,7 +31,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EditPhotoTest {
private val mockServer = MockServer()
private lateinit var activity: PhotoEditActivity
private lateinit var activityScenario: ActivityScenario<PhotoEditActivity>
@ -46,15 +43,10 @@ class EditPhotoTest {
@Before
fun before() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
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()
// Launch PhotoEditActivity
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PhotoEditActivity::class.java).putExtra("uri", uri)
val intent = Intent(context, PhotoEditActivity::class.java).putExtra("picture_uri", uri)
activityScenario = ActivityScenario.launch<PhotoEditActivity>(intent).onActivity{a -> activity = a}
@ -108,10 +100,11 @@ class EditPhotoTest {
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(5, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@Test
fun BirghtnessSaturationContrastTest() {
fun BrightnessSaturationContrastTest() {
Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1))
Thread.sleep(1000)
@ -139,11 +132,10 @@ class EditPhotoTest {
@Test
fun SaveButton() {
Espresso.onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
Espresso.onView(withId(R.id.toolbar)).check(matches(isDisplayed()))
Espresso.onView(withId(R.id.action_save)).perform(click())
Espresso.onView(withId(com.google.android.material.R.id.snackbar_text))
.check(matches(withText("Image succesfully saved")))
}
@Test
@ -153,4 +145,11 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.post_creation_picture_frame)).check(matches(isDisplayed()))
}
@Test
fun croppingIsPossible() {
Espresso.onView(withId(R.id.cropImageButton)).perform(click())
Thread.sleep(1000)
Espresso.onView(withId(R.id.menu_crop)).perform(click())
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
}

View File

@ -4,32 +4,29 @@ import android.content.Context
import android.content.Intent
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.Gravity
import android.view.View
import android.widget.TextView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@ -45,6 +42,8 @@ import org.junit.runner.RunWith
class IntentTest {
private val mockServer = MockServer()
private lateinit var db: AppDatabase
private lateinit var context: Context
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@ -59,10 +58,30 @@ class IntentTest {
fun before() {
mockServer.start()
val baseUrl = mockServer.getUrl()
val preferences = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply()
context = ApplicationProvider.getApplicationContext()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
Intents.init()
}
@ -96,7 +115,7 @@ class IntentTest {
intended(expectedIntent)
}
fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction {
private fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
@ -104,7 +123,7 @@ class IntentTest {
}
override fun getDescription(): String {
return "clicking on a ClickableSpan";
return "clicking on a ClickableSpan"
}
override fun perform(uiController: UiController, view: View) {
@ -116,7 +135,7 @@ class IntentTest {
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
.build()
}
// Get the links inside the TextView and check if we find textToClick
@ -128,9 +147,9 @@ class IntentTest {
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().equals(sequence.toString())) {
if (textToClick.toString() == sequence.toString()) {
span.onClick(textView)
return;
return
}
}
}
@ -145,14 +164,15 @@ class IntentTest {
}
}
@Test
/*@Test
fun launchesIntent() {
// Open Drawer to click on navigation.
ActivityScenario.launch(MainActivity::class.java)
Espresso.onView(ViewMatchers.withId(R.id.drawer_layout))
.check(ViewAssertions.matches(DrawerMatchers.isClosed(Gravity.LEFT))) // Left Drawer should be closed.
.perform(DrawerActions.open()) // Open Drawer
Espresso.onView(ViewMatchers.withId(R.id.nav_view))
Espresso.onView(ViewMatchers.withId(R.id.drawer))
.perform(NavigationViewActions.navigateTo(R.id.nav_account))
val expectedIntent: Matcher<Intent> = CoreMatchers.allOf(
@ -166,7 +186,7 @@ class IntentTest {
Thread.sleep(1000)
intended(expectedIntent)
}
} */
@After
fun after() {

View File

@ -1,29 +1,20 @@
package com.h.pixeldroid
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
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.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString
import androidx.test.espresso.matcher.ViewMatchers.hasErrorText
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.junit.After
@ -39,34 +30,6 @@ import org.junit.runner.RunWith
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class LoginInstrumentedTest {
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule
var activityRule: ActivityScenarioRule<LoginActivity>
= ActivityScenarioRule(LoginActivity::class.java)
@Test
fun clickConnect() {
onView(withId(R.id.connect_instance_button)).check(matches(withText("Connect to Pixelfed")))
}
@Test
fun invalidURL() {
onView(withId(R.id.editText)).perform(ViewActions.replaceText("/jdi"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain")))
}
@Test
fun notPixelfedInstance() {
onView(withId(R.id.editText)).perform(ViewActions.replaceText("localhost"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Could not register the application with this server")))
}
}
@RunWith(AndroidJUnit4::class)
class LoginCheckIntent {
@get:Rule
@ -117,33 +80,3 @@ class LoginCheckIntent {
Intents.release()
}
}
@RunWith(AndroidJUnit4::class)
class AfterIntent {
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule
val rule = ActivityTestRule(LoginActivity::class.java)
private var launchedActivity: Activity? = null
@Before
fun setup() {
val preferences = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("domain", "http://localhost").apply()
val intent = Intent(ACTION_VIEW, Uri.parse("oauth2redirect://com.h.pixeldroid?code=sdfdqsf"))
launchedActivity = rule.launchActivity(intent)
}
@Test
fun usesIntent() {
Thread.sleep(5000)
onView(withId(R.id.editText)).check(matches(
anyOf(hasErrorText("Error getting token"),
hasErrorText("Could not authenticate"))))
}
}

View File

@ -0,0 +1,58 @@
package com.h.pixeldroid
import android.content.Context
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.utils.DBUtils
import org.junit.After
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 LoginActivityOfflineTest {
private lateinit var db: AppDatabase
private lateinit var device: UiDevice
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@Before
fun before() {
device = UiDevice.getInstance(getInstrumentation())
device.openQuickSettings()
device.findObject(UiSelector().textContains("airplane")).click()
device.pressHome()
val context = ApplicationProvider.getApplicationContext<Context>()
db = DBUtils.initDB(context)
db.clearAllTables()
db.close()
}
@Test
fun emptyDBandOfflineModeDisplayCorrectMessage() {
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.login_activity_connection_required_text)).check(matches(isDisplayed()))
}
@After
fun after() {
device.openQuickSettings()
device.findObject(UiSelector().textContains("airplane")).click()
device.pressHome()
}
}

View File

@ -0,0 +1,133 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.content.SharedPreferences
import android.net.Uri
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasErrorText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
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 LoginActivityOnlineTest {
private lateinit var db: AppDatabase
private lateinit var context: Context
private lateinit var pref: SharedPreferences
private lateinit var server: MockServer
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@Before
fun setup() {
server = MockServer()
server.start()
context = ApplicationProvider.getApplicationContext()
pref = context.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
pref.edit().clear().apply()
db = DBUtils.initDB(context)
db.clearAllTables()
db.close()
}
@Test
fun notPixelfedInstance() {
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.editText))
.perform(replaceText("localhost"), closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click())
onView(withId(R.id.editText))
.check(matches(hasErrorText(context.getString(R.string.registration_failed))))
}
@Test
fun emptyStringNotAllowed() {
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.connect_instance_button)).perform(click())
onView(withId(R.id.editText)).check(matches(
hasErrorText(context.getString(R.string.invalid_domain))
))
}
@Test
fun wrongIntentReturnInfoFailsTest() {
pref.edit()
.putString("domain", "https://dhbfnhgbdbbet")
.putString("clientID", "iwndoiuqwnd")
.putString("clientSecret", "wlifowed")
.apply()
val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=sdfdqsf")
val intent = Intent(ACTION_VIEW, uri, context, LoginActivity::class.java)
ActivityScenario.launch<LoginActivity>(intent)
onView(withId(R.id.editText)).check(matches(
hasErrorText(context.getString(R.string.token_error))
))
}
@Test
fun incompleteIntentReturnInfoFailsTest() {
val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=")
val intent = Intent(ACTION_VIEW, uri, context, LoginActivity::class.java)
ActivityScenario.launch<LoginActivity>(intent)
onView(withId(R.id.editText)).check(matches(
hasErrorText(context.getString(R.string.auth_failed))
))
}
@Test
fun correctIntentReturnLoadsMainActivity() {
context = ApplicationProvider.getApplicationContext()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = server.getUrl().toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = server.getUrl().toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
pref.edit()
.putString("domain", server.getUrl().toString())
.putString("clientID", "test_id")
.putString("clientSecret", "test_secret")
.apply()
val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=test_code")
val intent = Intent(ACTION_VIEW, uri, context, LoginActivity::class.java)
ActivityScenario.launch<LoginActivity>(intent)
Thread.sleep(1000)
onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed()))
}
}

View File

@ -1,27 +1,37 @@
package com.h.pixeldroid
import android.content.Context
import android.graphics.ColorMatrix
import android.widget.TextView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
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
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
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.testUtility.CustomMatchers
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.DBUtils
import com.h.pixeldroid.utils.PostUtils.Companion.censorColorMatrix
import com.h.pixeldroid.utils.PostUtils.Companion.uncensorColorMatrix
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -34,21 +44,38 @@ class MockedServerTest {
private val mockServer = MockServer()
private lateinit var activityScenario: ActivityScenario<MainActivity>
private lateinit var db: AppDatabase
private lateinit var context: Context
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule
var activityRule: ActivityScenarioRule<MainActivity>
= ActivityScenarioRule(MainActivity::class.java)
@Before
fun before(){
mockServer.start()
val baseUrl = mockServer.getUrl()
val preferences = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply()
context = ApplicationProvider.getApplicationContext()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
activityScenario = ActivityScenario.launch(MainActivity::class.java)
}
@ -120,12 +147,12 @@ class MockedServerTest {
Thread.sleep(1000)
// Unfollow
onView(withId(R.id.followButton)).perform((ViewActions.click()))
onView(withId(R.id.followButton)).perform((click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Follow")))
// Follow
onView(withId(R.id.followButton)).perform((ViewActions.click()))
onView(withId(R.id.followButton)).perform((click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Unfollow")))
}
@ -177,7 +204,7 @@ class MockedServerTest {
onView(withId(R.id.view_pager)).perform(ViewActions.swipeUp()).perform(ViewActions.swipeDown())
Thread.sleep(1000)
onView(withText("Dobios liked your post")).perform(ViewActions.click())
onView(withText("Dobios liked your post")).perform(click())
Thread.sleep(1000)
onView(withText("6 Likes")).check(matches(withId(R.id.nlikes)))
@ -193,7 +220,7 @@ class MockedServerTest {
onView(withId(R.id.view_pager)).perform(ViewActions.swipeUp()).perform(ViewActions.swipeDown())
Thread.sleep(1000)
onView(withText("Dobios followed you")).perform(ViewActions.click())
onView(withText("Dobios followed you")).perform(click())
Thread.sleep(1000)
onView(withText("Andrew Dobis")).check(matches(withId(R.id.accountNameTextView)))
}
@ -208,10 +235,10 @@ class MockedServerTest {
onView(withId(R.id.view_pager)).perform(ViewActions.swipeUp()).perform(ViewActions.swipeDown())
Thread.sleep(1000)
onView(withText("Dobios liked your post")).perform(ViewActions.click())
onView(withText("Dobios liked your post")).perform(click())
Thread.sleep(1000)
onView(withId(R.id.username)).perform(ViewActions.click())
onView(withId(R.id.username)).perform(click())
Thread.sleep(10000)
onView(withText("Dante")).check(matches(withId(R.id.accountNameTextView)))
}
@ -226,7 +253,7 @@ class MockedServerTest {
onView(withId(R.id.view_pager)).perform(ViewActions.swipeUp()).perform(ViewActions.swipeDown())
Thread.sleep(1000)
onView(withText("Clement shared your post")).perform(ViewActions.click())
onView(withText("Clement shared your post")).perform(click())
Thread.sleep(1000)
onView(first(withText("Clement"))).check(matches(withId(R.id.username)))
@ -286,7 +313,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()
}
}
@ -529,5 +558,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

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -11,7 +12,11 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -21,7 +26,9 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PostCreationActivityTest {
val mockServer = MockServer()
private val mockServer = MockServer()
private lateinit var db: AppDatabase
@get:Rule
val globalTimeout: Timeout = Timeout.seconds(30)
@ -31,10 +38,27 @@ class PostCreationActivityTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
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()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PostCreationActivity::class.java)
.putExtra("picture_uri", uri)

View File

@ -16,8 +16,13 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.camera_ui_container.*
import org.hamcrest.Matcher
import org.junit.Before
import org.junit.Rule
@ -50,7 +55,7 @@ class PostCreationFragmentTest {
fun uploadButtonLaunchesGalleryIntent() {
val expectedIntent: Matcher<Intent> = hasAction(Intent.ACTION_CHOOSER)
intending(expectedIntent)
onView(withId(R.id.uploadPictureButton)).perform(click())
onView(withId(R.id.photo_view_button)).perform(click())
Thread.sleep(1000)
intended(expectedIntent)
}
@ -65,24 +70,45 @@ class PostFragmentUITests {
@get:Rule
var rule = ActivityScenarioRule(MainActivity::class.java)
private lateinit var db: AppDatabase
@Before
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
mockServer.start()
val baseUrl = mockServer.getUrl()
val preferences = InstrumentationRegistry.getInstrumentation()
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
ActivityScenario.launch(MainActivity::class.java).onActivity {
a -> a.tabs.getTabAt(2)!!.select()
}
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
Thread.sleep(300)
}
@Test
fun newPostUiTest() {
onView(withId(R.id.uploadPictureButton)).check(matches(isDisplayed()))
onView(withId(R.id.takePictureButton)).check(matches(isDisplayed()))
ActivityScenario.launch(MainActivity::class.java).onActivity {
a -> a.tabs.getTabAt(2)!!.select()
}
Thread.sleep(1500)
onView(withId(R.id.photo_view_button)).check(matches(isDisplayed()))
onView(withId(R.id.camera_capture_button)).check(matches(isDisplayed()))
}
}

View File

@ -4,30 +4,32 @@ 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
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.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.rule.IntentsTestRule
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 androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Application
import com.h.pixeldroid.objects.Attachment
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.objects.Tag
import com.h.pixeldroid.testUtility.MockServer
import org.hamcrest.CoreMatchers
import com.h.pixeldroid.utils.DBUtils
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.*
import org.junit.rules.Timeout
import org.junit.runner.RunWith
@ -36,6 +38,7 @@ import org.junit.runner.RunWith
class PostTest {
private lateinit var context: Context
private lateinit var db: AppDatabase
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@ -46,11 +49,27 @@ class PostTest {
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()
db = DBUtils.initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.close()
Intents.init()
}
@ -166,6 +185,62 @@ class PostTest {
Intents.intended(expectedIntent)
}
@Test
fun getNLikesReturnsCorrectFormat() {
val status = Status(id="140364967936397312", uri="https://pixelfed.de/p/Miike/140364967936397312",
created_at="2020-03-03T08:00:16.000000Z",
account= Account(id="115114166443970560", username="Miike", acct="Miike",
url="https://pixelfed.de/Miike", display_name="Miike Duart", note="",
avatar="https://pixelfed.de/storage/avatars/011/511/416/644/397/056/0/ZhaopLJWTWJ3hsVCS5pS_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35",
avatar_static="https://pixelfed.de/storage/avatars/011/511/416/644/397/056/0/ZhaopLJWTWJ3hsVCS5pS_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35",
header="", header_static="", locked=false, emojis= emptyList(), discoverable=false,
created_at="2019-12-24T15:42:35.000000Z", statuses_count=71, followers_count=14,
following_count=0, moved=null, fields=null, bot=false, source=null),
content="""Day 8 <a href="https://pixelfed.de/discover/tags/rotavicentina?src=hash" title="#rotavicentina" class="u-url hashtag" rel="external nofollow noopener">#rotavicentina</a> <a href="https://pixelfed.de/discover/tags/hiking?src=hash" title="#hiking" class="u-url hashtag" rel="external nofollow noopener">#hiking</a> <a href="https://pixelfed.de/discover/tags/nature?src=hash" title="#nature" class="u-url hashtag" rel="external nofollow noopener">#nature</a>""",
visibility=Status.Visibility.public, sensitive=false, spoiler_text="",
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
emojis= emptyList(), reblogs_count=0, favourites_count=0, replies_count=0, url="https://pixelfed.de/p/Miike/140364967936397312",
in_reply_to_id=null, in_reply_to_account=null, reblog=null, poll=null, card=null, language=null, text=null, favourited=false, reblogged=false, muted=false, bookmarked=false, pinned=false)
Assert.assertEquals("${status.favourites_count} Likes",
status.getNLikes(getInstrumentation().targetContext))
}
@Test
fun getNSharesReturnsCorrectFormat() {
val status = Status(id="140364967936397312", uri="https://pixelfed.de/p/Miike/140364967936397312",
created_at="2020-03-03T08:00:16.000000Z",
account= Account(id="115114166443970560", username="Miike", acct="Miike",
url="https://pixelfed.de/Miike", display_name="Miike Duart", note="",
avatar="https://pixelfed.de/storage/avatars/011/511/416/644/397/056/0/ZhaopLJWTWJ3hsVCS5pS_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35",
avatar_static="https://pixelfed.de/storage/avatars/011/511/416/644/397/056/0/ZhaopLJWTWJ3hsVCS5pS_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35",
header="", header_static="", locked=false, emojis= emptyList(), discoverable=false,
created_at="2019-12-24T15:42:35.000000Z", statuses_count=71, followers_count=14,
following_count=0, moved=null, fields=null, bot=false, source=null),
content="""Day 8 <a href="https://pixelfed.de/discover/tags/rotavicentina?src=hash" title="#rotavicentina" class="u-url hashtag" rel="external nofollow noopener">#rotavicentina</a> <a href="https://pixelfed.de/discover/tags/hiking?src=hash" title="#hiking" class="u-url hashtag" rel="external nofollow noopener">#hiking</a> <a href="https://pixelfed.de/discover/tags/nature?src=hash" title="#nature" class="u-url hashtag" rel="external nofollow noopener">#nature</a>""",
visibility=Status.Visibility.public, sensitive=false, spoiler_text="",
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
emojis= emptyList(), reblogs_count=0, favourites_count=0, replies_count=0, url="https://pixelfed.de/p/Miike/140364967936397312",
in_reply_to_id=null, in_reply_to_account=null, reblog=null, poll=null, card=null, language=null, text=null, favourited=false, reblogged=false, muted=false, bookmarked=false, pinned=false)
Assert.assertEquals("${status.reblogs_count} Shares",
status.getNShares(getInstrumentation().targetContext))
}
@After
fun after() {
Intents.release()

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

File diff suppressed because one or more lines are too long

View File

@ -4,10 +4,10 @@
package="com.h.pixeldroid">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature
android:name="android.hardware.camera.any"
@ -15,6 +15,7 @@
<uses-feature android:name="android.hardware.location.gps" />
<application
android:name=".Pixeldroid"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -76,7 +77,8 @@
</intent-filter>
</activity>
<activity android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="fullSensor"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity

View File

@ -1,20 +1,20 @@
package com.h.pixeldroid
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.AccountListFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.objects.Account.Companion.FOLLOWING_TAG
import com.h.pixeldroid.utils.DBUtils
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class FollowsActivity : AppCompatActivity() {
var followsFragment = AccountListFragment()
private var followsFragment = AccountListFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -25,11 +25,15 @@ class FollowsActivity : AppCompatActivity() {
val following = intent.getSerializableExtra(FOLLOWING_TAG) as Boolean
if(id == null) {
val preferences = this.getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
val accessToken = preferences.getString("accessToken", "")
val db = DBUtils.initDB(applicationContext)
val user = db.userDao().getActiveUser()
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
db.close()
val pixelfedAPI = PixelfedAPI.create(domain)
pixelfedAPI.verifyCredentials("Bearer $accessToken").enqueue(object :
Callback<Account> {

View File

@ -6,15 +6,20 @@ import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Application
import com.h.pixeldroid.objects.Instance
import com.h.pixeldroid.objects.Token
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.DBUtils.Companion.storeInstance
import com.h.pixeldroid.utils.Utils
import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain
import kotlinx.android.synthetic.main.activity_login.*
import okhttp3.HttpUrl
import retrofit2.Call
@ -24,41 +29,50 @@ import retrofit2.Response
class LoginActivity : AppCompatActivity() {
private val TAG = "Login Activity"
companion object {
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
private const val SCOPE = "read write follow"
}
private lateinit var OAUTH_SCHEME: String
private val PACKAGE_ID = BuildConfig.APPLICATION_ID
private val SCOPE = "read write follow"
private lateinit var APP_NAME: String
private lateinit var oauthScheme: String
private lateinit var appName: String
private lateinit var preferences: SharedPreferences
private lateinit var db: AppDatabase
private lateinit var pixelfedAPI: PixelfedAPI
private var inputVisibility: Int = View.GONE
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
connect_instance_button.setOnClickListener { onClickConnect() }
whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
loadingAnimation(true)
appName = getString(R.string.app_name)
oauthScheme = getString(R.string.auth_scheme)
preferences = getSharedPreferences("$PACKAGE_ID.pref", Context.MODE_PRIVATE)
db = DBUtils.initDB(applicationContext)
APP_NAME = getString(R.string.app_name)
OAUTH_SCHEME = getString(R.string.auth_scheme)
preferences = getSharedPreferences(
"$PACKAGE_ID.pref", Context.MODE_PRIVATE
)
if (Utils.hasInternet(applicationContext)) {
connect_instance_button.setOnClickListener {
registerAppToServer(normalizeDomain(editText.text.toString()))
}
whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
inputVisibility = View.VISIBLE
} else {
login_activity_connection_required_text.visibility = View.VISIBLE
}
loadingAnimation(false)
}
override fun onStart(){
super.onStart()
val url = intent.data
val url: Uri? = intent.data
//Check if the activity was started after the authentication
if (url == null || !url.toString().startsWith("$OAUTH_SCHEME://$PACKAGE_ID")) return
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
loadingAnimation(true)
val code = url.getQueryParameter("code")
authenticate(code)
}
override fun onStop() {
@ -66,29 +80,6 @@ class LoginActivity : AppCompatActivity() {
loadingAnimation(false)
}
override fun onBackPressed() {
}
private fun onClickConnect() {
val normalizedDomain = normalizeDomain(editText.text.toString())
try{
HttpUrl.Builder().host(normalizedDomain).scheme("https").build()
} catch (e: IllegalArgumentException) {
return failedRegistration(getString(R.string.invalid_domain))
}
hideKeyboard()
loadingAnimation(true)
preferences.edit()
.putString("domain", "https://$normalizedDomain")
.apply()
getInstanceConfig()
registerAppToServer("https://$normalizedDomain")
}
private fun whatsAnInstance() {
val i = Intent(Intent.ACTION_VIEW)
@ -106,45 +97,47 @@ class LoginActivity : AppCompatActivity() {
}
}
private fun normalizeDomain(domain: String): String {
var d = domain.replace("http://", "")
d = d.replace("https://", "")
return d.trim(Char::isWhitespace)
}
private fun registerAppToServer(normalizedDomain: String) {
val callback = object : Callback<Application> {
try{
HttpUrl.Builder().host(normalizedDomain.replace("https://", "")).scheme("https").build()
} catch (e: IllegalArgumentException) {
return failedRegistration(getString(R.string.invalid_domain))
}
hideKeyboard()
loadingAnimation(true)
PixelfedAPI.create(normalizedDomain).registerApplication(
appName,"$oauthScheme://$PACKAGE_ID", SCOPE
).enqueue(object : Callback<Application> {
override fun onResponse(call: Call<Application>, response: Response<Application>) {
if (!response.isSuccessful) {
return failedRegistration()
}
val credentials = response.body()
val clientId = credentials?.client_id ?: return failedRegistration()
val clientSecret = credentials.client_secret
preferences.edit()
.putString("domain", normalizedDomain)
.apply()
val credentials = response.body() as Application
val clientId = credentials.client_id ?: return failedRegistration()
preferences.edit()
.putString("clientID", clientId)
.putString("clientSecret", clientSecret)
.putString("clientSecret", credentials.client_secret)
.apply()
promptOAuth(normalizedDomain, clientId)
}
override fun onFailure(call: Call<Application>, t: Throwable) {
return failedRegistration()
}
}
PixelfedAPI.create(normalizedDomain).registerApplication(
APP_NAME,"$OAUTH_SCHEME://$PACKAGE_ID", SCOPE
).enqueue(callback)
})
}
private fun promptOAuth(normalizedDomain: String, client_id: String) {
val url = "$normalizedDomain/oauth/authorize?" +
"client_id" + "=" + client_id + "&" +
"redirect_uri" + "=" + "$OAUTH_SCHEME://$PACKAGE_ID" + "&" +
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
"response_type=code" + "&" +
"scope=$SCOPE"
@ -165,11 +158,11 @@ class LoginActivity : AppCompatActivity() {
private fun authenticate(code: String?) {
// Get previous values from preferences
val domain = preferences.getString("domain", "")
val clientId = preferences.getString("clientID", "")
val clientSecret = preferences.getString("clientSecret", "")
val domain = preferences.getString("domain", "") as String
val clientId = preferences.getString("clientID", "") as String
val clientSecret = preferences.getString("clientSecret", "") as String
if (code == null || domain.isNullOrEmpty() || clientId.isNullOrEmpty() || clientSecret.isNullOrEmpty()) {
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
return failedRegistration(getString(R.string.auth_failed))
}
@ -187,61 +180,81 @@ class LoginActivity : AppCompatActivity() {
}
}
PixelfedAPI.create("$domain")
.obtainToken(
clientId, clientSecret, "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, code,
pixelfedAPI = PixelfedAPI.create(domain)
pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).enqueue(callback)
}
private fun authenticationSuccessful(accessToken: String) {
preferences.edit().putString("accessToken", accessToken).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
saveUserAndInstance(accessToken)
wipeSharedSettings()
}
private fun failedRegistration(message: String =
getString(R.string.registration_failed)){
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
loadingAnimation(false)
editText.error = message
wipeSharedSettings()
}
private fun wipeSharedSettings(){
preferences.edit().remove("domain").remove("clientId").remove("clientSecret")
.apply()
}
private fun loadingAnimation(on: Boolean){
if(on) {
domainTextInputLayout.visibility = View.GONE
login_activity_instance_input_layout.visibility = View.GONE
progressLayout.visibility = View.VISIBLE
}
else {
domainTextInputLayout.visibility = View.VISIBLE
login_activity_instance_input_layout.visibility = inputVisibility
progressLayout.visibility = View.GONE
}
}
private fun getInstanceConfig() {
// to get max post description length, can be enhanced for other things
// see /api/v1/instance
PixelfedAPI.create(preferences.getString("domain", "")!!)
.instance().enqueue(object : Callback<Instance> {
override fun onFailure(call: Call<Instance>, t: Throwable) {
Log.e(TAG, "Request to fetch instance config failed.")
preferences.edit().putInt("max_toot_chars", 500).apply()
}
override fun onResponse(call: Call<Instance>, response: Response<Instance>) {
if (response.code() == 200) {
preferences.edit().putInt(
"max_toot_chars",
response.body()!!.max_toot_chars.toInt()
).apply()
} else {
Log.e(TAG, "Server response to fetch instance config failed.")
preferences.edit().putInt("max_toot_chars", 500).apply()
private fun saveUserAndInstance(accessToken: String) {
pixelfedAPI.instance().enqueue(object : Callback<Instance> {
override fun onFailure(call: Call<Instance>, t: Throwable) {
return failedRegistration(getString(R.string.instance_error))
}
}
})
override fun onResponse(call: Call<Instance>, response: Response<Instance>) {
if (response.isSuccessful && response.body() != null) {
val instance = response.body() as Instance
storeInstance(db, instance)
storeUser(accessToken, instance.uri)
} else {
return failedRegistration(getString(R.string.instance_error))
}
}
})
}
private fun storeUser(accessToken: String, instance: String) {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.body() != null && response.isSuccessful) {
db.userDao().deActivateActiveUser()
val user = response.body() as Account
DBUtils.addUser(
db,
user,
instance,
activeUser = true,
accessToken = accessToken
)
db.close()
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
}
})
}
}

View File

@ -2,122 +2,264 @@ package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.NonNull
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout
import com.bumptech.glide.Glide
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.NewPostFragment
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.fragments.CameraFragment
import com.h.pixeldroid.fragments.SearchDiscoverFragment
import com.h.pixeldroid.fragments.feeds.PostsFeedFragment
import com.h.pixeldroid.fragments.feeds.NotificationsFragment
import com.h.pixeldroid.fragments.feeds.OfflineFeedFragment
import com.h.pixeldroid.fragments.feeds.PostsFeedFragment
import com.h.pixeldroid.fragments.feeds.PublicTimelineFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.Utils.Companion.hasInternet
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var drawerLayout: DrawerLayout
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private lateinit var preferences: SharedPreferences
private val searchDiscoverFragment: SearchDiscoverFragment = SearchDiscoverFragment()
private lateinit var db: AppDatabase
private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null
companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme_NoActionBar)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
preferences = getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
db = DBUtils.initDB(applicationContext)
//get the currently active user
user = db.userDao().getActiveUser()
//Check if we have logged in and gotten an access token
if(!preferences.contains("accessToken")){
launchActivity(LoginActivity())
if (user == null) {
launchActivity(LoginActivity(), firstTime = true)
} else {
setupDrawer()
val tabs = arrayOf(
PostsFeedFragment(),
if (hasInternet(applicationContext)) PostsFeedFragment()
else OfflineFeedFragment(),
searchDiscoverFragment,
NewPostFragment(),
CameraFragment(),
NotificationsFragment(),
PublicTimelineFragment()
)
setupTabs(tabs)
}
}
private fun setupDrawer() {
drawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.nav_view)
navigationView.setNavigationItemSelectedListener(this)
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
currentHiddenInList = true
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean ->
clickProfile(profile, current)
}
addProfile(ProfileSettingDrawerItem().apply {
identifier = ADD_ACCOUNT_IDENTIFIER
nameRes = R.string.add_account_name
descriptionRes = R.string.add_account_description
iconicsIcon = GoogleMaterial.Icon.gmd_add
}, 0)
attachToSliderView(drawer)
dividerBelowHeader = false
closeDrawerOnProfileListClick = true
}
// Setup views
val accessToken = preferences.getString("accessToken", "")
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(imageView.context)
.load(uri)
.placeholder(placeholder)
.into(imageView)
}
val drawerHeader = navigationView.getHeaderView(0)
val accountName = drawerHeader.findViewById<TextView>(R.id.drawer_account_name)
val avatar = drawerHeader.findViewById<ImageView>(R.id.drawer_avatar)
override fun cancel(imageView: ImageView) {
Glide.with(imageView.context).clear(imageView)
}
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
val account = response.body()!!
// Set profile picture
ImageConverter.setRoundImageFromURL(
View(applicationContext), account.avatar_static, avatar)
avatar.setOnClickListener{ launchActivity(ProfileActivity()) }
// Set account name
accountName.text = account.display_name
accountName.setOnClickListener{ launchActivity(ProfileActivity()) }
}
override fun placeholder(ctx: Context, tag: String?): Drawable {
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
return ctx.getDrawable(R.drawable.ic_default_user)!!
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("DRAWER ACCOUNT:", t.toString())
}
return super.placeholder(ctx, tag)
}
})
fillDrawerAccountInfo(user!!.user_id)
//after setting with the values in the db, we make sure to update the database and apply
//with the received one. This happens asynchronously.
getUpdatedAccount()
drawer.itemAdapter.add(
primaryDrawerItem {
nameRes = R.string.menu_account
iconicsIcon = GoogleMaterial.Icon.gmd_person
},
primaryDrawerItem {
nameRes = R.string.menu_settings
iconicsIcon = GoogleMaterial.Icon.gmd_settings
},
primaryDrawerItem {
nameRes = R.string.logout
iconicsIcon = GoogleMaterial.Icon.gmd_close
})
drawer.onDrawerItemClickListener = { v, drawerItem, position ->
when (position){
1 -> launchActivity(ProfileActivity())
2 -> launchActivity(SettingsActivity())
3 -> logOut()
}
false
}
}
private fun logOut(){
db.userDao().deleteActiveUsers()
val remainingUsers = db.userDao().getAll()
if (remainingUsers.isEmpty()){
//no more users, start first-time login flow
launchActivity(LoginActivity(), firstTime = true)
} else {
val newActive = remainingUsers.first()
db.userDao().activateUser(newActive.user_id)
//relaunch the app
launchActivity(MainActivity(), firstTime = true)
}
}
private fun getUpdatedAccount(){
if (hasInternet(applicationContext)) {
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
val pixelfedAPI = PixelfedAPI.create(domain)
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.body() != null && response.isSuccessful) {
val account = response.body() as Account
DBUtils.addUser(db, account, domain, accessToken = accessToken)
fillDrawerAccountInfo(account.id)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("DRAWER ACCOUNT:", t.toString())
}
})
}
}
//called when switching profiles, or when clicking on current profile
private fun clickProfile(profile: IProfile, current: Boolean): Boolean {
if(current){
launchActivity(ProfileActivity())
return false
}
//Clicked on add new account
if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){
launchActivity(LoginActivity())
return false
}
db.userDao().deActivateActiveUser()
db.userDao().activateUser(profile.identifier.toString())
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
return false
}
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
return PrimaryDrawerItem()
.apply {
isSelectable = false
isIconTinted = true
}
.apply(block)
}
private fun fillDrawerAccountInfo(account: String) {
val users = db.userDao().getAll().toMutableList()
users.sortWith(Comparator { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
}
})
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = "${user.username}@${user.instance_uri.removePrefix("https://")}"
}
}.toMutableList()
// reuse the already existing "add account" item
for (profile in header.profiles.orEmpty()) {
if (profile.identifier == ADD_ACCOUNT_IDENTIFIER) {
profiles.add(profile)
break
}
}
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
}
private fun setupTabs(tabs: Array<Fragment>){
viewPager = findViewById(R.id.view_pager)
viewPager.adapter = object : FragmentStateAdapter(this) {
private fun setupTabs(tab_array: Array<Fragment>){
view_pager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return tabs[position]
return tab_array[position]
}
override fun getItemCount(): Int {
return 5
}
}
tabLayout = findViewById(R.id.tabs)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
TabLayoutMediator(tabs, view_pager) { tab, position ->
when(position){
0 -> tab.icon = getDrawable(R.drawable.ic_home_white_24dp)
1 -> tab.icon = getDrawable(R.drawable.ic_search_white_24dp)
@ -128,26 +270,17 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}.attach()
}
/**
When clicking in the drawer menu, go to the corresponding activity
*/
override fun onNavigationItemSelected(@NonNull item: MenuItem): Boolean {
when (item.itemId){
R.id.nav_account -> launchActivity(ProfileActivity())
R.id.nav_settings -> launchActivity(SettingsActivity())
R.id.nav_logout -> launchActivity(LoginActivity())
}
drawerLayout.closeDrawer(GravityCompat.START)
return true
}
/**
Launches the given activity and put it as the current one
Setting argument firstTime to true means the task history will be reset (as if the app were launched anew into
this activity)
*/
private fun launchActivity(activity: AppCompatActivity) {
private fun launchActivity(activity: AppCompatActivity, firstTime: Boolean = false) {
val intent = Intent(this, activity::class.java)
if(firstTime){
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
startActivity(intent)
}
@ -155,8 +288,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
Closes the drawer if it is open, when we press the back button
*/
override fun onBackPressed() {
if(drawerLayout.isDrawerOpen(GravityCompat.START)){
drawerLayout.closeDrawer(GravityCompat.START)
if(drawer_layout.isDrawerOpen(GravityCompat.START)){
drawer_layout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}

View File

@ -1,16 +1,21 @@
package com.h.pixeldroid
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.EditPhotoViewPagerAdapter
@ -19,6 +24,7 @@ import com.h.pixeldroid.fragments.FilterListFragment
import com.h.pixeldroid.interfaces.EditImageFragmentListener
import com.h.pixeldroid.interfaces.FilterListFragmentListener
import com.h.pixeldroid.utils.NonSwipeableViewPager
import com.yalantis.ucrop.UCrop
import com.zomato.photofilters.imageprocessors.Filter
import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter
@ -29,6 +35,9 @@ import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors.newSingleThreadExecutor
import java.util.concurrent.Future
// This is an arbitrary number we are using to keep track of the permission
// request. Where an app has multiple context for requesting permission,
@ -40,12 +49,19 @@ private val REQUIRED_PERMISSIONS = arrayOf(android.Manifest.permission.READ_EXTE
class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditImageFragmentListener {
val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val BRIGHTNESS_START = 0
private val SATURATION_START = 1.0f
private val CONTRAST_START = 1.0f
private var originalImage: Bitmap? = null
private var compressedImage: Bitmap? = null
private var compressedOriginalImage: Bitmap? = null
private lateinit var filteredImage: Bitmap
private lateinit var finalImage: Bitmap
private var actualFilter: Filter? = null
private lateinit var filterListFragment: FilterListFragment
private lateinit var editImageFragment: EditImageFragment
@ -54,11 +70,12 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
lateinit var viewPager: NonSwipeableViewPager
lateinit var tabLayout: TabLayout
private var brightnessFinal = 0
private var saturationFinal = 1.0f
private var contrastFinal = 1.0f
private var brightnessFinal = BRIGHTNESS_START
private var saturationFinal = SATURATION_START
private var contrastFinal = CONTRAST_START
private var resultUri: Uri? = null
private var imageUri: Uri? = null
private var cropUri: Uri? = null
object URI {var picture_uri: Uri? = null}
@ -66,20 +83,35 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
System.loadLibrary("NativeImageProcessor")
}
companion object{
private var executor: ExecutorService = newSingleThreadExecutor()
private var future: Future<*>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photo_edit)
URI.picture_uri = intent.getParcelableExtra("uri")
resultUri = URI.picture_uri
//TODO move to xml:
setSupportActionBar(toolbar)
supportActionBar!!.title = "Edit"
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setHomeButtonEnabled(true)
val cropButton: FloatingActionButton = findViewById(R.id.cropImageButton)
cropUri = intent.getParcelableExtra("picture_uri")
// set on-click listener
cropButton.setOnClickListener {
startCrop()
}
loadImage()
val file = File.createTempFile("temp_compressed_img", ".png", cacheDir)
file.writeBitmap(compressedImage!!)
URI.picture_uri = Uri.fromFile(file)
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabs)
@ -88,21 +120,24 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
outputDirectory = getOutputDirectory()
}
/** Use external media if it is available, our app's file directory otherwise */
private fun getOutputDirectory(): File {
val appContext = applicationContext
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
//<editor-fold desc="ON LAUNCH">
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, cropUri)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(compressedImage)
}
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, URI.picture_uri)
private fun resizeImage(image: Bitmap): Bitmap {
val display = windowManager.defaultDisplay
val size = Point()
display.getSize(size)
filteredImage = originalImage!!.copy(BITMAP_CONFIG, true)
finalImage = originalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(originalImage)
val newY = size.y * 0.7
val scale = newY / image.height
return Bitmap.createScaledBitmap(image, (image.width * scale).toInt(), newY.toInt(), true)
}
private fun setupViewPager(viewPager: NonSwipeableViewPager?) {
@ -142,6 +177,139 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
return super.onOptionsItemSelected(item)
}
//</editor-fold>
//<editor-fold desc="FILTERS">
override fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(filter.processFilter(filteredImage))
compressedImage = filteredImage.copy(BITMAP_CONFIG, true)
actualFilter = filter
}
private fun resetControls() {
editImageFragment.resetControl()
brightnessFinal = BRIGHTNESS_START
saturationFinal = SATURATION_START
contrastFinal = CONTRAST_START
}
//</editor-fold>
//<editor-fold desc="EDITS">
private fun applyFilterAndShowImage(filter: Filter, image: Bitmap?) {
future?.cancel(true)
future = executor.submit {
val bitmap = filter.processFilter(image!!.copy(BITMAP_CONFIG, true))
image_preview.post {
image_preview.setImageBitmap(bitmap)
}
}
}
override fun onBrightnessChange(brightness: Int) {
brightnessFinal = brightness
val myFilter = Filter()
myFilter.addEditFilters(brightness, saturationFinal, contrastFinal)
applyFilterAndShowImage(myFilter, filteredImage)
}
override fun onSaturationChange(saturation: Float) {
saturationFinal = saturation
val myFilter = Filter()
myFilter.addEditFilters(brightnessFinal, saturation, contrastFinal)
applyFilterAndShowImage(myFilter, filteredImage)
}
override fun onContrastChange(contrast: Float) {
contrastFinal = contrast
val myFilter = Filter()
myFilter.addEditFilters(brightnessFinal, saturationFinal, contrast)
applyFilterAndShowImage(myFilter, filteredImage)
}
private fun Filter.addEditFilters(br: Int, sa: Float, co: Float): Filter {
addSubFilter(BrightnessSubFilter(br))
addSubFilter(ContrastSubFilter(co))
addSubFilter(SaturationSubfilter(sa))
return this
}
override fun onEditStarted() {
}
override fun onEditCompleted() {
val myFilter = Filter()
myFilter.addEditFilters(brightnessFinal, saturationFinal, contrastFinal)
val bitmap = filteredImage.copy(BITMAP_CONFIG, true)
compressedImage = myFilter.processFilter(bitmap)
}
//</editor-fold>
//<editor-fold desc="CROPPING">
private fun startCrop() {
applyFinalFilters(MediaStore.Images.Media.getBitmap(contentResolver, cropUri))
val file = File.createTempFile("temp_crop_img", ".png", cacheDir)
file.writeBitmap(finalImage)
val uCrop: UCrop = UCrop.of(Uri.fromFile(file), URI.picture_uri!!)
uCrop.start(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK) {
imageUri = data!!.data
if (requestCode == UCrop.RESULT_ERROR) {
handleCropError(data)
} else {
handleCropResult(data)
}
}
}
private fun resetFilteredImage(){
val newBr = if(brightnessFinal != 0) BRIGHTNESS_START/brightnessFinal else 0
val newSa = if(saturationFinal != 0.0f) SATURATION_START/saturationFinal else 0.0f
val newCo = if(contrastFinal != 0.0f) CONTRAST_START/contrastFinal else 0.0f
val myFilter = Filter().addEditFilters(newBr, newSa, newCo)
filteredImage = myFilter.processFilter(filteredImage)
}
private fun handleCropResult(data: Intent?) {
val resultCrop: Uri? = UCrop.getOutput(data!!)
if(resultCrop != null) {
image_preview.setImageURI(resultCrop)
val bitmap = (image_preview.drawable as BitmapDrawable).bitmap
originalImage = bitmap.copy(Bitmap.Config.ARGB_8888, true)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
resetFilteredImage()
} else {
Toast.makeText(this, "Cannot retrieve image", Toast.LENGTH_SHORT).show()
}
}
private fun handleCropError(data: Intent?) {
val resultError = UCrop.getError(data!!)
if(resultError != null) {
Toast.makeText(this, "" + resultError, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Unexpected Error", Toast.LENGTH_SHORT).show()
}
}
//</editor-fold>
//<editor-fold desc="FLOW">
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@ -156,14 +324,21 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
REQUEST_CODE_PERMISSIONS_SEND_PHOTO -> permissionsGrantedToSave(false)
}
} else {
Snackbar.make(coordinator_edit, "Permission denied", Snackbar.LENGTH_LONG).show()
Snackbar.make(coordinator_edit, getString(R.string.permission_denied),
Snackbar.LENGTH_LONG).show()
}
}
private fun applyFinalFilters(image: Bitmap?) {
val editFilter = Filter().addEditFilters(brightnessFinal, saturationFinal, contrastFinal)
finalImage = editFilter.processFilter(image!!.copy(BITMAP_CONFIG, true))
if (actualFilter!=null) finalImage = actualFilter!!.processFilter(finalImage)
}
private fun uploadImage(file: File) {
val intent = Intent (applicationContext, PostCreationActivity::class.java)
intent.putExtra("picture_uri", Uri.fromFile(file))
//file.delete()
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
applicationContext!!.startActivity(intent)
}
@ -189,6 +364,15 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
applicationContext, it) == PackageManager.PERMISSION_GRANTED
}
/** Use external media if it is available, our app's file directory otherwise */
private fun getOutputDirectory(): File {
val appContext = applicationContext
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
private fun File.writeBitmap(bitmap: Bitmap) {
outputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 85, out)
@ -197,74 +381,29 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
}
private fun permissionsGrantedToSave(save: Boolean) {
val file = if(!save){
//put picture in cache
File.createTempFile("temp_img", ".png", cacheDir)
} else{
// Save the picture (quality is ignored for PNG)
File(outputDirectory, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
val file =
if(!save){
//put picture in cache
File.createTempFile("temp_edit_img", ".png", cacheDir)
} else{
// Save the picture (quality is ignored for PNG)
File(outputDirectory, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png")
}
}
try {
applyFinalFilters(originalImage)
file.writeBitmap(finalImage)
} catch (e: IOException) {
Snackbar.make(coordinator_edit, "Unable to save image", Snackbar.LENGTH_LONG).show()
Snackbar.make(coordinator_edit, getString(R.string.save_image_failed),
Snackbar.LENGTH_LONG).show()
}
if (!save) {
uploadImage(file)
} else {
Snackbar.make(coordinator_edit, "Image succesfully saved", Snackbar.LENGTH_LONG).show()
Snackbar.make(coordinator_edit, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG).show()
}
}
override fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = originalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(filter.processFilter(filteredImage))
finalImage = filteredImage.copy(BITMAP_CONFIG, true)
}
private fun resetControls() {
editImageFragment.resetControl()
brightnessFinal = 0
saturationFinal = 1.0f
contrastFinal = 1.0f
}
override fun onBrightnessChange(brightness: Int) {
brightnessFinal = brightness
val myFilter = Filter()
myFilter.addSubFilter(BrightnessSubFilter(brightness))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onSaturationChange(saturation: Float) {
saturationFinal = saturation
val myFilter = Filter()
myFilter.addSubFilter(SaturationSubfilter(saturation))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onContrastChange(contrast: Float) {
contrastFinal = contrast
val myFilter = Filter()
myFilter.addSubFilter(ContrastSubFilter(contrast))
image_preview.setImageBitmap(myFilter.processFilter(finalImage.copy(BITMAP_CONFIG, true)))
}
override fun onEditStarted() {
}
override fun onEditCompleted() {
val bitmap = filteredImage.copy(BITMAP_CONFIG, true)
val myFilter = Filter()
myFilter.addSubFilter(ContrastSubFilter(contrastFinal))
myFilter.addSubFilter(SaturationSubfilter(saturationFinal))
myFilter.addSubFilter(BrightnessSubFilter(brightnessFinal))
finalImage = myFilter.processFilter(bitmap)
}
//</editor-fold>
}

View File

@ -0,0 +1,14 @@
package com.h.pixeldroid
import android.app.Application
import androidx.preference.PreferenceManager
import com.h.pixeldroid.utils.ThemeUtils
class Pixeldroid: Application() {
override fun onCreate() {
super.onCreate()
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this)
ThemeUtils.setThemeFromPreferences(sharedPreferences, resources)
}
}

View File

@ -1,7 +1,5 @@
package com.h.pixeldroid
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.View
@ -13,15 +11,16 @@ import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.objects.Status.Companion.DISCOVER_TAG
import com.h.pixeldroid.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.activity_post.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class PostActivity : AppCompatActivity() {
private lateinit var preferences: SharedPreferences
lateinit var postFragment : PostFragment
private lateinit var postFragment : PostFragment
lateinit var domain : String
private lateinit var accessToken : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -29,11 +28,13 @@ class PostActivity : AppCompatActivity() {
val status = intent.getSerializableExtra(POST_TAG) as Status?
val discoverPost: DiscoverPost? = intent.getSerializableExtra(DISCOVER_TAG) as DiscoverPost?
val db = DBUtils.initDB(applicationContext)
preferences = getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
domain = preferences.getString("domain", "")!!
val user = db.userDao().getActiveUser()
domain = user?.instance_uri.orEmpty()
accessToken = user?.accessToken.orEmpty()
db.close()
postFragment = PostFragment()
val arguments = Bundle()
@ -52,7 +53,6 @@ class PostActivity : AppCompatActivity() {
discoverPost: DiscoverPost
) {
val api = PixelfedAPI.create(domain)
val accessToken = preferences.getString("accessToken", "") ?: ""
val id = discoverPost.url?.substringAfterLast('/') ?: ""
api.getStatus("Bearer $accessToken", id).enqueue(object : Callback<Status> {

View File

@ -1,8 +1,6 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
@ -16,8 +14,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import com.google.android.material.textfield.TextInputEditText
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.Attachment
import com.h.pixeldroid.objects.Instance
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
@ -38,26 +39,43 @@ class PostCreationActivity : AppCompatActivity() {
private lateinit var accessToken: String
private lateinit var pixelfedAPI: PixelfedAPI
private lateinit var preferences: SharedPreferences
private lateinit var pictureFrame: ImageView
private lateinit var image: File
private var user: UserDatabaseEntity? = null
private var maxLength: Int = Instance.DEFAULT_MAX_TOOT_CHARS
private var description: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_creation)
val imageUri: Uri = intent.getParcelableExtra<Uri>("picture_uri")!!
val imageUri: Uri = intent.getParcelableExtra("picture_uri")!!
saveImage(imageUri)
pictureFrame = findViewById<ImageView>(R.id.post_creation_picture_frame)
pictureFrame = findViewById(R.id.post_creation_picture_frame)
pictureFrame.setImageURI(image.toUri())
preferences = getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "")!!
val db = DBUtils.initDB(applicationContext)
user = db.userDao().getActiveUser()
val instances = db.instanceDao().getAll()
db.close()
maxLength = if (user!=null){
val thisInstances =
instances.filter { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(user!!.instance_uri)
}
thisInstances.first().max_toot_chars
} else {
Instance.DEFAULT_MAX_TOOT_CHARS
}
val domain = user?.instance_uri.orEmpty()
accessToken = user?.accessToken.orEmpty()
pixelfedAPI = PixelfedAPI.create(domain)
// check if the picture is alright
// TODO
@ -97,10 +115,10 @@ class PostCreationActivity : AppCompatActivity() {
private fun setDescription(): Boolean {
val textField = findViewById<TextInputEditText>(R.id.new_post_description_input_field)
val content = textField.text.toString()
val maxLength = preferences.getInt("max_toot_chars", 500)
if (content.length > maxLength) {
// error, too much characters
textField.error = "Description must contain $maxLength characters at most."
textField.error = getString(R.string.description_max_characters).format(maxLength)
return false
}
// store the description
@ -115,7 +133,8 @@ class PostCreationActivity : AppCompatActivity() {
Callback<Attachment> {
override fun onFailure(call: Call<Attachment>, t: Throwable) {
Log.e(TAG, t.toString() + call.request())
Toast.makeText(applicationContext,"Picture upload error!",Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext,getString(R.string.upload_picture_failed),
Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<Attachment>, response: Response<Attachment>) {
@ -124,10 +143,14 @@ class PostCreationActivity : AppCompatActivity() {
if (body.type.name == "image") {
post(body.id)
} else
Toast.makeText(applicationContext, "Upload error: wrong picture format.", Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext, getString(R.string.picture_format_error),
Toast.LENGTH_SHORT).show()
} else {
Log.e(TAG, "Server responded: $response" + call.request() + call.request().body)
Toast.makeText(applicationContext,"Upload error: bad request format",Toast.LENGTH_SHORT).show()
Log.e(TAG,
"Server responded: $response${call.request()}${call.request().body}"
)
Toast.makeText(applicationContext,getString(R.string.request_format_error),
Toast.LENGTH_SHORT).show()
}
}
})
@ -141,16 +164,19 @@ class PostCreationActivity : AppCompatActivity() {
media_ids = listOf(id)
).enqueue(object: Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Toast.makeText(applicationContext,"Post upload failed",Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext,getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, t.message + call.request())
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if (response.code() == 200) {
Toast.makeText(applicationContext,"Post upload success",Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext,getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
startActivity(Intent(applicationContext, MainActivity::class.java))
} else {
Toast.makeText(applicationContext,"Post upload failed : not 200",Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext,getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, call.request().toString() + response.raw().toString())
}
}

View File

@ -1,8 +1,6 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
@ -24,25 +22,31 @@ import com.h.pixeldroid.fragments.ProfilePostFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.objects.Relationship
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.ImageConverter
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ProfileActivity : AppCompatActivity() {
private lateinit var preferences : SharedPreferences
private lateinit var pixelfedAPI : PixelfedAPI
private var accessToken : String? = null
private var account: Account? = null
private lateinit var domain : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
preferences = this.getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE)
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "")
val db = DBUtils.initDB(applicationContext)
val user = db.userDao().getActiveUser()
domain = user?.instance_uri.orEmpty()
pixelfedAPI = PixelfedAPI.create(domain)
accessToken = user?.accessToken.orEmpty()
db.close()
setContent()
}
@ -97,17 +101,20 @@ class ProfileActivity : AppCompatActivity() {
accountName.setTypeface(null, Typeface.BOLD)
val nbPosts = findViewById<TextView>(R.id.nbPostsTextView)
nbPosts.text = "${account!!.statuses_count}\nPosts"
nbPosts.text = applicationContext.getString(R.string.nb_posts)
.format(account!!.statuses_count.toString())
nbPosts.setTypeface(null, Typeface.BOLD)
val nbFollowers = findViewById<TextView>(R.id.nbFollowersTextView)
nbFollowers.text = "${account!!.followers_count}\nFollowers"
nbFollowers.text = applicationContext.getString(R.string.nb_followers)
.format(account!!.followers_count.toString())
nbFollowers.setTypeface(null, Typeface.BOLD)
// On click open followers list
nbFollowers.setOnClickListener{ onClickFollowers() }
val nbFollowing = findViewById<TextView>(R.id.nbFollowingTextView)
nbFollowing.text = "${account!!.following_count}\nFollowing"
nbFollowing.text = applicationContext.getString(R.string.nb_following)
.format(account!!.following_count.toString())
nbFollowing.setTypeface(null, Typeface.BOLD)
// On click open followers list
nbFollowing.setOnClickListener{ onClickFollowing() }
@ -138,14 +145,13 @@ class ProfileActivity : AppCompatActivity() {
}
private fun onClickEditButton() {
val url = "${preferences.getString("domain", "")}/settings/home"
val url = "$domain/settings/home"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if(browserIntent.resolveActivity(packageManager) != null) {
startActivity(browserIntent)
} else {
val text = "Cannot open this link"
Log.e("ProfileActivity", text)
Log.e("ProfileActivity", "Cannot open this link")
}
}
@ -175,7 +181,8 @@ class ProfileActivity : AppCompatActivity() {
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(applicationContext,"Could not get follow status", Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext,getString(R.string.follow_status_failed),
Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<List<Relationship>>, response: Response<List<Relationship>>) {
@ -193,8 +200,8 @@ class ProfileActivity : AppCompatActivity() {
followButton.visibility = View.VISIBLE
}
} else {
Toast.makeText(applicationContext, "Could not display follow button", Toast.LENGTH_SHORT)
.show()
Toast.makeText(applicationContext, getString(R.string.follow_button_failed),
Toast.LENGTH_SHORT).show()
}
}
})
@ -209,8 +216,8 @@ class ProfileActivity : AppCompatActivity() {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(applicationContext, "Could not follow", Toast.LENGTH_SHORT)
.show()
Toast.makeText(applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT).show()
}
override fun onResponse(
@ -221,8 +228,8 @@ class ProfileActivity : AppCompatActivity() {
followButton.text = "Unfollow"
setOnClickUnfollow()
} else if (response.code() == 403) {
Toast.makeText(applicationContext, "This action is not allowed", Toast.LENGTH_SHORT)
.show()
Toast.makeText(applicationContext, getString(R.string.action_not_allowed),
Toast.LENGTH_SHORT).show()
}
}
})
@ -238,8 +245,8 @@ class ProfileActivity : AppCompatActivity() {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("UNFOLLOW ERROR", t.toString())
Toast.makeText(applicationContext, "Could not unfollow", Toast.LENGTH_SHORT)
.show()
Toast.makeText(applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
@ -247,8 +254,8 @@ class ProfileActivity : AppCompatActivity() {
followButton.text = "Follow"
setOnClickFollow()
} else if (response.code() == 401) {
Toast.makeText(applicationContext, "The access token is invalid", Toast.LENGTH_SHORT)
.show()
Toast.makeText(applicationContext, getString(R.string.access_token_invalid),
Toast.LENGTH_SHORT).show()
}
}
})

View File

@ -2,7 +2,6 @@ package com.h.pixeldroid
import android.app.SearchManager
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
@ -17,7 +16,6 @@ import com.h.pixeldroid.fragments.feeds.search.SearchPostsFragment
import com.h.pixeldroid.objects.Results
class SearchActivity : AppCompatActivity() {
private lateinit var preferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -26,11 +24,15 @@ class SearchActivity : AppCompatActivity() {
var query = intent.getSerializableExtra("searchFeed") as String
query = query.trim()
val searchType = if (query.startsWith("#")){
Results.SearchType.hashtags
} else if(query.startsWith("@")){
Results.SearchType.accounts
} else Results.SearchType.statuses
val searchType = when {
query.startsWith("#") -> {
Results.SearchType.hashtags
}
query.startsWith("@") -> {
Results.SearchType.accounts
}
else -> Results.SearchType.statuses
}
if(searchType != Results.SearchType.statuses) query = query.drop(1)
@ -75,9 +77,9 @@ class SearchActivity : AppCompatActivity() {
val tabLayout = findViewById<TabLayout>(R.id.search_tabs)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when(position){
0 -> tab.text = "POSTS"
1 -> tab.text = "ACCOUNTS"
2 -> tab.text = "HASHTAGS"
0 -> tab.text = getString(R.string.posts)
1 -> tab.text = getString(R.string.accounts)
2 -> tab.text = getString(R.string.hashtags)
}
}.attach()
when(searchType){

View File

@ -1,12 +1,19 @@
package com.h.pixeldroid
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.h.pixeldroid.utils.ThemeUtils.Companion.setThemeFromPreferences
class SettingsActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private var restartActivitiesOnExit = false
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings)
supportFragmentManager
.beginTransaction()
@ -15,7 +22,31 @@ class SettingsActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment : PreferenceFragmentCompat() {
private fun restartCurrentActivity() {
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
super.startActivity(intent)
}
override fun onResume() {
super.onResume()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"theme" -> setThemeFromPreferences(sharedPreferences, resources)
}
restartActivitiesOnExit = true
restartCurrentActivity()
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
}

View File

@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
class EditPhotoViewPagerAdapter (manager: FragmentManager):
FragmentPagerAdapter(manager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val fragmentList = ArrayList<Fragment>()
private val fragmentTitleList = ArrayList<String>()

View File

@ -13,8 +13,7 @@ import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.ImageConverter.Companion.setSquareImageFromURL
/**
* [RecyclerView.Adapter] that can display a list of [PostMiniature]s and makes a call to the
* specified [OnListFragmentInteractionListener].
* [RecyclerView.Adapter] that can display a list of [Status]s
*/
class ProfilePostsRecyclerViewAdapter(
private val context: Context
@ -29,7 +28,11 @@ class ProfilePostsRecyclerViewAdapter(
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)

View File

@ -46,12 +46,7 @@ class ThumbnailAdapter (private val context: Context,
}
class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var thumbnail: ImageView
var filterName: TextView
init {
thumbnail = itemView.thumbnail
filterName = itemView.filter_name
}
var thumbnail: ImageView = itemView.thumbnail
var filterName: TextView = itemView.filter_name
}
}

View File

@ -1,39 +1,10 @@
package com.h.pixeldroid.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(entities = [PostEntity::class], version = 1)
@TypeConverters(Converters::class)
@Database(entities = [InstanceDatabaseEntity::class, UserDatabaseEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun postDao(): PostDao
val MAX_NUMBER_OF_POSTS = 100
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: AppDatabase? = null
var TEST_MODE = false
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
// To be able to create a temporary database that flushes when tests are over
var instance = if (TEST_MODE) {
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).allowMainThreadQueries().build()
} else {
Room.databaseBuilder(
context.applicationContext, AppDatabase::class.java, "posts_database"
).build()
}
INSTANCE = instance
return instance
}
}
}
abstract fun instanceDao(): InstanceDao
abstract fun userDao(): UserDao
}

View File

@ -1,16 +0,0 @@
package com.h.pixeldroid.db
import androidx.room.TypeConverter
import java.util.Date
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time?.toLong()
}
}

View File

@ -0,0 +1,15 @@
package com.h.pixeldroid.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface InstanceDao {
@Query("SELECT * FROM instances")
fun getAll(): List<InstanceDatabaseEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertInstance(instance: InstanceDatabaseEntity)
}

View File

@ -0,0 +1,13 @@
package com.h.pixeldroid.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.h.pixeldroid.objects.Instance
@Entity(tableName = "instances")
data class InstanceDatabaseEntity (
@PrimaryKey var uri: String,
var title: String = "",
var max_toot_chars: Int = Instance.DEFAULT_MAX_TOOT_CHARS,
var thumbnail: String = ""
)

View File

@ -1,36 +0,0 @@
package com.h.pixeldroid.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import java.util.Date
@Dao
interface PostDao {
@Query("SELECT * FROM posts")
fun getAll(): LiveData<List<PostEntity>>
@Query("SELECT * FROM posts WHERE uid = :postId")
fun getById(postId: Int): PostEntity
@Query("SELECT count(*) FROM posts")
fun getPostsCount(): Int
@Query("UPDATE posts SET date = :date WHERE uid = :postId")
fun addDateToPost(postId: Int, date: Date)
@Query("DELETE FROM posts")
fun deleteAll()
@Query("DELETE FROM posts WHERE date IN (SELECT min(date) FROM posts) ")
fun deleteOldestPost(): Int
@Insert(onConflict = REPLACE)
fun insertAll(vararg posts: PostEntity)
@Delete
fun delete(post: PostEntity)
}

View File

@ -1,17 +0,0 @@
package com.h.pixeldroid.db
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import java.util.Date
@Entity(tableName= "posts")
data class PostEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "domain") val domain: String? = "",
@ColumnInfo(name = "username") val username: String? = "",
@ColumnInfo(name = "display name") val displayName: String? = "",
@ColumnInfo(name = "accountID") val accountID: Int? = -1,
@ColumnInfo(name = "image url") val ImageURL: String? = "",
@ColumnInfo(name = "date") val date: Date?
)

View File

@ -0,0 +1,30 @@
package com.h.pixeldroid.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: UserDatabaseEntity)
@Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity>
@Query("SELECT * FROM users WHERE isActive=1 LIMIT 1")
fun getActiveUser(): UserDatabaseEntity?
@Query("UPDATE users SET isActive=0 WHERE isActive=1")
fun deActivateActiveUser()
@Query("UPDATE users SET isActive=1 WHERE user_id=:id")
fun activateUser(id: String)
@Query("DELETE FROM users WHERE isActive=1")
fun deleteActiveUsers()
@Query("SELECT * FROM users WHERE user_id=:id LIMIT 1")
fun getUserWithId(id: String): UserDatabaseEntity
}

View File

@ -0,0 +1,25 @@
package com.h.pixeldroid.db
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "users",
primaryKeys = ["user_id", "instance_uri"],
foreignKeys = [ForeignKey(
entity = InstanceDatabaseEntity::class,
parentColumns = arrayOf("uri"),
childColumns = arrayOf("instance_uri"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)
data class UserDatabaseEntity (
var user_id: String,
var instance_uri: String,
var username: String,
var display_name: String,
var avatar_static: String,
var isActive: Boolean,
var accessToken: String
)

View File

@ -0,0 +1,399 @@
package com.h.pixeldroid.fragments
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.camera.core.*
import androidx.camera.core.ImageCapture.Metadata
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.h.pixeldroid.PhotoEditActivity
import com.h.pixeldroid.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
// This is an arbitrary number we are using to keep track of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts.
private const val REQUEST_CODE_PERMISSIONS = 10
private const val ANIMATION_FAST_MILLIS = 50L
private const val ANIMATION_SLOW_MILLIS = 100L
/**
* Camera fragment
*/
class CameraFragment : Fragment() {
private lateinit var container: ConstraintLayout
private lateinit var viewFinder: PreviewView
private lateinit var outputDirectory: File
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)
private val PICK_IMAGE_REQUEST = 1
private val CAPTURE_IMAGE_REQUEST = 2
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var camera: Camera? = null
/** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
override fun onResume() {
super.onResume()
// Make sure that all permissions are still present on resume,
// since they could have been removed while away.
if (!allPermissionsGranted()) {
ActivityCompat.requestPermissions(
requireActivity(),
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
} else {
//Bind the viewfinder here, since when leaving the fragment it gets unbound
bindCameraUseCases()
// Build UI controls
updateCameraUi()
}
}
/**
* Check if all permission specified in the manifest have been granted
*/
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroyView() {
super.onDestroyView()
// Shut down our background executor
cameraExecutor.shutdown()
// Unregister the broadcast receivers and listeners
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_camera, container, false)
private fun setGalleryThumbnail(uri: String) {
// Reference of the view that holds the gallery thumbnail
val thumbnail = container.findViewById<ImageButton>(R.id.photo_view_button)
// Run the operations in the view's thread
thumbnail.post {
// Remove thumbnail padding
thumbnail.setPadding(10)
// Load thumbnail into circular button using Glide
Glide.with(thumbnail)
.load(uri)
.apply(RequestOptions.circleCropTransform())
.into(thumbnail)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container = view as ConstraintLayout
viewFinder = container.findViewById(R.id.view_finder)
// Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor()
// Every time the orientation of device changes, update rotation for use cases
// Determine the output directory
outputDirectory = getGalleryDirectory(requireContext())
// Wait for the views to be properly laid out
viewFinder.post {
// Keep track of the display in which this view is attached
displayId = viewFinder.display?.displayId ?: -1
}
}
/**
* Inflate camera controls and update the UI manually upon config changes to avoid removing
* and re-adding the view finder from the view hierarchy; this provides a seamless rotation
* transition on devices that support it.
*
* NOTE: The flag is supported starting in Android 8 but there still is a small flash on the
* screen for devices that run Android 9 or below.
*/
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateCameraUi()
}
/** Declare and bind preview, capture and analysis use cases */
private fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = viewFinder.display.rotation
// Bind the CameraProvider to the LifeCycleOwner
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
// CameraProvider
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
preview = Preview.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation
.setTargetRotation(rotation)
.build()
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
// We request aspect ratio but no resolution to match preview config, but letting
// CameraX optimize for whatever specific resolution best fits our use cases
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
// Must unbind the use-cases before rebinding them
cameraProvider.unbindAll()
try {
// A variable number of use-cases can be passed here -
// camera provides access to CameraControl & CameraInfo
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(requireContext()))
}
/**
* setTargetAspectRatio requires enum value of
* [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
*
* Detecting the most suitable ratio for dimensions provided in @params by counting absolute
* of preview ratio to one of the provided values.
*
* @param width - preview width
* @param height - preview height
* @return suitable aspect ratio
*/
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
/** Method used to re-draw the camera UI controls, called every time configuration changes. */
private fun updateCameraUi() {
// Remove previous UI if any
container.findViewById<ConstraintLayout>(R.id.camera_ui_container)?.let {
container.removeView(it)
}
// Inflate a new view containing all UI for controlling the camera
val controls = View.inflate(requireContext(), R.layout.camera_ui_container, container)
// In the background, load latest photo taken (if any) for gallery thumbnail
lifecycleScope.launch(Dispatchers.IO) {
// Find the last picture
// Find the last picture
val projection = arrayOf(
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.MIME_TYPE
)
val cursor = requireContext().contentResolver
.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null,
null, MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"
)
if (cursor != null && cursor.moveToFirst()) {
val uri = Uri.parse(cursor.getString(1)).path ?: ""
setGalleryThumbnail(uri)
cursor.close()
}
}
setupImageCapture(controls)
setupFlipCameras(controls)
setupUploadImage(controls)
}
private fun setupUploadImage(controls: View) {
// Listener for button used to view the most recent photo
controls.findViewById<ImageButton>(R.id.photo_view_button).setOnClickListener {
Intent().apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
startActivityForResult(
Intent.createChooser(this, "Select a Picture"), PICK_IMAGE_REQUEST
)
}
}
}
private fun setupFlipCameras(controls: View) {
// Listener for button used to switch cameras
controls.findViewById<ImageButton>(R.id.camera_switch_button).setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK
} else {
CameraSelector.LENS_FACING_FRONT
}
// Re-bind use cases to update selected camera, being careful about permissions.
if (!allPermissionsGranted()) {
ActivityCompat.requestPermissions(
requireActivity(),
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
} else {
bindCameraUseCases()
}
}
}
private fun setupImageCapture(controls: View) {
// Listener for button used to capture photo
controls.findViewById<ImageButton>(R.id.camera_capture_button).setOnClickListener {
// Get a stable reference of the modifiable image capture use case
imageCapture?.let { imageCapture ->
// Create output file to hold the image
val photoFile = File.createTempFile(
"${System.currentTimeMillis()}.jpg", null, context?.cacheDir
)
// Setup image capture metadata
val metadata = Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.build()
// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
startPostCreation(savedUri)
}
})
// Display flash animation to indicate that photo was captured
container.postDelayed({
container.foreground = ColorDrawable(Color.WHITE)
container.postDelayed(
{ container.foreground = null }, ANIMATION_FAST_MILLIS
)
}, ANIMATION_SLOW_MILLIS)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && data != null
&& (requestCode == PICK_IMAGE_REQUEST || requestCode == CAPTURE_IMAGE_REQUEST)
&& data.data != null) {
startPostCreation(data.data!!)
}
}
private fun startPostCreation(uri: Uri) {
startActivity(
Intent(activity, PhotoEditActivity::class.java)
.putExtra("picture_uri", uri)
)
}
companion object {
private const val TAG = "CameraFragment"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
/** Use external media if it is available, our app's file directory otherwise */
private fun getGalleryDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
}
}

View File

@ -17,9 +17,12 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private lateinit var seekbarSaturation: SeekBar
private lateinit var seekbarContrast: SeekBar
private var BRIGHTNESS_START = 100
private var SATURATION_START = 0
private var CONTRAST_START = 10
private var BRIGHTNESS_MAX = 200
private var SATURATION_MAX = 20
private var CONTRAST_MAX= 30
private var BRIGHTNESS_START = BRIGHTNESS_MAX/2
private var SATURATION_START = SATURATION_MAX/2
private var CONTRAST_START = CONTRAST_MAX/2
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@ -32,13 +35,13 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
seekbarSaturation = view.findViewById(R.id.seekbar_saturation)
seekbarContrast = view.findViewById(R.id.seekbar_contrast)
seekbarBrightness.max = 200
seekbarBrightness.max = BRIGHTNESS_MAX
seekbarBrightness.progress = BRIGHTNESS_START
seekbarContrast.max = 20
seekbarContrast.max = CONTRAST_MAX
seekbarContrast.progress = CONTRAST_START
seekbarSaturation.max = 30
seekbarSaturation.max = SATURATION_MAX
seekbarSaturation.progress = SATURATION_START
seekbarBrightness.setOnSeekBarChangeListener(this)

View File

@ -53,15 +53,12 @@ class FilterListFragment : Fragment(), FilterListFragmentListener {
fun displayImage(bitmap: Bitmap?) {
val r = Runnable {
val tbImage: Bitmap?
if (bitmap == null) {
tbImage = MediaStore.Images.Media.getBitmap(requireActivity().contentResolver, PhotoEditActivity.URI.picture_uri)
val tbImage: Bitmap = (if (bitmap == null) {
MediaStore.Images.Media.getBitmap(requireActivity().contentResolver, PhotoEditActivity.URI.picture_uri)
} else {
tbImage = Bitmap.createScaledBitmap(bitmap, 100, 100, false)
}
if (tbImage == null)
return@Runnable
Bitmap.createScaledBitmap(bitmap, 100, 100, false)
})
?: return@Runnable
setupFilter(tbImage)
@ -78,7 +75,7 @@ class FilterListFragment : Fragment(), FilterListFragmentListener {
val tbItem = ThumbnailItem()
tbItem.image = tbImage
tbItem.filterName = "Normal"
tbItem.filterName = getString(R.string.normal_filter)
ThumbnailsManager.addThumb(tbItem)
val filters = FilterPack.getFilterPack(requireActivity())

View File

@ -1,28 +1,23 @@
package com.h.pixeldroid
package com.h.pixeldroid.fragments
import android.Manifest
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
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 android.widget.Toast
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.ImageUtils
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 java.io.Serializable
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val IMG_URL = "imgurl"
@ -58,7 +53,9 @@ class ImageFragment : Fragment() {
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object: BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(view.context, "You need to grant write permission to download pictures!", Toast.LENGTH_SHORT).show()
Toast.makeText(view.context,
view.context.getString(R.string.write_permission_download_pic),
Toast.LENGTH_SHORT).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
@ -72,7 +69,9 @@ class ImageFragment : Fragment() {
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object: BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(view.context, "You need to grant write permission to share pictures!", Toast.LENGTH_SHORT).show()
Toast.makeText(view.context,
view.context.getString(R.string.write_permission_share_pic),
Toast.LENGTH_SHORT).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
@ -96,13 +95,11 @@ class ImageFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Load the image into to view
val imageView : ImageView = view.findViewById(R.id.imageImageView)!!
val picRequest = Glide.with(this)
Glide.with(this)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
picRequest.load(imgUrl).into(imageView)
.load(imgUrl)
.into(view.findViewById(R.id.imageImageView)!!)
}
companion object {

View File

@ -1,67 +0,0 @@
package com.h.pixeldroid.fragments
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import com.h.pixeldroid.PhotoEditActivity
import com.h.pixeldroid.R
/**
* This fragment is the entry point to create a post.
* You can either upload an existing picture or take a new one.
* once the URI of the picture to be posted is set, it will send
* it to the post creation activity where you can modify it,
* add a description and more.
*/
class NewPostFragment : Fragment() {
private val PICK_IMAGE_REQUEST = 1
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_new_post, container, false)
val uploadPictureButton: Button = view.findViewById(R.id.uploadPictureButton)
uploadPictureButton.setOnClickListener{
uploadPicture()
}
val takePictureButton: Button = view.findViewById(R.id.takePictureButton)
takePictureButton.setOnClickListener{
val uri: Uri = Uri.parse("android.resource://com.h.pixeldroid/drawable/index")
val intent = Intent(context, PhotoEditActivity::class.java).putExtra("uri", uri)
startActivity(intent)
}
return view
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && data != null
&& requestCode == PICK_IMAGE_REQUEST && data.data != null)
startActivity(Intent(activity, PhotoEditActivity::class.java)
.putExtra("uri", data.data)
)
}
private fun uploadPicture() {
Intent().apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
startActivityForResult(
Intent.createChooser(this, "Select a Picture"), PICK_IMAGE_REQUEST
)
}
}
}

View File

@ -1,6 +1,5 @@
package com.h.pixeldroid.fragments
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
@ -9,13 +8,13 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.DBUtils
class PostFragment : Fragment() {
@ -25,29 +24,30 @@ class PostFragment : Fragment() {
savedInstanceState: Bundle?
): View? {
val current_status = arguments?.getSerializable(POST_TAG) as Status?
val domain = arguments?.getString(DOMAIN_TAG)!!
val statusDomain = arguments?.getString(DOMAIN_TAG)!!
val root: View = inflater.inflate(R.layout.post_fragment, container, false)
val picRequest = Glide.with(this)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
current_status?.setupPost(root, picRequest, this, domain, true)
current_status?.setupPost(root, picRequest, this, statusDomain, true)
//Setup arguments needed for the onclicklisteners
val holder = PostViewHolder(root, requireContext())
val db = DBUtils.initDB(requireContext())
val preferences = requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
val accessToken = preferences.getString("accessToken", "")
val api = PixelfedAPI.create("${preferences.getString("domain", "")}")
val user = db.userDao().getActiveUser()
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
val api = PixelfedAPI.create(domain)
current_status?.setDescription(root, api, "Bearer $accessToken")
//Activate onclickListeners
current_status?.activateBookmarker(holder, api, "Bearer $accessToken", current_status.bookmarked)
current_status?.activateLiker(holder, api, "Bearer $accessToken", current_status!!.favourited)
current_status?.activateReblogger(holder, api, "Bearer $accessToken", current_status!!.reblogged)
current_status?.activateLiker(holder, api, "Bearer $accessToken", current_status.favourited)
current_status?.activateReblogger(holder, api, "Bearer $accessToken", current_status.reblogged)
current_status?.activateCommenter(holder, api, "Bearer $accessToken")
current_status?.showComments(holder, api, "Bearer $accessToken")

View File

@ -13,11 +13,13 @@ import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
/**
<<<<<<< HEAD:app/src/main/java/com/h/pixeldroid/fragments/ProfilePostGridFragment.kt
* A fragment representing a list of Items.
* Activities containing this fragment MUST implement the
* [ProfilePostGridFragment.OnListFragmentInteractionListener] interface.
*/
class ProfilePostGridFragment : Fragment() {
private var columnCount = 3
override fun onCreateView(

View File

@ -1,8 +1,6 @@
package com.h.pixeldroid.fragments
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -16,7 +14,6 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.PostActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.SearchActivity
@ -24,6 +21,7 @@ import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.DiscoverPost
import com.h.pixeldroid.objects.DiscoverPosts
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.ImageConverter
import retrofit2.Call
import retrofit2.Callback
@ -35,7 +33,6 @@ import retrofit2.Response
class SearchDiscoverFragment : Fragment() {
private lateinit var api: PixelfedAPI
private lateinit var preferences: SharedPreferences
private lateinit var recycler : RecyclerView
private lateinit var adapter : DiscoverRecyclerViewAdapter
private lateinit var accessToken: String
@ -43,7 +40,6 @@ class SearchDiscoverFragment : Fragment() {
private lateinit var discoverRefreshLayout: SwipeRefreshLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -67,11 +63,14 @@ class SearchDiscoverFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
preferences = requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
api = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "") ?: ""
val db = DBUtils.initDB(requireContext())
val user = db.userDao().getActiveUser()
val domain = user?.instance_uri.orEmpty()
api = PixelfedAPI.create(domain)
accessToken = user?.accessToken.orEmpty()
discoverProgressBar = view.findViewById(R.id.discoverProgressBar)
discoverRefreshLayout = view.findViewById(R.id.discoverRefreshLayout)

View File

@ -1,7 +1,6 @@
package com.h.pixeldroid.fragments.feeds
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -20,10 +19,12 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.FeedContent
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.fragment_feed.view.*
import retrofit2.Call
import retrofit2.Callback
@ -36,12 +37,15 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
protected var accessToken: String? = null
protected lateinit var pixelfedAPI: PixelfedAPI
protected lateinit var preferences: SharedPreferences
protected lateinit var list : RecyclerView
protected lateinit var adapter : FeedsRecyclerViewAdapter<T, VH>
protected lateinit var swipeRefreshLayout: SwipeRefreshLayout
internal lateinit var loadingIndicator: ProgressBar
private var user: UserDatabaseEntity? = null
private lateinit var db: AppDatabase
override fun onCreateView(
inflater: LayoutInflater,
@ -54,12 +58,11 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
loadingIndicator = view.findViewById(R.id.progressBar)
list = swipeRefreshLayout.list
preferences = requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
list.layoutManager = LinearLayoutManager(context)
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "")
db = DBUtils.initDB(requireContext())
user = db.userDao().getActiveUser()
pixelfedAPI = PixelfedAPI.create(user?.instance_uri.orEmpty())
accessToken = user?.accessToken.orEmpty()
return view
}
@ -113,14 +116,14 @@ open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment(
callback.onResult(notifications as List<T>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<List<T>>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Toast.makeText(context, getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})
@ -149,7 +152,7 @@ abstract class FeedsRecyclerViewAdapter<T: FeedContent, VH : RecyclerView.ViewHo
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
return oldItem.equals(newItem)
}
}
){

View File

@ -9,7 +9,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
@ -81,7 +80,7 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.N
private fun makeContent(): LiveData<PagedList<Notification>> {
fun makeInitialCall(requestedLoadSize: Int): Call<List<Notification>> {
return pixelfedAPI
.notifications("Bearer $accessToken", min_id="1", limit="$requestedLoadSize")
.notifications("Bearer $accessToken", limit="$requestedLoadSize")
}
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Notification>> {
return pixelfedAPI

View File

@ -0,0 +1,35 @@
package com.h.pixeldroid.fragments.feeds
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.h.pixeldroid.R
import kotlinx.android.synthetic.main.fragment_feed.view.feed_fragment_placeholder_text
/**
* A simple [Fragment] subclass.
* Use the [OfflineFeedFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class OfflineFeedFragment: Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_offline_feed, container, false)
view.feed_fragment_placeholder_text.visibility = View.VISIBLE
return view
}
}

View File

@ -4,7 +4,6 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -21,20 +20,26 @@ import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.util.ViewPreloadSizeProvider
import com.h.pixeldroid.R
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import retrofit2.Call
open class PostsFeedFragment : FeedFragment<Status, PostViewHolder>() {
lateinit var picRequest: RequestBuilder<Drawable>
lateinit var domain : String
private var user: UserDatabaseEntity? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
domain = preferences.getString("domain", "")!!
val db = DBUtils.initDB(requireContext())
user = db.userDao().getActiveUser()
domain = user?.instance_uri.orEmpty()
//RequestBuilder that is re-used for every image
picRequest = Glide.with(this)
.asDrawable().fitCenter()

View File

@ -8,8 +8,7 @@ import retrofit2.Call
class PublicTimelineFragment: PostsFeedFragment() {
inner class SearchFeedDataSource(
) : FeedDataSource(null, null){
inner class SearchFeedDataSource : FeedDataSource(null, null){
override fun newSource(): FeedDataSource {
return SearchFeedDataSource()

View File

@ -9,6 +9,7 @@ import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.h.pixeldroid.R
import com.h.pixeldroid.fragments.feeds.AccountListFragment
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.objects.Account
@ -72,14 +73,14 @@ class SearchAccountFragment: AccountListFragment(){
callback.onResult(notifications as List<Account>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Toast.makeText(context,getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})

View File

@ -6,7 +6,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
@ -14,21 +13,11 @@ import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.util.ViewPreloadSizeProvider
import com.h.pixeldroid.R
import com.h.pixeldroid.fragments.feeds.AccountListFragment
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.fragments.feeds.FeedsRecyclerViewAdapter
import com.h.pixeldroid.fragments.feeds.NotificationsFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Notification
import com.h.pixeldroid.objects.Results
import com.h.pixeldroid.objects.Tag
import kotlinx.android.synthetic.main.account_list_entry.view.*
import kotlinx.android.synthetic.main.fragment_tags.view.*
import retrofit2.Call
import retrofit2.Callback
@ -110,14 +99,14 @@ class SearchHashtagFragment: FeedFragment<Tag, SearchHashtagFragment.TagsRecycle
callback.onResult(notifications as List<Tag>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
Toast.makeText(context,getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Toast.makeText(context,getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})

View File

@ -5,7 +5,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
@ -34,8 +33,7 @@ class SearchPostsFragment: PostsFeedFragment(){
return view
}
inner class SearchFeedDataSource(
) : FeedDataSource(null, null){
inner class SearchFeedDataSource : FeedDataSource(null, null){
override fun newSource(): FeedDataSource {
return SearchFeedDataSource()

View File

@ -23,7 +23,7 @@ data class Account(
val acct: String = "",
val url: String = "", //HTTPS URL
//Display attributes
val display_name: String? = null,
val display_name: String = "",
val note: String = "", //HTML
val avatar: String = "", //URL
val avatar_static: String = "", //URL

View File

@ -3,10 +3,6 @@ package com.h.pixeldroid.objects
abstract class FeedContent {
abstract val id: String
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return id.hashCode()
}

View File

@ -5,3 +5,4 @@ import java.io.Serializable
class Field : Serializable {
}

View File

@ -3,10 +3,14 @@ package com.h.pixeldroid.objects
data class Instance (
val description: String,
val email: String,
val max_toot_chars: String = "500",
val max_toot_chars: String = DEFAULT_MAX_TOOT_CHARS.toString(),
val registrations: Boolean,
val thumbnail: String,
val title: String,
val uri: String,
val version: String
)
) {
companion object {
const val DEFAULT_MAX_TOOT_CHARS = 500
}
}

View File

@ -2,6 +2,4 @@ package com.h.pixeldroid.objects
import java.io.Serializable
class Poll : Serializable {
}
class Poll : Serializable

View File

@ -5,3 +5,4 @@ import java.io.Serializable
class Source : Serializable {
}

View File

@ -2,37 +2,31 @@ package com.h.pixeldroid.objects
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.ColorMatrixColorFilter
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.*
import androidx.core.text.toSpanned
import android.widget.TextView
import android.widget.LinearLayout
import android.widget.Toast
import android.widget.PopupMenu
import android.widget.ImageView
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.RequestBuilder
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.ImageFragment
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.ImageFragment
import com.h.pixeldroid.fragments.feeds.PostViewHolder
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.bookmarkPostCall
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
@ -40,25 +34,18 @@ import com.h.pixeldroid.utils.PostUtils.Companion.retrieveComments
import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput
import com.h.pixeldroid.utils.PostUtils.Companion.unBookmarkPostCall
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.PermissionToken
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.single.BasePermissionListener
import com.karumi.dexter.listener.single.PermissionListener
import kotlinx.android.synthetic.main.post_fragment.view.postDate
import kotlinx.android.synthetic.main.post_fragment.view.postDomain
import kotlinx.android.synthetic.main.post_fragment.view.*
import java.io.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.*
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.
@ -99,13 +86,11 @@ data class Status(
val muted: Boolean = false,
val bookmarked: Boolean = false,
val pinned: Boolean = false
) : Serializable, FeedContent()
) : Serializable, FeedContent()
{
companion object {
const val SAVE_TO_GALLERY_WRITE_PERMISSION = 1
const val POST_TAG = "postTag"
const val POST_FRAG_TAG = "postFragTag"
const val DOMAIN_TAG = "domainTag"
const val DISCOVER_TAG = "discoverTag"
}
@ -120,31 +105,24 @@ data class Status(
private fun getDescription(api: PixelfedAPI, context: Context, credential: String) : Spanned {
val description = content
if(description.isEmpty()) {
return "No description".toSpanned()
return context.getString(R.string.no_description).toSpanned()
}
return parseHTMLText(description, mentions, api, context, credential)
}
fun getUsername() : CharSequence {
var name = account.username
if (name.isNullOrEmpty()) {
name = account.display_name?: "NoName"
}
return name
fun getUsername() : CharSequence =
account.username.ifBlank{account.display_name.ifBlank{"NoName"}}
fun getNLikes(context: Context) : CharSequence {
return context.getString(R.string.likes).format(favourites_count.toString())
}
fun getNLikes() : CharSequence {
val nLikes = favourites_count
return "$nLikes Likes"
fun getNShares(context: Context) : CharSequence {
return context.getString(R.string.shares).format(reblogs_count.toString())
}
fun getNShares() : CharSequence {
val nShares = reblogs_count
return "$nShares Shares"
}
private fun ISO8601toDate(dateString : String, textView: TextView, isActivity: Boolean) {
private fun ISO8601toDate(dateString : String, textView: TextView, isActivity: Boolean, context: Context) {
var format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.hhmmss'Z'")
if(dateString.matches("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}Z".toRegex())) {
format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.hhmmss'Z'")
@ -160,8 +138,10 @@ data class Status(
.getRelativeTimeSpanString(then, now,
android.text.format.DateUtils.SECOND_IN_MILLIS,
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE)
textView.text = if(isActivity) "Posted on $date"
textView.text = if(isActivity) context.getString(R.string.posted_on).format(date)
else "$formattedDate"
} catch (e: ParseException) {
e.printStackTrace()
}
@ -175,28 +155,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) {
@ -208,6 +207,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()
@ -221,25 +221,29 @@ data class Status(
isActivity : Boolean
) {
//Setup username as a button that opens the profile
val username = rootView.findViewById<TextView>(R.id.username)
username.text = this.getUsername()
username.setTypeface(null, Typeface.BOLD)
username.setOnClickListener { account.openProfile(rootView.context) }
rootView.findViewById<TextView>(R.id.username).apply {
text = this@Status.getUsername()
setTypeface(null, Typeface.BOLD)
setOnClickListener { account.openProfile(rootView.context) }
}
val usernameDesc = rootView.findViewById<TextView>(R.id.usernameDesc)
usernameDesc.text = this.getUsername()
usernameDesc.setTypeface(null, Typeface.BOLD)
rootView.findViewById<TextView>(R.id.usernameDesc).apply {
text = this@Status.getUsername()
setTypeface(null, Typeface.BOLD)
}
val nlikes = rootView.findViewById<TextView>(R.id.nlikes)
nlikes.text = this.getNLikes()
nlikes.setTypeface(null, Typeface.BOLD)
rootView.findViewById<TextView>(R.id.nlikes).apply {
text = this@Status.getNLikes(rootView.context)
setTypeface(null, Typeface.BOLD)
}
val nshares = rootView.findViewById<TextView>(R.id.nshares)
nshares.text = this.getNShares()
nshares.setTypeface(null, Typeface.BOLD)
rootView.findViewById<TextView>(R.id.nshares).apply {
text = this@Status.getNShares(rootView.context)
setTypeface(null, Typeface.BOLD)
}
//Convert the date to a readable string
ISO8601toDate(created_at, rootView.postDate, isActivity)
ISO8601toDate(created_at, rootView.postDate, isActivity, rootView.context)
rootView.postDomain.text = getStatusDomain(domain)
@ -258,16 +262,16 @@ data class Status(
//Set comment initial visibility
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE
imagePopUpMenu(rootView, homeFragment.requireActivity())
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = GONE
}
fun setDescription(rootView: View, api : PixelfedAPI, credential: String) {
val desc = rootView.findViewById<TextView>(R.id.description)
desc.text = this.getDescription(api, rootView.context, credential)
desc.movementMethod = LinkMovementMethod.getInstance()
desc.apply {
text = this@Status.getDescription(api, rootView.context, credential)
movementMethod = LinkMovementMethod.getInstance()
}
}
fun activateReblogger(
@ -276,19 +280,21 @@ data class Status(
credential: String,
isReblogged : Boolean
) {
//Set initial button state
holder.reblogger.isChecked = isReblogged
holder.reblogger.apply {
//Set initial button state
isChecked = isReblogged
//Activate the button
holder.reblogger.setEventListener { _, buttonState ->
if (buttonState) {
Log.e("REBLOG", "Reblogged post")
// Button is active
reblogPost(holder, api, credential, this)
} else {
Log.e("REBLOG", "Undo Reblogged post")
// Button is inactive
undoReblogPost(holder, api, credential, this)
//Activate the button
setEventListener { _, buttonState ->
if (buttonState) {
// Button is active
undoReblogPost(holder, api, credential, this@Status)
} else {
// Button is inactive
reblogPost(holder, api, credential, this@Status)
}
//show animation or not?
true
}
}
}
@ -299,20 +305,25 @@ data class Status(
credential: String,
isLiked: Boolean
) {
//Set initial state
holder.liker.isChecked = isLiked
//Activate the liker
holder.liker.setEventListener { _, buttonState ->
holder.liker.apply {
//Set initial state
isChecked = isLiked
//Activate the liker
setEventListener { _, buttonState ->
if (buttonState) {
// Button is active
likePostCall(holder, api, credential, this)
// Button is active, unlike
unLikePostCall(holder, api, credential, this@Status)
} else {
// Button is inactive
unLikePostCall(holder, api, credential, this)
// Button is inactive, like
likePostCall(holder, api, credential, this@Status)
}
//show animation or not?
true
}
}
}
fun activateBookmarker(
holder : PostViewHolder,
@ -321,18 +332,20 @@ data class Status(
isBookmarked: Boolean
) {
// Set initial state
holder.bookmarker.isChecked = isBookmarked
holder.bookmarker.apply {
isChecked = isBookmarked
// Activate bookmarker
holder.bookmarker.setEventListener { _, buttonState ->
if (buttonState) {
Log.e("BUTTON ACTIVE", buttonState.toString())
// Button is active
bookmarkPostCall(holder, api, credential, this)
} else {
Log.e("BUTTON INACTIVE", buttonState.toString())
// Button is inactive
unBookmarkPostCall(holder, api, credential, this)
// Activate bookmarker
setEventListener { _, buttonState ->
if (buttonState) {
// Button is active
bookmarkPostCall(holder, api, credential, this@Status)
} else {
// Button is inactive
unBookmarkPostCall(holder, api, credential, this@Status)
}
//show animation or not?
true
}
}
}
@ -345,14 +358,16 @@ data class Status(
) {
//Show all comments of a post
if (replies_count == 0) {
holder.viewComment.text = "No comments on this post..."
holder.viewComment.text = holder.context.getString(R.string.NoCommentsToShow)
} else {
holder.viewComment.text = "View all $replies_count comments..."
holder.viewComment.setOnClickListener {
holder.viewComment.visibility = View.GONE
holder.viewComment.apply {
text = "$replies_count ${holder.context.getString(R.string.CommentDisplay)}"
setOnClickListener {
visibility = GONE
//Retrieve the comments
retrieveComments(holder, api, credential, this)
//Retrieve the comments
retrieveComments(holder, api, credential, this@Status)
}
}
}
}
@ -370,7 +385,7 @@ data class Status(
val textIn = holder.comment.text
//Open text input
if(textIn.isNullOrEmpty()) {
Toast.makeText(holder.context,"Comment must not be empty!", Toast.LENGTH_SHORT).show()
Toast.makeText(holder.context, holder.context.getString(R.string.empty_comment), Toast.LENGTH_SHORT).show()
} else {
//Post the comment
@ -384,7 +399,7 @@ data class Status(
}
fun imagePopUpMenu(view: View, activity: FragmentActivity) {
private fun imagePopUpMenu(view: View, activity: FragmentActivity) {
val anchor = view.findViewById<FrameLayout>(R.id.post_fragment_image_popup_menu_anchor)
if (!media_attachments.isNullOrEmpty() && media_attachments.size == 1) {
view.findViewById<ImageView>(R.id.postPicture).setOnLongClickListener {
@ -396,7 +411,7 @@ data class Status(
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object: BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(view.context, "You need to grant write permission to download pictures!", Toast.LENGTH_SHORT).show()
Toast.makeText(view.context, view.context.getString(R.string.write_permission_download_pic), Toast.LENGTH_SHORT).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
@ -410,7 +425,7 @@ data class Status(
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object: BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(view.context, "You need to grant write permission to share pictures!", Toast.LENGTH_SHORT).show()
Toast.makeText(view.context, view.context.getString(R.string.write_permission_share_pic), Toast.LENGTH_SHORT).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
@ -429,4 +444,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

@ -0,0 +1,55 @@
package com.h.pixeldroid.utils
import android.content.Context
import androidx.room.Room
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Instance
import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain
class DBUtils {
companion object {
fun initDB(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java, "pixeldroid"
).allowMainThreadQueries().build()
}
private fun normalizeOrNot(uri: String): String{
return if(uri.startsWith("http://localhost")){
uri
} else {
normalizeDomain(uri)
}
}
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, accessToken: String) {
db.userDao().insertUser(
UserDatabaseEntity(
user_id = account.id,
//make sure not to normalize to https when localhost, to allow testing
instance_uri = normalizeOrNot(instance_uri),
username = account.username,
display_name = account.display_name,
avatar_static = account.avatar_static,
isActive = activeUser,
accessToken = accessToken
)
)
}
fun storeInstance(db: AppDatabase, instance: Instance) {
val maxTootChars = instance.max_toot_chars.toInt()
val dbInstance = InstanceDatabaseEntity(
//make sure not to normalize to https when localhost, to allow testing
uri = normalizeOrNot(instance.uri),
title = instance.title,
max_toot_chars = maxTootChars,
thumbnail = instance.thumbnail
)
db.instanceDao().insertInstance(dbInstance)
}
}
}

View File

@ -1,45 +0,0 @@
package com.h.pixeldroid.utils
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.PostEntity
import java.util.Calendar
class DatabaseUtils {
companion object {
/**
* Inserts one post into the specified database,
* after it has checked the LRU
*/
fun insertPost(db: AppDatabase, post: PostEntity) {
if (!IsInsertable(db)) {
removeEldestPost(db)
}
db.postDao().addDateToPost(post.uid, Calendar.getInstance().time)
db.postDao().insertAll(post)
}
/**
* Inserts multiple posts into the specified database
*/
fun insertAllPosts(db: AppDatabase, vararg posts: PostEntity) {
posts.forEach { insertPost(db, it) }
}
/**
* Checks if we can add one post into the database
* or if it is full
*/
private fun IsInsertable(db: AppDatabase): Boolean {
return db.postDao().getPostsCount() + 1 <= db.MAX_NUMBER_OF_POSTS
}
/**
* Removes the eldest post from the database
*/
private fun removeEldestPost(db: AppDatabase) {
db.postDao().deleteOldestPost()
}
}
}

View File

@ -32,7 +32,7 @@ class HtmlUtils {
return result.trim().toSpanned()
}
public fun getDomain(urlString: String?): String {
fun getDomain(urlString: String?): String {
val uri: URI
try {
uri = URI(urlString!!)
@ -111,8 +111,8 @@ class HtmlUtils {
}
}
builder.removeSpan(span);
builder.setSpan(customSpan, start, end, flags);
builder.removeSpan(span)
builder.setSpan(customSpan, start, end, flags)
// Add zero-width space after links in end of line to fix its too large hitbox.
if (end >= builder.length || builder.subSequence(end, end + 1).toString() == "\n") {

View File

@ -89,7 +89,7 @@ class ImageUtils {
e.printStackTrace()
}
intentShare.putExtra(Intent.EXTRA_STREAM, uri)
activity.startActivity(Intent.createChooser(intentShare, "Share Image"))
activity.startActivity(Intent.createChooser(intentShare, context.getString(R.string.share_image)))
}
cursor.close()
}

View File

@ -1,5 +1,6 @@
package com.h.pixeldroid.utils
import android.graphics.ColorMatrix
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@ -54,7 +55,7 @@ abstract class PostUtils {
val resp = response.body()!!
//Update shown share count
holder.nshares.text = resp.getNShares()
holder.nshares.text = resp.getNShares(holder.context)
holder.reblogger.isChecked = resp.reblogged
} else {
Log.e("RESPONSE_CODE", response.code().toString())
@ -83,7 +84,7 @@ abstract class PostUtils {
val resp = response.body()!!
//Update shown share count
holder.nshares.text = resp.getNShares()
holder.nshares.text = resp.getNShares(holder.context)
holder.reblogger.isChecked = resp.reblogged
} else {
Log.e("RESPONSE_CODE", response.code().toString())
@ -112,7 +113,7 @@ abstract class PostUtils {
val resp = response.body()!!
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes()
holder.nlikes.text = resp.getNLikes(holder.context)
holder.liker.isChecked = resp.favourited
} else {
Log.e("RESPONSE_CODE", response.code().toString())
@ -141,7 +142,7 @@ abstract class PostUtils {
val resp = response.body()!!
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes()
holder.nlikes.text = resp.getNLikes(holder.context)
holder.liker.isChecked = resp.favourited
} else {
Log.e("RESPONSE_CODE", response.code().toString())
@ -221,7 +222,8 @@ abstract class PostUtils {
Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("COMMENT ERROR", t.toString())
Toast.makeText(holder.context,"Comment error!", Toast.LENGTH_SHORT).show()
Toast.makeText(holder.context, holder.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
@ -233,7 +235,9 @@ abstract class PostUtils {
//Add the comment to the comment section
addComment(holder.context, holder.commentCont, resp.account.username, resp.content)
Toast.makeText(holder.context,"Comment: \"$textIn\" posted!", Toast.LENGTH_SHORT).show()
Toast.makeText(holder.context,
holder.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT).show()
Log.e("COMMENT SUCCESS", "posted: $textIn")
} else {
Log.e("ERROR_CODE", response.code().toString())
@ -280,5 +284,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

@ -0,0 +1,39 @@
package com.h.pixeldroid.utils
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import com.h.pixeldroid.R
class ThemeUtils {
companion object {
/**
* @brief Updates the application's theme depending on the given preferences and resources
*/
fun setThemeFromPreferences(preferences: SharedPreferences, resources : Resources) {
val themes = resources.getStringArray(R.array.theme_values)
val theme = preferences.getString("theme", "")
Log.e("themePref", theme!!)
//Set the theme
when(theme) {
//Light
themes[1] -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
//Dark
themes[2] -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
package com.h.pixeldroid.utils
import android.content.Context
import android.net.ConnectivityManager
class Utils {
companion object {
fun hasInternet(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return cm.activeNetwork != null
}
fun normalizeDomain(domain: String): String {
return "https://" + domain
.replace("http://", "")
.replace("https://", "")
.trim(Char::isWhitespace)
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="@color/icPressed" />
<item android:state_focused="true" android:color="@color/icFocused" />
<item android:color="@color/icActive" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="48"
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="m18.9609,24.1172q0,2.793 1.3867,4.3945 1.3867,1.582 3.8086,1.582 2.4023,0 3.7695,-1.6016 1.3867,-1.6016 1.3867,-4.375 0,-2.7344 -1.4063,-4.3359 -1.4063,-1.6211 -3.7891,-1.6211 -2.3633,0 -3.7695,1.6016 -1.3867,1.6016 -1.3867,4.3555zM29.6055,29.957q-1.1719,1.5039 -2.6953,2.2266 -1.5039,0.7031 -3.5156,0.7031 -3.3594,0 -5.4688,-2.4219 -2.0898,-2.4414 -2.0898,-6.3477 0,-3.9063 2.1094,-6.3477 2.1094,-2.4414 5.4492,-2.4414 2.0117,0 3.5352,0.7422 1.5234,0.7227 2.6758,2.207v-2.5586h2.793v14.375q2.8516,-0.4297 4.4531,-2.5977 1.6211,-2.1875 1.6211,-5.6445 0,-2.0898 -0.625,-3.9258 -0.6055,-1.8359 -1.8555,-3.3984 -2.0313,-2.5586 -4.9609,-3.9063 -2.9102,-1.3672 -6.3477,-1.3672 -2.4023,0 -4.6094,0.6445 -2.207,0.625 -4.082,1.875 -3.0664,1.9922 -4.8047,5.2344 -1.7188,3.2227 -1.7188,6.9922 0,3.1055 1.1133,5.8203 1.1328,2.7148 3.2617,4.7852 2.0508,2.0313 4.7461,3.0859 2.6953,1.0742 5.7617,1.0742 2.5195,0 4.9414,-0.8594 2.4414,-0.8398 4.4727,-2.4219l1.7578,2.168q-2.4414,1.8945 -5.332,2.8906 -2.8711,1.0156 -5.8398,1.0156 -3.6133,0 -6.8164,-1.2891 -3.2031,-1.2695 -5.7031,-3.7109 -2.5,-2.4414 -3.8086,-5.6445 -1.3086,-3.2227 -1.3086,-6.9141 0,-3.5547 1.3281,-6.7773 1.3281,-3.2227 3.7891,-5.6641 2.5195,-2.4805 5.8203,-3.7891 3.3008,-1.3281 6.9922,-1.3281 4.1406,0 7.6758,1.6992 3.5547,1.6992 5.957,4.8242 1.4648,1.9141 2.2266,4.1602 0.7813,2.2461 0.7813,4.6484 0,5.1367 -3.1055,8.1055 -3.1055,2.9688 -8.5742,3.0859z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/>
</vector>

View File

@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
android:fillColor="#FFFFFF"
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z"/>
</vector>

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,4L24,4A20,20 0,0 1,44 24L44,24A20,20 0,0 1,24 44L24,44A20,20 0,0 1,4 24L4,24A20,20 0,0 1,24 4z"
android:strokeWidth="0.82808512"
android:fillColor="#7f7f7f"
android:fillAlpha="1"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M24.0666,16.1964m-6.6463,0a6.6463,6.6463 0,1 1,13.2926 0a6.6463,6.6463 0,1 1,-13.2926 0"
android:strokeWidth="0.46640581"/>
<group>
<clip-path
android:pathData="M24,27.5161L24,27.5161A5.4669,13.0074 90,0 1,37.0074 32.983L37.0074,32.983A5.4669,13.0074 90,0 1,24 38.4498L24,38.4498A5.4669,13.0074 90,0 1,10.9926 32.983L10.9926,32.983A5.4669,13.0074 90,0 1,24 27.5161zM10.9926,32.983l26.0147,0l0,5.4669l-26.0147,0z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M24,27.5161L24,27.5161A5.4669,13.0074 90,0 1,37.0074 32.983L37.0074,32.983A5.4669,13.0074 90,0 1,24 38.4498L24,38.4498A5.4669,13.0074 90,0 1,10.9926 32.983L10.9926,32.983A5.4669,13.0074 90,0 1,24 27.5161z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M10.9926,32.983l26.0147,0l0,5.4669l-26.0147,0z"/>
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@ -0,0 +1,4 @@
<vector android:height="18dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M462.3,62.6C407.5,15.9 326,24.3 275.7,76.2L256,96.5l-19.7,-20.3C186.1,24.3 104.5,15.9 49.7,62.6c-62.8,53.6 -66.1,149.8 -9.9,207.9l193.5,199.8c12.5,12.9 32.8,12.9 45.3,0l193.5,-199.8c56.3,-58.1 53,-154.3 -9.8,-207.9z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="21.6dp" android:viewportHeight="99"
android:viewportWidth="110" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M54.5,94.5C58.2,90.74 79.39,69.25 92.63,55.83C104.5,43.8 104.24,26.26 93.46,15.39C82.67,4.5 65.23,4.55 54.5,15.48C43.76,4.55 26.32,4.5 15.54,15.38C4.75,26.25 4.5,43.79 16.36,55.83C29.6,69.25 50.79,90.74 54.5,94.5Z"
android:strokeColor="#FFFFFF" android:strokeWidth="9"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector android:height="24dp" android:viewportHeight="85"
android:viewportWidth="111" android:width="31.341177dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M30.5,64.5L90.5,64.5L90.5,39.81"
android:strokeColor="#FFFFFF" android:strokeWidth="9"/>
<path android:fillColor="#FFFFFF"
android:pathData="M90.5,25.56L100,44.56L90.5,39.81L81,44.56Z"
android:strokeColor="#FFFFFF" android:strokeWidth="9"/>
<path android:fillColor="#00000000"
android:pathData="M80.5,18.5L20.5,18.5L20.5,44.19"
android:strokeColor="#FFFFFF" android:strokeWidth="9"/>
<path android:fillColor="#FFFFFF"
android:pathData="M20.5,58.44L11,39.44L20.5,44.19L30,39.44Z"
android:strokeColor="#FFFFFF" android:strokeWidth="9"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index"
android:gravity="center"/>
</item>
</layer-list>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/black"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index_night"
android:gravity="center"/>
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<stroke
android:width="@dimen/stroke_small"
android:color="@color/icPressed" />
<size
android:width="@dimen/round_button_medium"
android:height="@dimen/round_button_medium" />
</shape>
</item>
<item android:state_focused="true">
<shape android:shape="oval">
<stroke
android:width="@dimen/stroke_small"
android:color="@color/icFocused" />
<size
android:width="@dimen/round_button_medium"
android:height="@dimen/round_button_medium" />
</shape>
</item>
<item>
<shape android:shape="oval">
<stroke
android:width="@dimen/stroke_small"
android:color="@color/icActive" />
<size
android:width="@dimen/round_button_medium"
android:height="@dimen/round_button_medium" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="@color/selector_ic"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000000"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/ic_shutter_pressed" />
<item android:state_focused="true" android:drawable="@drawable/ic_shutter_focused" />
<item android:drawable="@drawable/ic_shutter_normal" />
</selector>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#58A0C4" android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#CFD7DB" android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More