From ea7276ad711f62f4a09be8d49b2150cb648d525d Mon Sep 17 00:00:00 2001 From: Victor Albertos Gil Date: Thu, 13 Aug 2020 12:31:23 +0200 Subject: [PATCH] implement SocialConnect first iteration --- .gitignore | 2 + README.md | 128 ++++++++++++++++++ app/build.gradle | 16 ++- app/proguard-rules.pro | 2 + app/src/main/AndroidManifest.xml | 11 +- .../cookpad/com/sample/ConnectionsFragment.kt | 88 ++++++++++++ .../java/cookpad/com/sample/StartActivity.kt | 5 + .../main/res/layout/connections_fragment.xml | 26 ++++ app/src/main/res/layout/start_activity.xml | 16 +++ app/src/main/res/navigation/nav_graph.xml | 26 ++++ app/src/main/res/values/colors.xml | 7 +- app/src/main/res/values/strings.xml | 2 + bitrise.yml | 51 +++++++ build.gradle | 2 +- social-connect/build.gradle | 25 +++- social-connect/consumer-rules.pro | 2 + social-connect/src/main/AndroidManifest.xml | 2 +- .../com/socialconnect/ConnectFragment.kt | 75 ++++++++++ .../com/socialconnect/ConnectResult.kt | 19 +++ .../socialconnect/DefaultStrategyOAuth1.kt | 19 +++ .../socialconnect/DefaultStrategyOAuth2.kt | 19 +++ .../com/socialconnect/QueryStringStrategy.kt | 23 ++++ .../com/socialconnect/ServiceConfigs.kt | 40 ++++++ .../internal/ConnectViewModel.kt | 48 +++++++ .../internal/OAuthWebViewClient.kt | 37 +++++ .../socialconnect/internal/ServiceHelper.kt | 61 +++++++++ .../internal/ServiceHelperFactory.kt | 54 ++++++++ .../com/socialconnect/internal/ViewState.kt | 9 ++ .../src/main/res/layout/fragment_connect.xml | 5 + .../src/main/res/navigation/connect_graph.xml | 20 +++ .../com/socialconnect/ConnectViewModelTest.kt | 89 ++++++++++++ .../com/socialconnect/LiveDataTestUtils.kt | 76 +++++++++++ .../internal/ServiceHelperFactoryTest.kt | 82 +++++++++++ 33 files changed, 1069 insertions(+), 18 deletions(-) create mode 100644 README.md create mode 100644 app/src/main/java/cookpad/com/sample/ConnectionsFragment.kt create mode 100644 app/src/main/java/cookpad/com/sample/StartActivity.kt create mode 100644 app/src/main/res/layout/connections_fragment.xml create mode 100644 app/src/main/res/layout/start_activity.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 bitrise.yml create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/ConnectFragment.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/ConnectResult.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/DefaultStrategyOAuth1.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/DefaultStrategyOAuth2.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/QueryStringStrategy.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/ServiceConfigs.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/internal/ConnectViewModel.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/internal/OAuthWebViewClient.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/internal/ServiceHelper.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/internal/ServiceHelperFactory.kt create mode 100644 social-connect/src/main/java/cookpad/com/socialconnect/internal/ViewState.kt create mode 100644 social-connect/src/main/res/layout/fragment_connect.xml create mode 100644 social-connect/src/main/res/navigation/connect_graph.xml create mode 100644 social-connect/src/test/java/cookpad/com/socialconnect/ConnectViewModelTest.kt create mode 100644 social-connect/src/test/java/cookpad/com/socialconnect/LiveDataTestUtils.kt create mode 100644 social-connect/src/test/java/cookpad/com/socialconnect/internal/ServiceHelperFactoryTest.kt diff --git a/.gitignore b/.gitignore index f65321e..e65e73c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ /captures .externalNativeBuild .cxx +.build +app/src/main/java/cookpad/com/sample/Credentials.kt diff --git a/README.md b/README.md new file mode 100644 index 0000000..288ece3 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# SocialConnect +SocialConnect simplifies the process of retrieving OAuth tokens from multiple social networks on Android by leveraging the [Navigation component](https://developer.android.com/guide/navigation/navigation-getting-started). This library has as many providers as [ScribeJava](https://github.com/scribejava/scribejava) has, as SocialConnect is built on top of it. + +## Setup +Add the JitPack repository in your build.gradle (top level module): +```gradle +allprojects { + repositories { + maven { url "https://jitpack.io" } + } +} +``` + +Apply the SafeArgs plugin ([see how](https://developer.android.com/guide/navigation/navigation-pass-data)) and add next dependency in the build.gradle of your app module: +```gradle +dependencies { + compile 'com.github.cookpad:SocialConnect:0.0.2' +} +``` + +## Usage +`ConnectFragment` is the entry point of SocialConnect library. To use it, add the `connect_graph` as a destination to your nav graph file, as follow: + +```xml + + + + + + +``` + +This will generate `YourFragmentDirections.actionConnectionsFragmentToConnectFragment` which expects an `OAuthServiceConfig` instance and provides the `NavDirection` to access `ConnectFragment` from your nav graph. + +OAuth1 with Twitter provider: + +```kotlin +findNavController().navigate( + ConnectionsFragmentDirections.actionConnectionsFragmentToConnectFragment( + keyRequestCode = RESULT_CONNECTION_TWITTER, + serviceConfig = OAuth10ServiceConfig( + apiKey = API_KEY_TWITTER, + apiSecret = API_SECRET, + callback = CALLBACK_URL, + clazz = TwitterApi::class.java + ) + ) +) +``` + +OAuth2 with Github provider: + +```kotlin +findNavController().navigate( + ConnectionsFragmentDirections.actionConnectionsFragmentToConnectFragment( + keyRequestCode = RESULT_CONNECTION_GITHUB, + serviceConfig = OAuth20ServiceConfig( + apiKey = API_KEY_GITHUB, + apiSecret = API_SECRET_GITHUB, + callback = CALLBACK_URL, + clazz = GitHubApi::class.java + ) + ) +) +``` + +To retrieve the `Token` back, listen for changes on `findNavController().currentBackStackEntry?.savedStateHandle` expecting a type of `ConnectResult`: ([see](https://developer.android.com/guide/navigation/navigation-programmatic) for more info about how retrieve a result to the previous Destination with the navigation Component) + +```kotlin +findNavController().currentBackStackEntry?.savedStateHandle + ?.getLiveData(RESULT_CONNECTION_TWITTER) + ?.observe(viewLifecycleOwner, Observer { connectResult -> + processConnectResult(nameProvider = "Twitter", connectResult = connectResult) + }) + +findNavController().currentBackStackEntry?.savedStateHandle + ?.getLiveData(RESULT_CONNECTION_GITHUB) + ?.observe(viewLifecycleOwner, Observer { connectResult -> + processConnectResult(nameProvider = "Github", connectResult = connectResult) + }) + + +private fun processConnectResult(nameProvider: String, connectResult: ConnectResult) { + when (connectResult) { + is ConnectResultOk -> { + val token = connectResult.token + val message = when (token) { + is OAuth1AccessToken -> "Provider: $nameProvider, Token: ${token.token} Secret:${token.tokenSecret}" + is OAuth2AccessToken -> "Provider: $nameProvider, AccessToken: ${token.accessToken}" + else -> throw IllegalStateException(token::class.java.canonicalName) + } + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() + } + is ConnectResultError -> { + Toast.makeText(requireContext(), connectResult.error.message, Toast.LENGTH_LONG).show() + } + } +} +``` + +## Sample app +Check the `:app` Gradle module for a showcase of Twitter and Github providers. But prior to build the module, you need to provide the `app/src/main/java/cookpad/com/sample/Credentials.kt` file with the credentials of both your [Twitter](https://developer.twitter.com/en/apps) and your [Github](https://docs.github.com/en/developers/apps/creating-a-github-app) app: + +```kotlin +const val API_KEY_TWITTER = "" +const val API_SECRET = "" + +const val API_KEY_GITHUB = "" +const val API_SECRET_GITHUB = "" + +const val CALLBACK_URL = "" +``` + +## Proguard +Proguard is already handled via the `consumer-rules.pro` config file. + + +## Credits +Oauth core authentication: [ScribeJava](https://github.com/scribejava/scribejava) diff --git a/app/build.gradle b/app/build.gradle index 1966db1..47c60f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,14 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion 29 buildToolsVersion "30.0.0" defaultConfig { - applicationId "cookpad.com.socialconnect" + applicationId "cookpad.com.sample" minSdkVersion 21 targetSdkVersion 29 versionCode 1 @@ -25,12 +26,13 @@ android { } dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation project(path: ':social-connect') + + implementation 'androidx.navigation:navigation-fragment:2.3.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.appcompat:appcompat:1.1.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - + implementation 'androidx.appcompat:appcompat:1.2.0' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..6a1a4b3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,5 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keep class * extends androidx.fragment.app.Fragment{} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 676fecd..1acc02f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="cookpad.com.sample"> + android:theme="@style/AppTheme"> + + + + + + + diff --git a/app/src/main/java/cookpad/com/sample/ConnectionsFragment.kt b/app/src/main/java/cookpad/com/sample/ConnectionsFragment.kt new file mode 100644 index 0000000..f9cb64f --- /dev/null +++ b/app/src/main/java/cookpad/com/sample/ConnectionsFragment.kt @@ -0,0 +1,88 @@ +package cookpad.com.sample + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import com.github.scribejava.apis.GitHubApi +import com.github.scribejava.apis.TwitterApi +import com.github.scribejava.core.model.OAuth1AccessToken +import com.github.scribejava.core.model.OAuth2AccessToken +import cookpad.com.socialconnect.ConnectResult +import cookpad.com.socialconnect.ConnectResultError +import cookpad.com.socialconnect.ConnectResultOk +import cookpad.com.socialconnect.OAuth10ServiceConfig +import cookpad.com.socialconnect.OAuth20ServiceConfig +import kotlinx.android.synthetic.main.connections_fragment.bt_github +import kotlinx.android.synthetic.main.connections_fragment.bt_twitter + +class ConnectionsFragment : Fragment(R.layout.connections_fragment) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + bt_twitter.setOnClickListener { + findNavController().navigate( + ConnectionsFragmentDirections.actionConnectionsFragmentToConnectFragment( + keyRequestCode = RESULT_CONNECTION_TWITTER, + serviceConfig = OAuth10ServiceConfig( + apiKey = API_KEY_TWITTER, + apiSecret = API_SECRET, + callback = CALLBACK_URL, + clazz = TwitterApi::class.java + ) + ) + ) + } + + bt_github.setOnClickListener { + findNavController().navigate( + ConnectionsFragmentDirections.actionConnectionsFragmentToConnectFragment( + keyRequestCode = RESULT_CONNECTION_GITHUB, + serviceConfig = OAuth20ServiceConfig( + apiKey = API_KEY_GITHUB, + apiSecret = API_SECRET_GITHUB, + callback = CALLBACK_URL, + clazz = GitHubApi::class.java + ) + ) + ) + } + + findNavController().currentBackStackEntry?.savedStateHandle + ?.getLiveData(RESULT_CONNECTION_TWITTER) + ?.observe(viewLifecycleOwner, Observer { connectResult -> + processConnectResult(nameProvider = "Twitter", connectResult = connectResult) + }) + + findNavController().currentBackStackEntry?.savedStateHandle + ?.getLiveData(RESULT_CONNECTION_GITHUB) + ?.observe(viewLifecycleOwner, Observer { connectResult -> + processConnectResult(nameProvider = "Github", connectResult = connectResult) + }) + } + + private fun processConnectResult(nameProvider: String, connectResult: ConnectResult) { + when (connectResult) { + is ConnectResultOk -> { + val token = connectResult.token + val message = when (token) { + is OAuth1AccessToken -> "Provider: $nameProvider, Token: ${token.token} Secret:${token.tokenSecret}" + is OAuth2AccessToken -> "Provider: $nameProvider, AccessToken: ${token.accessToken}" + else -> throw IllegalStateException(token::class.java.canonicalName) + } + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() + } + is ConnectResultError -> { + Toast.makeText(requireContext(), connectResult.error.message, Toast.LENGTH_LONG).show() + } + } + } + + companion object { + const val RESULT_CONNECTION_TWITTER = "result_connection_twitter" + const val RESULT_CONNECTION_GITHUB = "result_connection_github" + } +} diff --git a/app/src/main/java/cookpad/com/sample/StartActivity.kt b/app/src/main/java/cookpad/com/sample/StartActivity.kt new file mode 100644 index 0000000..f5944f5 --- /dev/null +++ b/app/src/main/java/cookpad/com/sample/StartActivity.kt @@ -0,0 +1,5 @@ +package cookpad.com.sample + +import androidx.appcompat.app.AppCompatActivity + +class StartActivity : AppCompatActivity(R.layout.start_activity) diff --git a/app/src/main/res/layout/connections_fragment.xml b/app/src/main/res/layout/connections_fragment.xml new file mode 100644 index 0000000..c37504b --- /dev/null +++ b/app/src/main/res/layout/connections_fragment.xml @@ -0,0 +1,26 @@ + + + +