1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-09 20:42:34 +01:00

create retrofacebook and token retrieval

This commit is contained in:
Allan Wang 2017-05-30 01:03:01 -07:00
parent 461425eb60
commit 4c44dbc993
20 changed files with 392 additions and 28 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
/build /build
/captures /captures
.externalNativeBuild .externalNativeBuild
/app/src/main/res/values/strings_facebook.xml
/app/src/main/kotlin/com/pitchedapps/frost/facebook/Private.kt

View File

@ -76,6 +76,17 @@ dependencies {
compile "com.lapism:searchview:${SEARCH_VIEW}" compile "com.lapism:searchview:${SEARCH_VIEW}"
compile "org.jsoup:jsoup:${JSOUP}"
compile "com.facebook.android:facebook-android-sdk:${FB_SDK}"
compile "org.jetbrains.anko:anko:${ANKO}"
compile "com.squareup.retrofit2:retrofit:${RETROFIT}"
compile "com.squareup.retrofit2:adapter-rxjava2:${RETROFIT}"
compile "com.squareup.retrofit2:converter-gson:${RETROFIT}"
compile "com.squareup.okhttp3:logging-interceptor:${OKHTTP_INTERCEPTOR}"
compile "com.github.bumptech.glide:glide:${GLIDE}" compile "com.github.bumptech.glide:glide:${GLIDE}"
annotationProcessor "com.github.bumptech.glide:compiler:${GLIDE}" annotationProcessor "com.github.bumptech.glide:compiler:${GLIDE}"
} }

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.pitchedapps.frost"> package="com.pitchedapps.frost">
<!-- To auto-complete the email text field in the login form with the user's emails --> <!-- To auto-complete the email text field in the login form with the user's emails -->
@ -18,12 +19,12 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".FrostApp"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:name=".FrostApp"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".StartActivity" android:name=".StartActivity"
@ -91,6 +92,37 @@
android:scheme="https" /> android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".LoginActivity" />
<activity
android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
android:label="@string/app_name"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:replace="android:theme" />
<activity
android:name="com.facebook.CustomTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/fb_login_protocol_scheme" />
</intent-filter>
</activity>
<meta-data
android:name="com.facebook.sdk.ApplicationId"
android:value="@string/facebook_app_id" />
<meta-data
android:name="com.facebook.sdk.ApplicationName"
android:value="@string/facebook_app_name" />
<provider
android:name="com.facebook.FacebookContentProvider"
android:authorities="@string/facebook_authorities"
android:exported="true" />
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,21 @@
package com.pitchedapps.frost
import android.os.Bundle
import android.support.annotation.CallSuper
import android.support.v7.app.AppCompatActivity
import com.facebook.AccessToken
import com.pitchedapps.frost.utils.L
/**
* Created by Allan Wang on 2017-05-29.
*/
open class FbActivity : AppCompatActivity() {
var accessToken: AccessToken? = null
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accessToken = AccessToken.getCurrentAccessToken()
L.e("Access ${accessToken?.token}")
}
}

View File

@ -1,6 +1,8 @@
package com.pitchedapps.frost package com.pitchedapps.frost
import android.app.Application import android.app.Application
import com.pitchedapps.frost.facebook.retro.FrostApi
import com.pitchedapps.frost.facebook.retro.IFrost
import com.pitchedapps.frost.utils.CrashReportingTree import com.pitchedapps.frost.utils.CrashReportingTree
import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.Prefs
import io.realm.Realm import io.realm.Realm
@ -13,15 +15,11 @@ import timber.log.Timber.DebugTree
*/ */
class FrostApp : Application() { class FrostApp : Application() {
companion object {
lateinit var prefs: Prefs
}
override fun onCreate() { override fun onCreate() {
if (BuildConfig.DEBUG) Timber.plant(DebugTree()) if (BuildConfig.DEBUG) Timber.plant(DebugTree())
else Timber.plant(CrashReportingTree()) else Timber.plant(CrashReportingTree())
Prefs(applicationContext)
prefs = Prefs(applicationContext) FrostApi(applicationContext)
Realm.init(applicationContext) Realm.init(applicationContext)
super.onCreate() super.onCreate()
} }

View File

@ -0,0 +1,64 @@
package com.pitchedapps.frost
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Bundle
import android.widget.Button
import com.facebook.CallbackManager
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.login.LoginBehavior
import com.facebook.login.LoginManager
import com.facebook.login.LoginResult
import com.facebook.login.widget.LoginButton
import com.pitchedapps.frost.utils.L
import java.util.*
/**
* Created by Allan Wang on 2017-05-29.
*/
class LoginActivity : FbActivity() {
lateinit var callback: CallbackManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_teest)
val loginButton = findViewById(R.id.login_button) as LoginButton
loginButton.loginBehavior = LoginBehavior.WEB_VIEW_ONLY
loginButton.setReadPermissions("email")
val switchh = findViewById(R.id.switchh) as Button
switchh.setOnClickListener {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
// If using in a fragment
// loginButton.setFragment(this)
// Other app specific specialization
// Callback registration
callback = CallbackManager.Factory.create()
loginButton.registerCallback(callback, object : FacebookCallback<LoginResult> {
override fun onSuccess(loginResult: LoginResult) {
L.e("Success")
L.e("Success ${loginResult.accessToken.token}")
}
override fun onCancel() {
// App code
L.e("Cancel")
}
override fun onError(exception: FacebookException) {
// App code
L.e("Error")
}
})
// LoginManager.getInstance().logInWithReadPermissions(this, Arrays.asList("public_profile"));
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
callback.onActivityResult(requestCode, resultCode, data)
}
}

View File

@ -1,5 +1,6 @@
package com.pitchedapps.frost package com.pitchedapps.frost
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.FloatingActionButton import android.support.design.widget.FloatingActionButton
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
@ -69,7 +70,10 @@ class MainActivity : AppCompatActivity(), KeyPairObservable {
// automatically handle clicks on the Home/Up button, so long // automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml. // as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) { when (item.itemId) {
R.id.action_settings -> return true R.id.action_settings -> {
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
R.id.action_changelog -> Changelog.show(this) R.id.action_changelog -> Changelog.show(this)
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }

View File

@ -11,7 +11,7 @@ class StartActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, LoginActivity::class.java))
finish() finish()
} }
} }

View File

@ -0,0 +1,13 @@
package com.pitchedapps.frost.facebook
import com.facebook.AccessToken
/**
* Created by Allan Wang on 2017-05-30.
*/
val token: String?
get() = AccessToken.getCurrentAccessToken()?.token
fun setToken() {
}

View File

@ -18,6 +18,7 @@ import io.realm.annotations.PrimaryKey
* Created by Allan Wang on 2017-05-29. * Created by Allan Wang on 2017-05-29.
*/ */
enum class FbUrl(@StringRes val titleId: Int, val icon: IIcon, val url: String) { enum class FbUrl(@StringRes val titleId: Int, val icon: IIcon, val url: String) {
LOGIN(R.string.feed, CommunityMaterial.Icon.cmd_newspaper, "https://www.facebook.com/v2.9/dialog/oauth?client_id=$FB_KEY&redirect_uri=https://touch.facebook.com/&response_type=token,granted_scopes"),
FEED(R.string.feed, CommunityMaterial.Icon.cmd_newspaper, "https://touch.facebook.com/"), FEED(R.string.feed, CommunityMaterial.Icon.cmd_newspaper, "https://touch.facebook.com/"),
PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "https://touch.facebook.com/me/"), PROFILE(R.string.profile, CommunityMaterial.Icon.cmd_account, "https://touch.facebook.com/me/"),
EVENTS(R.string.events, GoogleMaterial.Icon.gmd_event, "https://touch.facebook.com/events/upcoming"), EVENTS(R.string.events, GoogleMaterial.Icon.gmd_event, "https://touch.facebook.com/events/upcoming"),
@ -60,6 +61,6 @@ fun loadFbTab(c: Context): List<FbTab> {
val realmList = mutableListOf<FbTabRealm>() val realmList = mutableListOf<FbTabRealm>()
realm(RealmFiles.TABS, Realm.Transaction { it.copyFromRealm(realmList) }) realm(RealmFiles.TABS, Realm.Transaction { it.copyFromRealm(realmList) })
if (realmList.isNotEmpty()) return realmList.map { FbTab(it) } if (realmList.isNotEmpty()) return realmList.map { FbTab(it) }
return FbUrl.values().map { it.tabInfo(c) } return listOf(FbUrl.FEED, FbUrl.MESSAGES, FbUrl.NOTIFICATIONS).map { it.tabInfo(c) }
} }

View File

@ -0,0 +1,52 @@
package com.pitchedapps.frost.facebook.retro
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.google.gson.GsonBuilder
import com.pitchedapps.frost.BuildConfig
import io.reactivex.schedulers.Schedulers
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
/**
* Created by Allan Wang on 2017-05-30.
*
* API for data retrieval
*/
object FrostApi {
internal lateinit var frostApi: IFrost
operator fun invoke(context: Context) {
val cacheDir = File(context.cacheDir, "responses")
val cacheSize = 5L * 1024 * 1024 //10MiB
val cache = Cache(cacheDir, cacheSize)
val client = OkHttpClient.Builder()
.addInterceptor(FrostInterceptor(context))
.cache(cache)
//add logger and stetho last
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") { //log if not full release
client.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
client.addNetworkInterceptor(StethoInterceptor())
}
val gson = GsonBuilder().setLenient()
val retrofit = Retrofit.Builder()
.baseUrl("https://graph.facebook.com/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.addConverterFactory(GsonConverterFactory.create(gson.create()))
.client(client.build())
.build();
frostApi = retrofit.create(IFrost::class.java)
}
}

View File

@ -0,0 +1,8 @@
package com.pitchedapps.frost.facebook.retro
/**
* Created by Allan Wang on 2017-05-30.
*
* Collection of Graph API outputs
*/
data class Me(val name: String, val id: String)

View File

@ -0,0 +1,29 @@
package com.pitchedapps.frost.facebook.retro
import android.content.Context
import com.pitchedapps.frost.facebook.token
import com.pitchedapps.frost.utils.Utils
import okhttp3.Interceptor
import okhttp3.Response
/**
* Created by Allan Wang on 2017-05-30.
*/
private val maxStale = 60 * 60 * 24 * 28 //maxAge to get from cache if online (4 weeks)
const val ACCESS_TOKEN = "access_token"
class FrostInterceptor(val context: Context) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response? {
val request = chain.request()
val requestBuilder = request.newBuilder()
val urlBase = request.url()
val urlWithToken = urlBase.newBuilder()
if (urlBase.queryParameter(ACCESS_TOKEN) == null && token != null)
urlWithToken.addQueryParameter(ACCESS_TOKEN, token)
requestBuilder.url(urlWithToken.build())
if (!Utils.isNetworkAvailable(context)) requestBuilder.addHeader("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
return chain.proceed(requestBuilder.build())
}
}

View File

@ -0,0 +1,16 @@
package com.pitchedapps.frost.facebook.retro
import com.pitchedapps.frost.facebook.token
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
/**
* Created by Allan Wang on 2017-05-30.
*/
interface IFrost {
@GET("me")
fun me(@Query(ACCESS_TOKEN) accessToken: String? = token): Call<Me>
}

View File

@ -1,30 +1,36 @@
package com.pitchedapps.frost.utils package com.pitchedapps.frost.utils
import com.pitchedapps.frost.FrostApp import android.content.Context
import android.content.SharedPreferences
/** /**
* Created by Allan Wang on 2017-05-28. * Created by Allan Wang on 2017-05-28.
*/ */
val prefs: Prefs by lazy { FrostApp.prefs }
class Prefs(c: android.content.Context) { private val PREFERENCE_NAME = "${com.pitchedapps.frost.BuildConfig.APPLICATION_ID}.prefs"
private companion object { private val LAST_ACTIVE = "last_active"
val PREFERENCE_NAME = "${com.pitchedapps.frost.BuildConfig.APPLICATION_ID}.prefs"
val LAST_ACTIVE = "last_active" object Prefs {
val prefs: Prefs by lazy { this }
lateinit private var c: Context
operator fun invoke(c: Context) {
this.c = c
} }
private val prefs: android.content.SharedPreferences by lazy { c.getSharedPreferences(com.pitchedapps.frost.utils.Prefs.Companion.PREFERENCE_NAME, android.content.Context.MODE_PRIVATE) } private val sp: SharedPreferences by lazy { c.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) }
var lastActive: Long var lastActive: Long
get() = prefs.getLong(com.pitchedapps.frost.utils.Prefs.Companion.LAST_ACTIVE, -1) get() = sp.getLong(LAST_ACTIVE, -1)
set(value) = set(com.pitchedapps.frost.utils.Prefs.Companion.LAST_ACTIVE, System.currentTimeMillis()) set(value) = set(LAST_ACTIVE, System.currentTimeMillis())
init { init {
lastActive = 0 lastActive = 0
} }
private fun set(key: String, value: Boolean) = prefs.edit().putBoolean(key, value).apply() private fun set(key: String, value: Boolean) = sp.edit().putBoolean(key, value).apply()
private fun set(key: String, value: Int) = prefs.edit().putInt(key, value).apply() private fun set(key: String, value: Int) = sp.edit().putInt(key, value).apply()
private fun set(key: String, value: Long) = prefs.edit().putLong(key, value).apply() private fun set(key: String, value: Long) = sp.edit().putLong(key, value).apply()
private fun set(key: String, value: String) = prefs.edit().putString(key, value).apply() private fun set(key: String, value: String) = sp.edit().putString(key, value).apply()
} }

View File

@ -1,6 +1,8 @@
package com.pitchedapps.frost.utils package com.pitchedapps.frost.utils
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.ConnectivityManager
/** /**
* Created by Allan Wang on 2017-05-28. * Created by Allan Wang on 2017-05-28.
@ -8,4 +10,10 @@ import android.content.res.Resources
object Utils { object Utils {
fun dpToPx(dp: Int) = (dp * android.content.res.Resources.getSystem().displayMetrics.density).toInt() fun dpToPx(dp: Int) = (dp * android.content.res.Resources.getSystem().displayMetrics.density).toInt()
fun pxToDp(px:Int) = (px / android.content.res.Resources.getSystem().displayMetrics.density).toInt() fun pxToDp(px:Int) = (px / android.content.res.Resources.getSystem().displayMetrics.density).toInt()
fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetworkInfo = connectivityManager.activeNetworkInfo
return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
}
} }

View File

@ -68,6 +68,7 @@ class FrostWebView @JvmOverloads constructor(
super.onPageFinished(view, url) super.onPageFinished(view, url)
observable.onNext(WebStatus.LOADED) observable.onNext(WebStatus.LOADED)
// CookieManager.getInstance().flush() // CookieManager.getInstance().flush()
L.d("Loaded $url")
} }
}) })
} }

View File

@ -0,0 +1,78 @@
package com.pitchedapps.frost.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.UrlQuerySanitizer
import android.util.AttributeSet
import android.view.View
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import com.facebook.AccessToken
import com.pitchedapps.frost.facebook.FB_KEY
import com.pitchedapps.frost.facebook.retro.FrostApi.frostApi
import com.pitchedapps.frost.facebook.retro.Me
import com.pitchedapps.frost.utils.L
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
/**
* Created by Allan Wang on 2017-05-29.
*/
class LoginWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
init {
setupWebview()
}
@SuppressLint("SetJavaScriptEnabled")
fun setupWebview() {
settings.javaScriptEnabled = true
setLayerType(View.LAYER_TYPE_HARDWARE, null)
setWebViewClient(object : WebViewClient() {
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
super.onReceivedError(view, request, error)
L.e("Error ${request}")
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
L.d("Loading $url")
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (url == null) return
val sanitizer = UrlQuerySanitizer(url)
val accessToken = sanitizer.getValue("access_token")
val expiresIn = sanitizer.getValue("expires_in")
val grantedScopes = sanitizer.getValue("granted_scopes")
val deniedScopes = sanitizer.getValue("deniedScopes")
L.d("Loaded $url")
}
})
}
fun saveAccessToken(accessToken: String, expiresIn: String, grantedScopes: String?, deniedScopes: String?) {
L.d("Granted $grantedScopes")
L.d("Denied $deniedScopes")
frostApi.me(accessToken).enqueue(object : Callback<Me> {
override fun onFailure(call: Call<Me>?, t: Throwable?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onResponse(call: Call<Me>, response: Response<Me>) {
AccessToken.setCurrentAccessToken(AccessToken(accessToken, FB_KEY.toString(), response.body().id, null, null, null, null, null))
}
})
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.facebook.login.widget.LoginButton
android:id="@+id/login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="30dp"
android:layout_marginTop="30dp" />
<Button
android:id="@+id/switchh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="switch" />
</LinearLayout>

View File

@ -1,16 +1,12 @@
# Project-wide Gradle settings. # Project-wide Gradle settings.
# IDE (e.g. Android Studio) users: # IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override* # Gradle settings configured through the IDE *will override*
# any settings specified in this file. # any settings specified in this file.
# For more details on how to configure your build environment visit # For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@ -22,7 +18,6 @@ TARGET_SDK=25
BUILD_TOOLS=25.0.2 BUILD_TOOLS=25.0.2
VERSION_CODE=1 VERSION_CODE=1
VERSION_NAME=0.1 VERSION_NAME=0.1
ANDROID_SUPPORT_LIBS=25.3.1 ANDROID_SUPPORT_LIBS=25.3.1
TIMBER=4.5.1 TIMBER=4.5.1
MD=0.9.4.3 MD=0.9.4.3
@ -35,5 +30,10 @@ SEARCH_VIEW=4.0
RX_JAVA=2.0.7 RX_JAVA=2.0.7
RX_ANDROID=2.0.1 RX_ANDROID=2.0.1
RX_BINDING=2.0.0 RX_BINDING=2.0.0
JSOUP=1.10.2
FB_SDK=[4,5)
STETHO=1.4.2 STETHO=1.4.2
ANKO=0.10.0
GLIDE=4.0.0-RC0 GLIDE=4.0.0-RC0
RETROFIT=2.2.0
OKHTTP_INTERCEPTOR=3.6.0