Offline mode - Phase 1 (#156)

* rework post entity

* refactor login activity

* added db instance to login activity

* remember user logins offline

* drawer works offline

* fixed some tests

* move imagefragment in fragment folder

* added tests for the new login flow

* add missing drawer test

* add login offline test

* added online login flow tests

* fixed tests

* added mockserver /instance repsonse

* fixed marie's request
This commit is contained in:
Ulysse Widmer 2020-05-14 20:14:41 +02:00 committed by GitHub
parent b2842b8abe
commit e96d5e22a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 972 additions and 654 deletions

View File

@ -70,12 +70,14 @@ dependencies {
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'
@ -111,7 +113,10 @@ dependencies {
def fragment_version = '1.2.4'
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
implementation 'com.karumi:dexter:6.1.0'
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

@ -13,6 +13,8 @@ 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import com.h.pixeldroid.testUtility.MockServer
import org.junit.Before
import org.junit.Rule
@ -42,7 +44,7 @@ class DrawerMenuTest {
// 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
}
@ -116,4 +118,11 @@ class DrawerMenuTest {
onView(withId(R.id.accountNameTextView)).check(matches(withText("Andrew Dobis")))
}
@Test
fun onBackPressedClosesDrawer() {
UiDevice.getInstance(getInstrumentation()).pressBack()
Thread.sleep(1000)
onView(withId(R.id.drawer_layout)).check(matches(DrawerMatchers.isClosed()))
}
}

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.Intent.ACTION_VIEW
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
import androidx.test.espresso.action.ViewActions.click
@ -16,12 +17,19 @@ 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.isDisplayed
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.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.utils.DBUtils
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
@ -39,34 +47,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 +97,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,83 @@
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.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
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.getInstrumentation
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.InstanceDatabaseEntity
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.utils.DBUtils
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_chooser_button
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()
}
@Test
fun emptyDBandOfflineModeDisplayCorrectMessage() {
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.login_activity_connection_required_text)).check(matches(isDisplayed()))
}
@Test
fun offlineModeSelectAvailabeLaunchesMainActivityWithStoredAccountInstance() {
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = "some_uri",
title = "PixelTest"
))
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "some_user_id",
instance_uri = "some_uri",
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url"
))
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.login_activity_instance_chooser_button)).perform(click())
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.drawer_account_name)).check(matches(withText("Testi Testo")))
}
@After
fun after() {
device.openQuickSettings()
device.findObject(UiSelector().textContains("airplane")).click()
device.pressHome()
}
}

View File

@ -0,0 +1,130 @@
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()
}
@Test
fun connectToSavedAccount() {
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = "some_uri",
title = "PixelTest"
)
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "some_user_id",
instance_uri = "some_uri",
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url"
)
)
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.login_activity_instance_chooser_button)).perform(click())
}
@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.login_empty_string_error))
))
}
@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() {
pref.edit()
.putString("accessToken", "azerty")
.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)
onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed()))
}
}

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,6 +4,7 @@
package="com.h.pixeldroid">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

View File

@ -8,15 +8,30 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ArrayAdapter
import android.widget.Toast
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.db.InstanceDatabaseEntity
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 kotlinx.android.synthetic.main.activity_login.*
import okhttp3.HttpUrl
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.Utils
import kotlinx.android.synthetic.main.activity_login.connect_instance_button
import kotlinx.android.synthetic.main.activity_login.editText
import kotlinx.android.synthetic.main.activity_login.login_activity_connection_required_text
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_chooser
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_chooser_button
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_chooser_layout
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_chooser_offline_text
import kotlinx.android.synthetic.main.activity_login.login_activity_instance_input_layout
import kotlinx.android.synthetic.main.activity_login.progressLayout
import kotlinx.android.synthetic.main.activity_login.whatsAnInstanceTextView
import okhttp3.internal.toImmutableList
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -24,41 +39,108 @@ import retrofit2.Response
class LoginActivity : AppCompatActivity() {
private val TAG = "Login Activity"
companion object {
private const val TAG = "Login Activity"
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
private var chooserVisibility: 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
)
// check for stored accounts/instances
val accounts: List<Map<String, String>> = getSavedAccounts()
if (accounts.isNotEmpty()) {
displayChooser(accounts)
login_activity_instance_chooser_button.setOnClickListener {
val choice: Int = login_activity_instance_chooser.selectedItemId.toInt()
setPreferences(accounts[choice])
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
if (Utils.hasInternet(applicationContext)) {
connect_instance_button.setOnClickListener {
registerAppToServer(normalizeDomain(editText.text.toString()))
}
whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
inputVisibility = View.VISIBLE
} else {
if (accounts.isEmpty()) {
login_activity_connection_required_text.visibility = View.VISIBLE
} else {
login_activity_instance_chooser_offline_text.visibility = View.VISIBLE
}
}
loadingAnimation(false)
}
private fun getSavedAccounts(): List<Map<String, String>> {
val result = mutableListOf<Map<String, String>>()
val instances = db.instanceDao().getAll()
for (user in db.userDao().getAll()) {
val instance = instances.first {instance ->
instance.uri == user.instance_uri
}
result.add(mapOf(
Pair("username", user.username),
Pair("instance_title", instance.title),
Pair("instance_uri", instance.uri),
Pair("id", user.user_id)
))
}
return result.toImmutableList()
}
private fun displayChooser(accounts: List<Map<String, String>>) {
ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
accounts.map { account ->
"${account["username"]}@${account["instance_title"]}"
}).also {
adapter ->
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
login_activity_instance_chooser.adapter = adapter
}
chooserVisibility = View.VISIBLE
}
private fun setPreferences(account: Map<String, String>) {
if (Utils.hasInternet(applicationContext))
registerAppToServer(normalizeDomain(account["instance_uri"].orEmpty()))
else
preferences.edit()
.putString("user_id", account["id"])
.apply()
}
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() {
@ -69,82 +151,54 @@ class LoginActivity : AppCompatActivity() {
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)
i.data = Uri.parse("https://pixelfed.org/join")
startActivity(i)
}
private fun hideKeyboard() {
val view = currentFocus
if (view != null) {
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
view.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
private fun normalizeDomain(domain: String): String {
return "https://" + domain
.replace("http://", "")
.replace("https://", "")
.trim(Char::isWhitespace)
}
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> {
loadingAnimation(true)
if (normalizedDomain.replace("https://", "").isNullOrBlank())
return failedRegistration(getString(R.string.login_empty_string_error))
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 +219,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 +241,84 @@ 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) {
saveUserAndInstance(accessToken)
preferences.edit().putString("accessToken", accessToken).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
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
}
private fun loadingAnimation(on: Boolean){
if(on) {
domainTextInputLayout.visibility = View.GONE
login_activity_instance_input_layout.visibility = View.GONE
login_activity_instance_chooser_layout.visibility = View.GONE
progressLayout.visibility = View.VISIBLE
}
else {
domainTextInputLayout.visibility = View.VISIBLE
login_activity_instance_input_layout.visibility = inputVisibility
login_activity_instance_chooser_layout.visibility = chooserVisibility
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) {
preferences.edit().putInt("max_toot_chars", Instance.DEFAULT_MAX_TOOT_CHARS).apply()
pixelfedAPI.instance().enqueue(object : Callback<Instance> {
override fun onFailure(call: Call<Instance>, t: Throwable) {
}
}
})
override fun onResponse(call: Call<Instance>, response: Response<Instance>) {
if (response.isSuccessful && response.body() != null) {
val instance = response.body() as Instance
storeInstance(instance)
storeUser(accessToken)
}
}
})
}
private fun storeInstance(instance: Instance) {
val maxTootChars = instance.max_toot_chars.toInt()
preferences.edit().putInt("max_toot_chars", maxTootChars).apply()
preferences.edit().putString("instance_uri", instance.uri).apply()
val dbInstance = InstanceDatabaseEntity(
uri = instance.uri,
title = instance.title,
max_toot_chars = maxTootChars,
thumbnail = instance.thumbnail
)
db.instanceDao().insertInstance(dbInstance)
}
private fun storeUser(accessToken: String) {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.body() != null && response.isSuccessful) {
val user = response.body() as Account
preferences.edit().putString("user_id", user.id).apply()
DBUtils.addUser(
db,
user,
preferences.getString("instance_uri", null).orEmpty()
)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
}
})
}
}

View File

@ -7,38 +7,39 @@ 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.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.fragments.NewPostFragment
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.PublicTimelineFragment
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.Utils.Companion.hasInternet
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.nav_header.view.drawer_account_name
import kotlinx.android.synthetic.main.nav_header.view.drawer_avatar
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
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme_NoActionBar)
@ -48,75 +49,89 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
preferences = getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
)
db = DBUtils.initDB(applicationContext)
//Check if we have logged in and gotten an access token
if(!preferences.contains("accessToken")){
if((hasInternet(applicationContext) && !preferences.contains("accessToken"))
|| (!hasInternet(applicationContext) && !preferences.contains("user_id"))) {
launchActivity(LoginActivity())
} else {
setupDrawer()
val tabs = arrayOf(
PostsFeedFragment(),
if (hasInternet(applicationContext)) PostsFeedFragment()
else OfflineFeedFragment(),
searchDiscoverFragment,
NewPostFragment(),
NotificationsFragment(),
PublicTimelineFragment()
)
setupTabs(tabs)
}
}
private fun setupDrawer() {
drawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.nav_view)
navigationView.setNavigationItemSelectedListener(this)
// Setup views
val accessToken = preferences.getString("accessToken", "")
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
val drawerHeader = navigationView.getHeaderView(0)
val accountName = drawerHeader.findViewById<TextView>(R.id.drawer_account_name)
val avatar = drawerHeader.findViewById<ImageView>(R.id.drawer_avatar)
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()) }
nav_view.setNavigationItemSelectedListener(this)
if (hasInternet(applicationContext)) {
val accessToken = preferences.getString("accessToken", "")
val pixelfedAPI = PixelfedAPI.create("${preferences.getString("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)
fillDrawerAccountInfo(account)
}
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("DRAWER ACCOUNT:", t.toString())
}
})
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("DRAWER ACCOUNT:", t.toString())
}
})
} else {
val userId = preferences.getString("user_id", null).orEmpty()
if (userId.isNotEmpty()) {
val user: UserDatabaseEntity = db.userDao().getUserWithId(userId)
val account = Account(
id = user.user_id,
username = user.username,
display_name = user.display_name,
avatar_static = user.avatar_static
)
fillDrawerAccountInfo(account)
} else {
launchActivity(LoginActivity())
}
}
}
private fun setupTabs(tabs: Array<Fragment>){
private fun fillDrawerAccountInfo(account: Account) {
val drawerAvatar = nav_view.getHeaderView(0).drawer_avatar
val drawerAccountName = nav_view.getHeaderView(0).drawer_account_name
ImageConverter.setRoundImageFromURL(
View(applicationContext),
account.avatar_static,
drawerAvatar
)
drawerAvatar.setOnClickListener { launchActivity(ProfileActivity()) }
// Set account name
drawerAccountName.apply {
text = account.display_name
setOnClickListener { launchActivity(ProfileActivity()) }
}
}
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)
@ -137,7 +152,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
R.id.nav_logout -> launchActivity(LoginActivity())
}
drawerLayout.closeDrawer(GravityCompat.START)
drawer_layout.closeDrawer(GravityCompat.START)
return true
}
@ -154,8 +169,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,39 +1,11 @@
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
abstract fun instanceDao(): InstanceDao
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 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,21 @@
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 user_id=:id LIMIT 1")
fun getUserWithId(id: String): UserDatabaseEntity
@Query("UPDATE users SET username = :username, display_name = :display_name, avatar_static = :avatar_static WHERE user_id=:user_id")
fun updateUser(user_id: String, username: String, display_name: String, avatar_static: String)
}

View File

@ -0,0 +1,23 @@
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
)

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"
@ -96,13 +91,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

@ -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

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,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,7 +2,6 @@ package com.h.pixeldroid.objects
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spanned
@ -18,13 +17,12 @@ 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.fragments.ImageFragment
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.PostViewHolder
@ -40,12 +38,9 @@ import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput
import com.h.pixeldroid.utils.PostUtils.Companion.unLikePostCall
import com.h.pixeldroid.utils.PostUtils.Companion.undoReblogPost
import 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 java.io.Serializable
@ -124,13 +119,8 @@ data class Status(
}
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() : CharSequence {
val nLikes = favourites_count

View File

@ -0,0 +1,39 @@
package com.h.pixeldroid.utils
import android.content.Context
import androidx.room.Room
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.objects.Account
class DBUtils {
companion object {
fun initDB(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java, "pixeldroid"
).allowMainThreadQueries().build()
}
fun addUser(db: AppDatabase, account: Account, instance_uri: String = "") {
if (instance_uri.isEmpty()) {
db.userDao().updateUser(
user_id = account.id,
username = account.username,
display_name = account.display_name,
avatar_static = account.avatar_static
)
} else {
db.userDao().insertUser(
UserDatabaseEntity(
user_id = account.id,
instance_uri = instance_uri,
username = account.username,
display_name = account.display_name,
avatar_static = account.avatar_static
)
)
}
}
}
}

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!!)

View File

@ -0,0 +1,17 @@
package com.h.pixeldroid.utils
import android.content.Context
import android.net.ConnectivityManager
import androidx.room.Room
import com.h.pixeldroid.db.AppDatabase
class Utils {
companion object {
fun hasInternet(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return cm.activeNetwork != null
}
}
}

View File

@ -25,13 +25,47 @@
android:layout_marginBottom="20dp"
app:srcCompat="@drawable/ic_fred_phone" />
<LinearLayout
android:id="@+id/login_activity_instance_chooser_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="60dp"
android:layout_marginBottom="15dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/login_activity_instance_chooser_offline_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/you_are_in_offline_mode"
android:textAlignment="center"
android:visibility="gone"/>
<Spinner
android:id="@+id/login_activity_instance_chooser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="15dp"/>
<Button
android:id="@+id/login_activity_instance_chooser_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter"/>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/domainTextInputLayout"
android:id="@+id/login_activity_instance_input_layout"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="Domain of your instance"
app:errorEnabled="true">
android:hint="@string/domain_of_your_instance"
app:errorEnabled="true"
android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editText"
@ -45,8 +79,7 @@
android:id="@+id/connect_instance_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:text="Connect to Pixelfed" />
android:text="@string/connect_to_pixelfed" />
<TextView
android:id="@+id/whatsAnInstanceTextView"
@ -61,6 +94,14 @@
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/login_activity_connection_required_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_connection_required_once"
android:textAlignment="center"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/progressLayout"
android:layout_width="match_parent"

View File

@ -3,6 +3,16 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/feed_fragment_placeholder_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="25sp"
android:text="Nothing to see here!"
android:visibility="gone"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
@ -15,9 +25,9 @@
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager" />
app:layoutManager="LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progressBar"

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ImageFragment">
tools:context=".fragments.ImageFragment">
<ImageView

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/feed_fragment_placeholder_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="25sp"
android:text="Nothing to see here!"
android:visibility="gone"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -48,5 +48,15 @@
<string name="image_download_downloading">Downloading…</string>
<string name="image_download_success">Image downloaded successfully</string>
<string name="share_picture">Share picture…</string>
<string name="domain_of_your_instance">Domain of your instance</string>
<string name="connect_to_pixelfed">Connect to Pixelfed</string>
<string name="login_connection_required_once">You need to connect to the internet at least once to use PixelDroid :(</string>
<string name="you_are_in_offline_mode">You are in offline mode, but you can still view some content!</string>
<string name="enter">Enter</string>
<string name="auth_error_toast_msg">Server has responded with an error, try again!</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="login_empty_string_error">Instance address cannot be empty!</string>
</resources>