~captainepoch/husky

60e40fe657638f2fd22db38d8026deb15c0dada2 — Adolfo Santiago 2 months ago 9efc706
Init refactor
M husky/app/build.gradle.kts => husky/app/build.gradle.kts +3 -1
@@ 199,6 199,9 @@ dependencies {
    implementation(ApplicationLibs.RxJava.rxJava)
    implementation(ApplicationLibs.RxJava.rxKotlin)

    implementation(ApplicationLibs.SimpleStack.ext)
    implementation(ApplicationLibs.SimpleStack.lib)

    implementation(ApplicationLibs.Square.retrofit)
    implementation(ApplicationLibs.Square.retrofitAdapterRxJ2)
    implementation(ApplicationLibs.Square.retrofitConvGson)


@@ 219,7 222,6 @@ dependencies {
    implementation(ApplicationLibs.materialDrawer)
    implementation(ApplicationLibs.materialDrawerIconics)
    implementation(ApplicationLibs.materialDrawerTypeface)
    implementation(ApplicationLibs.filemojiCompat)
    implementation(ApplicationLibs.sparkButton)
    implementation(ApplicationLibs.timber)


M husky/app/proguard-rules.pro => husky/app/proguard-rules.pro +17 -2
@@ 2,7 2,7 @@

# turn on all optimizations except those that are known to cause problems on Android
-optimizations !code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 6
-optimizationpasses 7
-allowaccessmodification
-dontpreverify



@@ 53,12 53,27 @@

# remove all logging from production apk
-assumenosideeffects class android.util.Log {
    public static *** getStackTraceString(...);
    public static boolean isLoggable(java.lang.String, int);
    public static int d(...);
    public static int w(...);
    public static int v(...);
    public static int i(...);
    public static int e(...);
}

-assumenosideeffects class timber.log.Timber* {
    public static *** d(...);
    public static *** w(...);
    public static *** v(...);
    public static *** i(...);
    public static *** e(...);
    public static *** plant(...);
}

-assumenosideeffects class java.lang.Exception* {
public void printStackTrace();
}

-assumenosideeffects class java.lang.String {
    public static java.lang.String format(...);
}

M husky/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt => husky/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +78 -52
@@ 42,6 42,8 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
import timber.log.Timber
import timber.log.Timber.Forest

class LoginActivity : BaseActivity(), Injectable {



@@ 62,7 64,7 @@ class LoginActivity : BaseActivity(), Injectable {

        setContentView(R.layout.activity_login)

        if(savedInstanceState == null ) {
        if(savedInstanceState == null) {
            if(BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
                domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
                domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)


@@ 76,27 78,28 @@ class LoginActivity : BaseActivity(), Injectable {

        if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
            Glide.with(loginLogo)
                    .load(BuildConfig.CUSTOM_LOGO_URL)
                    .placeholder(null)
                    .into(loginLogo)
                .load(BuildConfig.CUSTOM_LOGO_URL)
                .placeholder(null)
                .into(loginLogo)
        }

        preferences = getSharedPreferences(
                getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
            getString(R.string.preferences_file_key), Context.MODE_PRIVATE
        )

        loginButton.setOnClickListener { onButtonClick() }
        settingsButton.setOnClickListener { onSettingsButtonClick() }

        whatsAnInstanceTextView.setOnClickListener {
            val dialog = AlertDialog.Builder(this)
                    .setMessage(R.string.dialog_whats_an_instance)
                    .setPositiveButton(R.string.action_close, null)
                    .show()
                .setMessage(R.string.dialog_whats_an_instance)
                .setPositiveButton(R.string.action_close, null)
                .show()
            val textView = dialog.findViewById<TextView>(android.R.id.message)
            textView?.movementMethod = LinkMovementMethod.getInstance()
        }

        if (isAdditionalLogin()) {
        if(isAdditionalLogin()) {
            setSupportActionBar(toolbar)
            supportActionBar?.setDisplayHomeAsUpEnabled(true)
            supportActionBar?.setDisplayShowTitleEnabled(false)


@@ 118,7 121,7 @@ class LoginActivity : BaseActivity(), Injectable {
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == android.R.id.home) {
        if(item.itemId == android.R.id.home) {
            onBackPressed()
            return true
        }


@@ 147,16 150,18 @@ class LoginActivity : BaseActivity(), Injectable {

        try {
            HttpUrl.Builder().host(domain).scheme("https").build()
        } catch (e: IllegalArgumentException) {
        } catch(e: IllegalArgumentException) {
            setLoading(false)
            domainTextInputLayout.error = getString(R.string.error_invalid_domain)
            return
        }

        val callback = object : Callback<AppCredentials> {
            override fun onResponse(call: Call<AppCredentials>,
                                    response: Response<AppCredentials>) {
                if (!response.isSuccessful) {
            override fun onResponse(
                call: Call<AppCredentials>,
                response: Response<AppCredentials>
            ) {
                if(!response.isSuccessful) {
                    loginButton.isEnabled = true
                    domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
                    setLoading(false)


@@ 168,10 173,10 @@ class LoginActivity : BaseActivity(), Injectable {
                val clientSecret = credentials.clientSecret

                preferences.edit()
                        .putString("domain", domain)
                        .putString("clientId", clientId)
                        .putString("clientSecret", clientSecret)
                        .apply()
                    .putString("domain", domain)
                    .putString("clientId", clientId)
                    .putString("clientSecret", clientSecret)
                    .apply()

                redirectUserToAuthorizeAndLogin(domain, clientId)
            }


@@ 192,9 197,11 @@ class LoginActivity : BaseActivity(), Injectable {
        }

        mastodonApi
                .authenticateApp(domain, appname, oauthRedirectUri,
                        OAUTH_SCOPES, website)
                .enqueue(callback)
            .authenticateApp(
                domain, appname, oauthRedirectUri,
                OAUTH_SCOPES, website
            )
            .enqueue(callback)
        setLoading(true)

    }


@@ 204,16 211,16 @@ class LoginActivity : BaseActivity(), Injectable {
         * login there, and the server will redirect back to the app with its response. */
        val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
        val parameters = mapOf(
                "client_id" to clientId,
                "redirect_uri" to oauthRedirectUri,
                "response_type" to "code",
                "scope" to OAUTH_SCOPES
            "client_id" to clientId,
            "redirect_uri" to oauthRedirectUri,
            "response_type" to "code",
            "scope" to OAUTH_SCOPES
        )
        val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
        val uri = Uri.parse(url)
        if (!openInCustomTab(uri, this)) {
        if(!openInCustomTab(uri, this)) {
            val viewIntent = Intent(Intent.ACTION_VIEW, uri)
            if (viewIntent.resolveActivity(packageManager) != null) {
            if(viewIntent.resolveActivity(packageManager) != null) {
                startActivity(viewIntent)
            } else {
                domainEditText.error = getString(R.string.error_no_web_browser_found)


@@ 229,7 236,7 @@ class LoginActivity : BaseActivity(), Injectable {
        val uri = intent.data
        val redirectUri = oauthRedirectUri

        if (uri != null && uri.toString().startsWith(redirectUri)) {
        if(uri != null && uri.toString().startsWith(redirectUri)) {
            // This should either have returned an authorization code or an error.
            val code = uri.getQueryParameter("code")
            val error = uri.getQueryParameter("error")


@@ 239,43 246,62 @@ class LoginActivity : BaseActivity(), Injectable {
            val clientId = preferences.getNonNullString(CLIENT_ID, "")
            val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")

            if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
            if(code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {

                setLoading(true)
                /* Since authorization has succeeded, the final step to log in is to exchange
                 * the authorization code for an access token. */
                val callback = object : Callback<AccessToken> {
                    override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
                        if (response.isSuccessful) {
                    override fun onResponse(
                        call: Call<AccessToken>,
                        response: Response<AccessToken>
                    ) {
                        if(response.isSuccessful) {
                            onLoginSuccess(response.body()!!.accessToken, domain)
                        } else {
                            setLoading(false)
                            domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
                            Log.e(TAG, String.format("%s %s",
                            domainTextInputLayout.error =
                                getString(R.string.error_retrieving_oauth_token)
                            Log.e(
                                TAG, String.format(
                                    "%s %s",
                                    getString(R.string.error_retrieving_oauth_token),
                                    response.message()))
                                    response.message()
                                )
                            )
                        }
                    }

                    override fun onFailure(call: Call<AccessToken>, t: Throwable) {
                        setLoading(false)
                        domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
                        Log.e(TAG, String.format("%s %s",
                        domainTextInputLayout.error =
                            getString(R.string.error_retrieving_oauth_token)
                        Log.e(
                            TAG, String.format(
                                "%s %s",
                                getString(R.string.error_retrieving_oauth_token),
                                t.message))
                                t.message
                            )
                        )
                    }
                }

                mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
                        "authorization_code").enqueue(callback)
            } else if (error != null) {
                mastodonApi.fetchOAuthToken(
                    domain, clientId, clientSecret, redirectUri, code,
                    "authorization_code"
                ).enqueue(callback)
            } else if(error != null) {
                /* Authorization failed. Put the error response where the user can read it and they
                 * can try again. */
                setLoading(false)
                domainTextInputLayout.error = getString(R.string.error_authorization_denied)
                Log.e(TAG, String.format("%s %s",
                Log.e(
                    TAG, String.format(
                        "%s %s",
                        getString(R.string.error_authorization_denied),
                        error))
                        error
                    )
                )
            } else {
                // This case means a junk response was received somehow.
                setLoading(false)


@@ 288,7 314,7 @@ class LoginActivity : BaseActivity(), Injectable {
    }

    private fun setLoading(loadingState: Boolean) {
        if (loadingState) {
        if(loadingState) {
            loginLoadingLayout.visibility = View.VISIBLE
            loginInputLayout.visibility = View.GONE
        } else {


@@ 337,7 363,7 @@ class LoginActivity : BaseActivity(), Injectable {
            s = s.replaceFirst("https://", "")
            // If a username was included (e.g. username@example.com), just take what's after the '@'.
            val at = s.lastIndexOf('@')
            if (at != -1) {
            if(at != -1) {
                s = s.substring(at + 1)
            }
            return s.trim { it <= ' ' }


@@ 350,7 376,7 @@ class LoginActivity : BaseActivity(), Injectable {
        private fun toQueryString(parameters: Map<String, String>): String {
            val s = StringBuilder()
            var between = ""
            for ((key, value) in parameters) {
            for((key, value) in parameters) {
                s.append(between)
                s.append(Uri.encode(key))
                s.append("=")


@@ 367,18 393,18 @@ class LoginActivity : BaseActivity(), Injectable {
            val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)

            val colorSchemeParams = CustomTabColorSchemeParams.Builder()
                    .setToolbarColor(toolbarColor)
                    .setNavigationBarColor(navigationbarColor)
                    .setNavigationBarDividerColor(navigationbarDividerColor)
                    .build()
                .setToolbarColor(toolbarColor)
                .setNavigationBarColor(navigationbarColor)
                .setNavigationBarDividerColor(navigationbarDividerColor)
                .build()

            val customTabsIntent = CustomTabsIntent.Builder()
                    .setDefaultColorSchemeParams(colorSchemeParams)
                    .build()
                .setDefaultColorSchemeParams(colorSchemeParams)
                .build()

            try {
                customTabsIntent.launchUrl(context, uri)
            } catch (e: ActivityNotFoundException) {
            } catch(e: ActivityNotFoundException) {
                Log.w(TAG, "Activity was not found for intent $customTabsIntent")
                return false
            }

A husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/LifecycleExt.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/LifecycleExt.kt +33 -0
@@ 0,0 1,33 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.extensions

import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer

fun <T : Any, L : LiveData<T>> Fragment.viewObserve(liveData: L, body: (T?) -> Unit) =
    liveData.observe(viewLifecycleOwner, Observer(body))

// TODO: When Failure class is ready
/*fun <L : LiveData<Failure>> Fragment.viewFailureObserve(
    liveData: L,
    body: (Failure?) -> Unit
) = liveData.observe(viewLifecycleOwner, Observer(body))*/

A husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/ViewExt.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/extensions/ViewExt.kt +38 -0
@@ 0,0 1,38 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.extensions

import android.view.View

fun View.isVisible(): Boolean {
    return (this.visibility == View.VISIBLE)
}

fun View.visible() {
    this.visibility = View.VISIBLE
}

fun View.invisible() {
    this.visibility = View.INVISIBLE
}

fun View.gone() {
    this.visibility = View.GONE
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/navigation/NavigationActivity.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/navigation/NavigationActivity.kt +79 -0
@@ 0,0 1,79 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.navigation

import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.core.extensions.viewBinding
import com.keylesspalace.tusky.databinding.ActivityNavigationBinding
import com.zhuinden.simplestack.SimpleStateChanger
import com.zhuinden.simplestack.StateChange
import com.zhuinden.simplestack.navigator.Navigator
import com.zhuinden.simplestackextensions.fragments.DefaultFragmentStateChanger
import timber.log.Timber

class NavigationActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {

    private val binding by viewBinding(ActivityNavigationBinding::inflate)
    private lateinit var fragmentStateChanger: DefaultFragmentStateChanger

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(binding.root)

        initNavigation()
    }

    override fun onBackPressed() {
        if(!Navigator.onBackPressed(this)) {
            Timber.i("No keys found, exiting the application.")

            this.finishAndRemoveTask()
        }
    }

    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }

    private fun initNavigation() {
        fragmentStateChanger = DefaultFragmentStateChanger(
            supportFragmentManager,
            binding.fragmentContainer.id
        )

        /*
        Navigator.configure()
            .setStateChanger(SimpleStateChanger(this))
            .setScopedServices(DefaultServiceProvider())
            //.setGlobalServices(GlobalServices(applicationContext).getGlobalServices())
            .install(
                this,
                binding.fragmentContainer,
                getHistoryKeys()
            )
        */
        Timber.d("Navigation setup completely")
    }

    private fun getHistoryKeys() {
    }
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/ui/fragment/BaseFragment.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/ui/fragment/BaseFragment.kt +23 -0
@@ 0,0 1,23 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.ui.fragment

class BaseFragment {
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/ui/navigation/BaseKey.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/ui/navigation/BaseKey.kt +29 -0
@@ 0,0 1,29 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.ui.navigation

import com.zhuinden.simplestackextensions.fragments.DefaultFragmentKey

abstract class BaseKey : DefaultFragmentKey() {

    override fun getFragmentTag(): String {
        return this.javaClass.simpleName
    }
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/ui/navigation/BaseServiceKey.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/ui/navigation/BaseServiceKey.kt +29 -0
@@ 0,0 1,29 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.ui.navigation

import com.zhuinden.simplestackextensions.services.DefaultServiceProvider

abstract class BaseServiceKey : BaseKey(), DefaultServiceProvider.HasServices {

    override fun getScopeTag(): String {
        return this.javaClass.simpleName
    }
}

A husky/app/src/main/java/com/keylesspalace/tusky/core/ui/viewmodel/BaseViewModel.kt => husky/app/src/main/java/com/keylesspalace/tusky/core/ui/viewmodel/BaseViewModel.kt +22 -0
@@ 0,0 1,22 @@
/*
 * Husky -- A Pleroma client for Android
 *
 * Copyright (C) 2021  The Husky Developers
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.keylesspalace.tusky.core.ui.viewmodel

abstract class BaseViewModel

A husky/app/src/main/res/layout/activity_navigation.xml => husky/app/src/main/res/layout/activity_navigation.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>