├── core
├── .gitignore
├── src
│ └── main
│ │ ├── java
│ │ └── nl
│ │ │ └── adaptivity
│ │ │ └── android
│ │ │ ├── coroutines
│ │ │ ├── impl
│ │ │ │ └── DelegateLayoutContainer.kt
│ │ │ ├── contexts
│ │ │ │ ├── FragmentContext.kt
│ │ │ │ └── AndroidContext.kt
│ │ │ ├── SimpleContextCoroutineScopeWrapper.kt
│ │ │ ├── CoroutineFragment.kt
│ │ │ ├── RetainedContinuationFragment.kt
│ │ │ ├── RequestPermissionContinuationFragment.kt
│ │ │ ├── ActivityCoroutineScopeWrapper.kt
│ │ │ ├── launchers.kt
│ │ │ ├── AndroidContextCoroutineScope.kt
│ │ │ ├── FragmentCoroutineScope.kt
│ │ │ ├── BaseRetainedContinuationFragment.kt
│ │ │ ├── WrappedContextCoroutineScope.kt
│ │ │ ├── Maybe.kt
│ │ │ ├── FragmentCoroutineScopeWrapper.kt
│ │ │ ├── SuspendableDialog.kt
│ │ │ ├── ParcelableContinuationCompat.kt
│ │ │ ├── accountmanager.kt
│ │ │ ├── CoroutineActivity.kt
│ │ │ ├── ParcelableContinuation.kt
│ │ │ └── DownloadFragment.kt
│ │ │ ├── kryo
│ │ │ ├── serializers
│ │ │ │ ├── KryoAndroidConstants.kt
│ │ │ │ ├── CoroutineImplSerializer.kt
│ │ │ │ ├── ContinuationImplSerializer.kt
│ │ │ │ ├── StandaloneCoroutineSerializer.kt
│ │ │ │ ├── PseudoObjectSerializer.kt
│ │ │ │ ├── ObjectSerializer.kt
│ │ │ │ ├── ReferenceSerializer.kt
│ │ │ │ ├── InitialResultSerializer.kt
│ │ │ │ ├── ContextSerializer.kt
│ │ │ │ ├── FragmentSerializer.kt
│ │ │ │ ├── SupportFragmentSerializer.kt
│ │ │ │ └── SafeContinuationSerializer.kt
│ │ │ ├── KotlinObjectInstantiatorStrategy.kt
│ │ │ ├── ParcelOutput.kt
│ │ │ ├── ParcelInput.kt
│ │ │ ├── KryoIO.kt
│ │ │ ├── KryoParcelable.kt
│ │ │ └── AndroidKotlinResolver.kt
│ │ │ └── util
│ │ │ └── GrantResult.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── testapp
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_test6.xml
│ │ │ │ ├── activity_test7.xml
│ │ │ │ ├── activity_test2.xml
│ │ │ │ ├── activity_test1.xml
│ │ │ │ └── fragment_test6.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── nl
│ │ │ │ └── adaptivity
│ │ │ │ └── android
│ │ │ │ ├── test
│ │ │ │ ├── TestActivity2.kt
│ │ │ │ ├── TestActivity1.kt
│ │ │ │ ├── TestActivity4.kt
│ │ │ │ ├── TestActivity5.kt
│ │ │ │ ├── TestActivity3.kt
│ │ │ │ ├── TestActivity6.kt
│ │ │ │ └── TestActivity7.kt
│ │ │ │ └── kryo
│ │ │ │ └── LineOutput.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── nl
│ │ │ └── adaptivity
│ │ │ └── android
│ │ │ └── test
│ │ │ ├── PlainCoroutineTestAndroid.kt
│ │ │ └── TestActivity1Test.kt
│ └── test
│ │ └── java
│ │ └── uk
│ │ └── ac
│ │ └── bmth
│ │ └── aprog
│ │ └── testapp
│ │ └── PlainCoroutineTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── _config.yml
├── appcompat
├── .gitignore
├── src
│ └── main
│ │ ├── java
│ │ └── nl
│ │ │ └── adaptivity
│ │ │ └── android
│ │ │ └── coroutinesCompat
│ │ │ ├── AppcompatFragmentContext.kt
│ │ │ ├── AppcompatCoroutineFragment.kt
│ │ │ ├── AppcompatLaunchers.kt
│ │ │ ├── CompatCoroutineActivity.kt
│ │ │ ├── AppcompatFragmentCoroutineScope.kt
│ │ │ └── AppcompatFragmentCoroutineScopeWrapper.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── proguard-project.txt
├── settings.gradle.kts
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | *.iml
--------------------------------------------------------------------------------
/testapp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/appcompat/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | *.iml
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | local.properties
2 | /.gradle
3 | /build
4 | /gen
5 | /bin
6 | *.iml
7 | /.idea/*
8 | /wiki
9 | /pages
10 | /testapp/build
11 |
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pdvrieze/android-coroutines/HEAD/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/testapp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/impl/DelegateLayoutContainer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines.impl
2 |
3 | import android.view.View
4 | import kotlinx.android.extensions.LayoutContainer
5 |
6 | class DelegateLayoutContainer(override val containerView: View?) :
7 | LayoutContainer
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/testapp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TestApp
3 | Cancelled
4 | Restored
5 | Test activity callback
6 | Test activity coroutine
7 |
8 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/KryoAndroidConstants.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | internal enum class KryoAndroidConstants {
4 | UNDECIDED,
5 | RESUMED,
6 | COROUTINE_SUSPENDED,
7 | APPLICATIONCONTEXT,
8 | OTHERCONTEXT,
9 | CONTEXT,
10 | FRAGMENTBYTAG,
11 | FRAGMENTBYID,
12 | FRAGMENTWITHOUTHANDLE
13 | }
--------------------------------------------------------------------------------
/testapp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.configureondemand=false
2 | android.useAndroidX=true
3 | kotlin.code.style=official
4 | kotlinVersion=1.5.20
5 | kryoVersion=4.0.2
6 | coroutinesVersion=1.5.0
7 | androidBuildToolsVersion=4.1.3
8 | dokkaVersion=1.4.30
9 | bintrayVersion=1.8.3
10 | selfVersion=0.7.992
11 | constraintLayoutVersion=1.1.3
12 | reqMinSdkVersion=16
13 | reqTargetSdkVersion=30
14 | reqCompileSdkVersion=30
15 | androidCompatVersion=28.0.0
16 | junitVersion=4.12
17 | espressoCoreVersion=3.1.0
18 | androidTestSupportVersion=1.1.0
19 |
--------------------------------------------------------------------------------
/testapp/src/main/res/layout/activity_test6.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/testapp/src/main/res/layout/activity_test7.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/contexts/FragmentContext.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines.contexts
2 |
3 | import android.app.Fragment
4 | import java.io.Serializable
5 | import kotlin.coroutines.AbstractCoroutineContextElement
6 | import kotlin.coroutines.CoroutineContext
7 |
8 | class FragmentContext(fragment: F) :
9 | AbstractCoroutineContextElement(FragmentContext) {
10 | var fragment = fragment
11 | internal set
12 |
13 | companion object Key : CoroutineContext.Key>,
14 | Serializable
15 |
16 | override fun toString(): String = "FragmentContext"
17 |
18 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/contexts/AndroidContext.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines.contexts
2 |
3 | import android.content.Context
4 | import java.io.Serializable
5 | import kotlin.coroutines.AbstractCoroutineContextElement
6 | import kotlin.coroutines.CoroutineContext
7 |
8 | class AndroidContext(androidContext: C) :
9 | AbstractCoroutineContextElement(AndroidContext) {
10 | var androidContext = androidContext
11 | internal set
12 |
13 | companion object Key : CoroutineContext.Key>,
14 | Serializable
15 |
16 | override fun toString(): String = "AndroidContext"
17 |
18 | }
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/AppcompatFragmentContext.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.support.v4.app.Fragment
4 | import java.io.Serializable
5 | import kotlin.coroutines.AbstractCoroutineContextElement
6 | import kotlin.coroutines.CoroutineContext
7 |
8 |
9 | class AppcompatFragmentContext(fragment: F) :
10 | AbstractCoroutineContextElement(AppcompatFragmentContext) {
11 | var fragment = fragment
12 | internal set
13 |
14 | companion object Key : CoroutineContext.Key>,
15 | Serializable
16 |
17 | override fun toString(): String = "AppcompatFragmentContext"
18 |
19 | }
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/AppcompatCoroutineFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.support.v4.app.Fragment
4 | import kotlinx.coroutines.*
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | open class AppcompatCoroutineFragment>: Fragment(),
8 | AppcompatFragmentCoroutineScope {
9 | override val coroutineContext: CoroutineContext = Job() + Dispatchers.Default + AppcompatFragmentContext(this)
10 |
11 | @Suppress("UNCHECKED_CAST")
12 | override val fragment: F get() = this as F
13 |
14 | override fun onDestroy() {
15 | coroutineContext.cancel(CancellationException("Fragment is being destroyed"))
16 | super.onDestroy()
17 | }
18 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/SimpleContextCoroutineScopeWrapper.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.content.Context
4 | import kotlinx.coroutines.CoroutineScope
5 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
6 |
7 | class SimpleContextCoroutineScopeWrapper(
8 | parentScope: CoroutineScope
9 | ) :
10 | WrappedContextCoroutineScope>(parentScope) {
11 |
12 | override fun getAndroidContext() = coroutineContext[AndroidContext]!!.androidContext as C
13 |
14 | override fun createScopeWrapper(parentScope: CoroutineScope): SimpleContextCoroutineScopeWrapper {
15 | return SimpleContextCoroutineScopeWrapper(parentScope)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/CoroutineFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Fragment
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.cancel
7 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | open class CoroutineFragment>: Fragment(),
11 | FragmentCoroutineScope {
12 | override val coroutineContext: CoroutineContext = Job() + Dispatchers.Default + FragmentContext(this)
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | override val fragment: F get() = this as F
16 |
17 | override fun onDestroy() {
18 | super.onDestroy()
19 | coroutineContext.cancel()
20 | }
21 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/CoroutineImplSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.io.Input
5 | import com.esotericsoftware.kryo.io.Output
6 | import com.esotericsoftware.kryo.serializers.FieldSerializer
7 | import com.esotericsoftware.kryo.serializers.FieldSerializerConfig
8 |
9 | internal class CoroutineImplSerializer(kryo: Kryo, type: Class<*>): FieldSerializer(kryo, type, null, FieldSerializerConfig().apply { isIgnoreSyntheticFields=false }) {
10 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
11 | super.write(kryo, output, obj)
12 | }
13 |
14 | override fun read(kryo: Kryo, input: Input, type: Class): Any {
15 | return super.read(kryo, input, type)
16 | }
17 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/ContinuationImplSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.io.Input
5 | import com.esotericsoftware.kryo.io.Output
6 | import com.esotericsoftware.kryo.serializers.FieldSerializer
7 | import com.esotericsoftware.kryo.serializers.FieldSerializerConfig
8 |
9 | internal class ContinuationImplSerializer(kryo: Kryo, type: Class<*>): FieldSerializer(kryo, type, null, FieldSerializerConfig().apply { isIgnoreSyntheticFields=false }) {
10 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
11 | super.write(kryo, output, obj)
12 | }
13 |
14 | override fun read(kryo: Kryo, input: Input, type: Class): Any {
15 | return super.read(kryo, input, type)
16 | }
17 | }
--------------------------------------------------------------------------------
/testapp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/appcompat/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/AppcompatLaunchers.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.content.Intent
4 | import nl.adaptivity.android.coroutines.FragmentCoroutineScopeWrapper
5 | import android.support.v4.app.Fragment as SupportFragment
6 |
7 |
8 | @Suppress("unused", "DEPRECATION")
9 | inline fun SupportFragment.startActivityForResult(requestCode: Int) = this.startActivityForResult(Intent(activity, A::class.java), requestCode)
10 |
11 |
12 | @Suppress("unused", "DEPRECATION")
13 | inline fun SupportFragment.startActivity() = startActivity(Intent(activity, A::class.java))
14 |
15 |
16 |
17 | @Suppress("unused")
18 | suspend inline fun AppcompatFragmentCoroutineScopeWrapper<*>.startActivityForResult() =
19 | startActivityForResult(Intent(fragment.activity, A::class.java))
20 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity2.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import kotlinx.android.synthetic.main.activity_test2.*
7 |
8 | /**
9 | * Simple activity that has a text box that can be passed as result.
10 | */
11 | class TestActivity2 : Activity() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContentView(R.layout.activity_test2)
16 | button2.setOnClickListener { _ ->
17 | val result = Intent("result").apply { putExtra(KEY_DATA, textView2.text) }
18 | this@TestActivity2.setResult(Activity.RESULT_OK, result)
19 | finish()
20 | }
21 | }
22 |
23 | companion object {
24 | const val KEY_DATA="data"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
20 |
--------------------------------------------------------------------------------
/appcompat/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
20 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/StandaloneCoroutineSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.io.Input
5 | import com.esotericsoftware.kryo.io.Output
6 | import com.esotericsoftware.kryo.serializers.FieldSerializer
7 |
8 | internal class StandaloneCoroutineSerializer(kryo: Kryo, type: Class<*>): FieldSerializer(kryo, type) {
9 | private val _parentContext: CachedField<*> = fields.first { it.field.declaringClass == type && it.field.name == "parentContext" }.also { removeField(it) }
10 |
11 | override fun create(kryo: Kryo, input: Input, type: Class): Any {
12 | val parentContext = kryo.readClassAndObject(input)
13 | return type.constructors.first().apply { isAccessible=true }.newInstance(parentContext, true)
14 | }
15 |
16 | override fun write(kryo: Kryo, output: Output, obj: Any) {
17 | _parentContext.write(output, obj)
18 |
19 | super.write(kryo, output, obj)
20 | }
21 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/PseudoObjectSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.Serializer
5 | import com.esotericsoftware.kryo.io.Input
6 | import com.esotericsoftware.kryo.io.Output
7 |
8 | /**
9 | * Serializer for Kotlin objects that stores nothing and just retrieves the current instance from
10 | * the field.
11 | */
12 | internal class PseudoObjectSerializer(kryo: Kryo, val type: Class, val value: T): Serializer(false, true) {
13 | /**
14 | * The correct way of getting an object is getting it's instance.
15 | */
16 | override fun read(kryo: Kryo, input: Input, type: Class): T {
17 | return value
18 | }
19 |
20 | override fun write(kryo: Kryo, output: Output, obj: T?) {
21 | // The class is already written by the caller so no need to write anything
22 | }
23 | }
24 |
25 | internal inline fun Kryo.pseudoObjectSerializer(value:T) = PseudoObjectSerializer(this, T::class.java, value)
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/KotlinObjectInstantiatorStrategy.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import org.objenesis.instantiator.ObjectInstantiator
4 | import org.objenesis.strategy.InstantiatorStrategy
5 | import java.lang.reflect.Modifier
6 |
7 | class KotlinObjectInstantiatorStrategy(private val fallback: InstantiatorStrategy) : InstantiatorStrategy {
8 |
9 | class KotlinObjectInstantiator(type: Class): ObjectInstantiator {
10 | @Suppress("UNCHECKED_CAST")
11 | private val objectInstance = type.getField("INSTANCE").get(null) as T
12 |
13 | override fun newInstance() = objectInstance
14 | }
15 |
16 | override fun newInstantiatorOf(type: Class): ObjectInstantiator {
17 | if (type.isKObject) {
18 | return KotlinObjectInstantiator(type)
19 | } else {
20 | return fallback.newInstantiatorOf(type)
21 | }
22 | }
23 | }
24 |
25 | internal val Class<*>.isKObject: Boolean get() {
26 | return Modifier.isFinal(modifiers) && constructors.isEmpty() && fields.any { it.name=="INSTANCE" && Modifier.isStatic(it.modifiers) }
27 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/RetainedContinuationFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 |
6 | class RetainedContinuationFragment : BaseRetainedContinuationFragment>() {
7 |
8 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
9 | when {
10 | requestCode != this.requestCode && this.requestCode!=-1 -> super.onActivityResult(requestCode, resultCode, data)
11 | resultCode == Activity.RESULT_OK -> dispatchResult(Maybe.Ok(data), requestCode)
12 | resultCode == Activity.RESULT_CANCELED -> dispatchResult(Maybe.cancelled(), requestCode)
13 | else -> super.onActivityResult(requestCode, resultCode, data)
14 | }
15 | }
16 |
17 | companion object {
18 | const val TAG = "__RETAINED_CONTINUATION_FRAGMENT__"
19 | }
20 | }
21 |
22 | @Suppress("FunctionName")
23 | fun RetainedContinuationFragment(activityContinuation: ParcelableContinuation>) = RetainedContinuationFragment().also {
24 | it.addContinuation(activityContinuation)
25 | }
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/CompatCoroutineActivity.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.support.v7.app.AppCompatActivity
4 | import kotlinx.coroutines.*
5 | import nl.adaptivity.android.coroutines.ActivityCoroutineScopeWrapper
6 | import nl.adaptivity.android.coroutines.AndroidContextCoroutineScope
7 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | open class CompatCoroutineActivity>: AppCompatActivity(),
11 | AndroidContextCoroutineScope> {
12 |
13 | override fun getAndroidContext(): A = this as A
14 |
15 | override val coroutineContext: CoroutineContext =
16 | Job() + Dispatchers.Default + AndroidContext(this)
17 |
18 | override fun createScopeWrapper(parentScope: CoroutineScope): ActivityCoroutineScopeWrapper {
19 | return ActivityCoroutineScopeWrapper(parentScope)
20 | }
21 |
22 | override fun onDestroy() {
23 | coroutineContext.cancel(CancellationException("Activity is being destroyed"))
24 | super.onDestroy()
25 | }
26 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/RequestPermissionContinuationFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.content.Intent
4 | import android.content.pm.PackageManager
5 | import nl.adaptivity.android.util.GrantResult
6 |
7 | class RequestPermissionContinuationFragment : BaseRetainedContinuationFragment() {
8 |
9 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
10 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
11 | when {
12 | requestCode != this.requestCode -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
13 | grantResults.isEmpty() || grantResults.all { it==PackageManager.PERMISSION_DENIED } -> dispatchResult(null, requestCode)
14 | else -> dispatchResult(GrantResult(permissions, grantResults), requestCode)
15 | }
16 | }
17 |
18 | companion object {
19 | const val TAG = "__REQUEST_PERMISSION_CONTINUATION_FRAGMENT__"
20 | }
21 | }
22 |
23 | @Suppress("FunctionName")
24 | fun RequestPermissionContinuationFragment(activityContinuation: ParcelableContinuation) = RequestPermissionContinuationFragment().also {
25 | it.addContinuation(activityContinuation)
26 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/ParcelOutput.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import android.os.Parcel
4 | import com.esotericsoftware.kryo.io.Output
5 | import java.io.OutputStream
6 |
7 | /**
8 | * Output class that uses a parcel to serialize. Perhaps not sustainable and ByteArrayStreams are better.
9 | */
10 | class ParcelOutput: Output {
11 | private val parcel: Parcel
12 |
13 | @JvmOverloads
14 | constructor(parcel: Parcel, bufferSize: Int = DEFAULT_BUFFER) : super(bufferSize) { this.parcel = parcel }
15 | constructor(parcel: Parcel, bufferSize: Int, maxBufferSize: Int) : super(bufferSize, maxBufferSize) { this.parcel = parcel }
16 | constructor(parcel: Parcel, buffer: ByteArray?) : super(buffer) { this.parcel = parcel }
17 | constructor(parcel: Parcel, buffer: ByteArray?, maxBufferSize: Int) : super(buffer, maxBufferSize) { this.parcel = parcel }
18 |
19 | override fun setOutputStream(outputStream: OutputStream) {
20 | throw UnsupportedOperationException("ParcelInput writes to parcels, not streams")
21 | }
22 |
23 | override fun flush() {
24 | for(i in 0 until position) {
25 | parcel.writeByte(buffer[i])
26 | }
27 | }
28 |
29 | override fun close() {
30 | // Do nothing
31 | }
32 |
33 | companion object {
34 | const val DEFAULT_BUFFER = 1024
35 | }
36 | }
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity1.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import kotlinx.android.synthetic.main.activity_test1.*
8 | import nl.adaptivity.android.coroutines.Maybe
9 | import nl.adaptivity.android.coroutines.withActivityResult
10 |
11 | /**
12 | * Version of the test activity that uses a callback rather than a coroutine.
13 | */
14 | @SuppressLint("RestrictedApi")
15 | class TestActivity1 : Activity() {
16 |
17 | private val resultHandler2: TestActivity1.(Maybe) -> Unit = { result ->
18 | result.onOk { data -> textView.text = data?.getCharSequenceExtra(TestActivity2.KEY_DATA)}
19 | result.onCancelled { textView.text = getString(R.string.lbl_cancelled) }
20 | }
21 |
22 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
23 | super.onRestoreInstanceState(savedInstanceState)
24 | restoredView.text = getString(R.string.lbl_restored)
25 | }
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | setContentView(R.layout.activity_test1)
30 | button.setOnClickListener({ _ ->
31 |
32 | withActivityResult(Intent(this@TestActivity1, TestActivity2::class.java), resultHandler2)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/testapp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/ParcelInput.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import android.os.Parcel
4 | import com.esotericsoftware.kryo.io.Input
5 | import java.io.InputStream
6 |
7 | /**
8 | * Input class that uses a parcel to serialize. Perhaps not sustainable and ByteArrayStreams are better.
9 | */
10 | class ParcelInput : Input {
11 |
12 | private val parcel: Parcel
13 |
14 | @JvmOverloads
15 | constructor(parcel: Parcel, bufferSize: Int = DEFAULT_BUFFER) : super(bufferSize) { this.parcel = parcel }
16 | constructor(parcel: Parcel, buffer: ByteArray?) : super(buffer) { this.parcel = parcel }
17 | constructor(parcel: Parcel, buffer: ByteArray?, offset: Int, count: Int) : super(buffer, offset, count) { this.parcel = parcel }
18 |
19 |
20 | override fun setInputStream(inputStream: InputStream?) {
21 | throw UnsupportedOperationException("ParcelInput reads from parcels, not streams")
22 | }
23 |
24 | override fun fill(buffer: ByteArray, offset: Int, count: Int): Int {
25 | val realCount = minOf(count, parcel.dataAvail())
26 | for (i in offset until realCount) {
27 | buffer[i] = parcel.readByte()
28 | }
29 | return realCount
30 | }
31 |
32 | override fun available(): Int {
33 | return limit - position + parcel.dataAvail()
34 | }
35 |
36 | override fun close() {
37 | // Don't do anything for now. Don't do our own recycling here.
38 | }
39 |
40 | companion object {
41 | const val DEFAULT_BUFFER = 1024
42 | }
43 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/util/GrantResult.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.util
2 |
3 | import android.content.pm.PackageManager
4 | import java.util.*
5 |
6 | /**
7 | * Class representing the result of a permission request
8 | * @property permissions The permissions requested
9 | * @property grantResults The result of the request
10 | */
11 | data class GrantResult(val permissions: Array, val grantResults: IntArray) {
12 | /**
13 | * Convenience method to check whether a permission was granted.
14 | */
15 | fun wasGranted(permission:String): Boolean {
16 | val index = permissions.indexOf(permission).also { if (it<0) return false }
17 | return grantResults[index] == PackageManager.PERMISSION_GRANTED
18 | }
19 |
20 | /**
21 | * Convenience property to determine whether all permissions requested were granted.
22 | */
23 | val allGranted: Boolean = grantResults.all { it== PackageManager.PERMISSION_GRANTED }
24 |
25 | override fun equals(other: Any?): Boolean {
26 | if (this === other) return true
27 | if (javaClass != other?.javaClass) return false
28 |
29 | other as GrantResult
30 |
31 | if (!Arrays.equals(permissions, other.permissions)) return false
32 | if (!Arrays.equals(grantResults, other.grantResults)) return false
33 |
34 | return true
35 | }
36 |
37 | override fun hashCode(): Int {
38 | var result = Arrays.hashCode(permissions)
39 | result = 31 * result + Arrays.hashCode(grantResults)
40 | return result
41 | }
42 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | pluginManagement {
3 | val androidBuildToolsVersion: String by settings
4 | val kotlinVersion: String by settings
5 | val dokkaVersion: String by settings
6 | val bintrayVersion: String by settings
7 |
8 | repositories {
9 | gradlePluginPortal()
10 | google()
11 | maven(url = "https://dl.bintray.com/kotlin/kotlin-eap")
12 | // maven("https://dl.bintray.com/kotlin/kotlin-dev")
13 | }
14 |
15 | resolutionStrategy {
16 | eachPlugin {
17 | when (requested.id.id) {
18 | "com.android.library",
19 | "com.android.application" -> {
20 | val ver = requested.version ?: androidBuildToolsVersion
21 | useModule("com.android.tools.build:gradle:${ver}");
22 | }
23 | "org.jetbrains.kotlin.android",
24 | "kotlin-android-extensions" -> {
25 | val ver = requested.version ?: kotlinVersion
26 | useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${ver}");
27 | }
28 | "org.jetbrains.dokka" -> {
29 | val ver = requested.version ?: dokkaVersion
30 | useVersion(ver)
31 | // useModule("${requested.module}:${ver}")
32 | }
33 | "com.jfrog.bintray" -> {
34 | val ver = requested.version ?: bintrayVersion
35 | useVersion(ver)
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | include(":testapp", ":core", ":appcompat")
43 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/ObjectSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import android.accounts.AccountManager
4 | import android.content.Context
5 | import com.esotericsoftware.kryo.Kryo
6 | import com.esotericsoftware.kryo.Serializer
7 | import com.esotericsoftware.kryo.io.Input
8 | import com.esotericsoftware.kryo.io.Output
9 |
10 | /**
11 | * Serializer for Kotlin objects that stores nothing and just retrieves the current instance from
12 | * the field.
13 | */
14 | internal class ObjectSerializer(kryo: Kryo, val type: Class<*>): Serializer(false, true) {
15 | /**
16 | * The correct way of getting an object is getting it's instance.
17 | */
18 | override fun read(kryo: Kryo, input: Input, type: Class): Any {
19 | return type.fields.first { it.name=="INSTANCE" }.get(null)
20 | }
21 |
22 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
23 | // The class is already written by the caller so no need to write anything
24 | }
25 | }
26 |
27 | /**
28 | * Serializer for Kotlin objects that stores nothing and just retrieves the current instance from
29 | * the field.
30 | */
31 | internal class AccountManagerSerializer(kryo: Kryo, val type: Class<*>, context: Context?): Serializer(false, true) {
32 | private val context = context?.applicationContext
33 | /**
34 | * The correct way of getting an object is getting it's instance.
35 | */
36 | override fun read(kryo: Kryo, input: Input, type: Class): Any {
37 | return AccountManager.get(context)
38 | }
39 |
40 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
41 | // The class is already written by the caller so no need to write anything
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/ReferenceSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.Serializer
5 | import com.esotericsoftware.kryo.io.Input
6 | import com.esotericsoftware.kryo.io.Output
7 | import nl.adaptivity.android.kryo.serializers.ReferenceType.*
8 | import java.lang.ref.Reference
9 | import java.lang.ref.SoftReference
10 | import java.lang.ref.WeakReference
11 |
12 | /**
13 | * Serializer for Kotlin objects that stores nothing and just retrieves the current instance from
14 | * the field.
15 | */
16 | internal class ReferenceSerializer(kryo: Kryo, val type: Class>): Serializer>(false, true) {
17 | /**
18 | * The correct way of getting an object is getting it's instance.
19 | */
20 | override fun read(kryo: Kryo, input: Input, type: Class>): Reference<*> {
21 | val result = when (kryo.readObject(input, ReferenceType::class.java)) {
22 | SOFTREF -> SoftReference(null)
23 | WEAKREF -> WeakReference(null)
24 | else -> throw IllegalArgumentException("Unsupported reference")
25 | }
26 | return result
27 | }
28 |
29 | override fun write(kryo: Kryo, output: Output, obj: Reference<*>?) {
30 | val substitute = when (obj) {
31 | is SoftReference<*> -> SOFTREF
32 | is WeakReference<*> -> WEAKREF
33 | else -> IllegalArgumentException("Serializing contexts only works for Soft and Weak references")
34 | }
35 | kryo.writeObject(output, substitute)
36 | // The class is already written by the caller so no need to write anything
37 | }
38 | }
39 |
40 | private enum class ReferenceType {
41 | SOFTREF,
42 | WEAKREF
43 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/InitialResultSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.Serializer
5 | import com.esotericsoftware.kryo.io.Input
6 | import com.esotericsoftware.kryo.io.Output
7 | import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
8 |
9 | internal class InitialResultSerializer(val parent: Serializer): Serializer() {
10 | override fun read(kryo: Kryo, input: Input, type: Class): Any? {
11 | val readValue = kryo.readClassAndObject(input)
12 | return when (readValue) {
13 | KryoAndroidConstants.COROUTINE_SUSPENDED -> COROUTINE_SUSPENDED
14 | else -> readValue
15 | }
16 | }
17 |
18 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
19 | when (obj) {
20 | // COROUTINE_SUSPENDED -> kryo.writeClassAndObject(output, KryoAndroidConstants.COROUTINE_SUSPENDED)
21 | // _Resumed -> kryo.writeClassAndObject(output, KryoAndroidConstants.RESUMED)
22 | // _Undecided -> kryo.writeClassAndObject(output, KryoAndroidConstants.UNDECIDED)
23 | else -> parent.write(kryo, output, obj)
24 | }
25 | }
26 |
27 | override fun isImmutable(): Boolean = parent.isImmutable()
28 |
29 | override fun setImmutable(immutable: Boolean) = parent.setImmutable(immutable)
30 |
31 | override fun setAcceptsNull(acceptsNull: Boolean) = parent.setAcceptsNull(acceptsNull)
32 |
33 | override fun copy(kryo: Kryo?, original: Any?): Any? = parent.copy(kryo, original)
34 |
35 | override fun getAcceptsNull(): Boolean = parent.getAcceptsNull()
36 |
37 | override fun setGenerics(kryo: Kryo?, generics: Array>?) =
38 | parent.setGenerics(kryo, generics)
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity4.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import kotlinx.android.synthetic.main.activity_test1.*
6 | import kotlinx.coroutines.CoroutineStart
7 | import kotlinx.coroutines.Dispatchers
8 | import nl.adaptivity.android.coroutines.CoroutineActivity
9 | import nl.adaptivity.android.coroutines.startActivityForResult
10 |
11 | /**
12 | * Implementation of an activity that uses an async launch to get a result from an activity using
13 | * coroutines. It uses the "safe" launch function and a synthetic accessor for the contained views.
14 | */
15 | class TestActivity4 : CoroutineActivity() {
16 |
17 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
18 | super.onRestoreInstanceState(savedInstanceState)
19 | restoredView.text = getString(R.string.lbl_restored)
20 | }
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_test1)
25 | button.setOnClickListener { onButtonClick() }
26 | }
27 |
28 | fun onButtonClick() {
29 | Log.w(TAG, "Activity is: $this")
30 | launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Main) {
31 | val activityResult = startActivityForResult()
32 |
33 | Log.w(TAG, "Deserialised Activity is: $activity")
34 | val newText = activityResult.flatMap { it?.getCharSequenceExtra(TestActivity2.KEY_DATA) } ?: getString(R.string.lbl_cancelled)
35 | Log.w(TAG, "newText: $newText")
36 |
37 | Log.w(TAG, "textview value: $textView")
38 | textView.text = newText
39 | }
40 | }
41 |
42 | companion object {
43 | const val TAG="TestActivity4"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity5.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import kotlinx.android.synthetic.main.activity_test1.*
6 | import kotlinx.coroutines.CoroutineStart
7 | import kotlinx.coroutines.Dispatchers
8 | import nl.adaptivity.android.coroutines.startActivityForResult
9 | import nl.adaptivity.android.coroutinesCompat.CompatCoroutineActivity
10 |
11 | /**
12 | * Implementation of an activity that uses an async launch to get a result from an activity using
13 | * coroutines. It uses the "safe" launch function and a synthetic accessor for the contained views.
14 | */
15 | class TestActivity5 : CompatCoroutineActivity() {
16 |
17 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
18 | super.onRestoreInstanceState(savedInstanceState)
19 | restoredView.text = getString(R.string.lbl_restored)
20 | }
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_test1)
25 | button.setOnClickListener { onButtonClick() }
26 | }
27 |
28 | fun onButtonClick() {
29 | Log.w(TAG, "Activity is: $this")
30 | launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Main) {
31 | val activityResult = startActivityForResult()
32 |
33 | Log.w(TAG, "Deserialised Activity is: $activity")
34 | val newText = activityResult.flatMap { it?.getCharSequenceExtra(TestActivity2.KEY_DATA) } ?: getString(R.string.lbl_cancelled)
35 | Log.w(TAG, "newText: $newText")
36 |
37 | Log.w(TAG, "textview value: $textView")
38 | textView.text = newText
39 | }
40 | }
41 |
42 | companion object {
43 | const val TAG="TestActivity5"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/testapp/src/main/res/layout/activity_test2.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
24 |
25 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/ActivityCoroutineScopeWrapper.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.app.FragmentManager
5 | import android.support.annotation.IdRes
6 | import android.view.View
7 | import kotlinx.android.extensions.LayoutContainer
8 | import kotlinx.coroutines.CoroutineScope
9 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
10 | import nl.adaptivity.android.coroutines.impl.DelegateLayoutContainer
11 |
12 | class ActivityCoroutineScopeWrapper(
13 | parentScope: CoroutineScope
14 | ) :
15 | WrappedContextCoroutineScope>(parentScope),
16 | LayoutContainer {
17 |
18 | @Suppress("UNCHECKED_CAST")
19 | val activity: A
20 | get() = coroutineContext[AndroidContext]!!.androidContext as A
21 |
22 | @Suppress("DEPRECATION")
23 | @Deprecated("Use function", ReplaceWith("fragmentManager()"))
24 | val fragmentManager: FragmentManager
25 | get() = activity.fragmentManager
26 |
27 | @Suppress("DEPRECATION")
28 | fun fragmentManager(): FragmentManager = fragmentManager()
29 |
30 | override fun getAndroidContext() = activity
31 |
32 | override val containerView: View? get() = activity.findViewById(android.R.id.content)
33 |
34 | fun findViewById(@IdRes id: Int): T = activity.findViewById(id)
35 |
36 | override fun createScopeWrapper(parentScope: CoroutineScope): ActivityCoroutineScopeWrapper {
37 | return ActivityCoroutineScopeWrapper(parentScope)
38 | }
39 |
40 | @Suppress("unused")
41 | suspend fun layoutContainer(body: LayoutContainer.() -> R): R {
42 | return DelegateLayoutContainer(activity.window.decorView)
43 | .body()
44 | }
45 |
46 | @Suppress("unused")
47 | suspend inline fun withActivity(body: A.() -> R): R {
48 | return activity.body()
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/testapp/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity3.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.widget.TextView
7 | import kotlinx.android.synthetic.main.activity_test1.*
8 | import kotlinx.coroutines.CoroutineStart
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import nl.adaptivity.android.coroutines.CoroutineActivity
12 | import nl.adaptivity.android.coroutines.activityResult
13 |
14 | /**
15 | * Implementation of an activity that uses an async launch to get a result from an activity using
16 | * coroutines. It uses the standard launch function.
17 | */
18 | //@SuppressLint("RestrictedApi")
19 | class TestActivity3 : CoroutineActivity() {
20 |
21 | override fun onRestoreInstanceState(savedInstanceState: Bundle) {
22 | super.onRestoreInstanceState(savedInstanceState)
23 | restoredView.text = getString(R.string.lbl_restored)
24 | }
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | setContentView(R.layout.activity_test1)
29 | button.setOnClickListener { onButtonClick() }
30 | }
31 |
32 | fun onButtonClick() {
33 | Log.w(TAG, "Activity is: $this")
34 | launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Main) {
35 | val activityResult = activityResult(Intent(this@TestActivity3, TestActivity2::class.java))
36 | Log.w(TAG, "Deserialised Activity is: ${this@TestActivity3}")
37 | val newText = activityResult.flatMap { it?.getCharSequenceExtra(TestActivity2.KEY_DATA) } ?: getString(R.string.lbl_cancelled)
38 | Log.w(TAG, "newText: $newText")
39 | val textView = findViewById(R.id.textView)
40 | Log.w(TAG, "textview value: $textView")
41 | textView.text = newText
42 | }
43 | }
44 |
45 | companion object {
46 | const val TAG="TestActivity3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/launchers.kt:
--------------------------------------------------------------------------------
1 | @file:UseExperimental(ExperimentalTypeInference::class)
2 |
3 | package nl.adaptivity.android.coroutines
4 |
5 | import android.app.Activity
6 | import android.app.Fragment
7 | import android.content.Intent
8 | import kotlin.experimental.ExperimentalTypeInference
9 |
10 | fun Activity.ensureRetainingFragment(): RetainedContinuationFragment {
11 | val fm = fragmentManager
12 | val existingFragment =
13 | fm.findFragmentByTag(RetainedContinuationFragment.TAG) as RetainedContinuationFragment?
14 |
15 | if (existingFragment != null) return existingFragment
16 |
17 | val contFragment = RetainedContinuationFragment()
18 | fm.beginTransaction().apply {
19 | // This shouldn't happen, but in that case remove the old continuation.
20 | existingFragment?.let { remove(it) }
21 |
22 | add(contFragment, RetainedContinuationFragment.TAG)
23 | }.commit()
24 | runOnUiThread { fm.executePendingTransactions() }
25 |
26 | return contFragment
27 | }
28 |
29 | @Suppress("unused")
30 | suspend inline fun FragmentCoroutineScopeWrapper<*>.startActivityForResult() =
31 | startActivityForResult(Intent(fragment.activity, A::class.java))
32 |
33 | suspend inline fun WrappedContextCoroutineScope.startActivityForResult(): ActivityResult =
34 | startActivityForResult(Intent(getAndroidContext(), A::class.java))
35 |
36 | inline fun Activity.startActivityForResult(requestCode: Int) =
37 | this.startActivityForResult(Intent(this, A::class.java), requestCode)
38 |
39 | @Suppress("unused", "DEPRECATION")
40 | inline fun Fragment.startActivityForResult(requestCode: Int) =
41 | this.startActivityForResult(Intent(activity, A::class.java), requestCode)
42 |
43 | @Suppress("unused")
44 | inline fun Activity.startActivity() = startActivity(Intent(this, A::class.java))
45 |
46 | @Suppress("unused", "DEPRECATION")
47 | inline fun Fragment.startActivity() = startActivity(Intent(activity, A::class.java))
48 |
49 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/KryoIO.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused", "KotlinDeprecation")
2 |
3 | package nl.adaptivity.android.kryo
4 |
5 | import android.content.Context
6 | import com.esotericsoftware.kryo.Kryo
7 | import com.esotericsoftware.kryo.util.MapReferenceResolver
8 | import nl.adaptivity.android.kryo.serializers.ObjectSerializer
9 | import nl.adaptivity.android.kryo.serializers.SafeContinuationSerializer
10 | import nl.adaptivity.android.kryo.serializers._SafeContinuation
11 | import org.objenesis.strategy.StdInstantiatorStrategy
12 |
13 |
14 | /**
15 | * Get a Kryo serializer for a context-less application. For serialization this should not make
16 | * a difference, but for deserialization any contexts present in the state will lead to failure.
17 | */
18 | val kryoAndroid get(): Kryo = Kryo(AndroidKotlinResolver(null), MapReferenceResolver()).apply {
19 | registerAndroidSerializers()
20 |
21 | fieldSerializerConfig.isIgnoreSyntheticFields = false
22 | }
23 |
24 | /**
25 | * Get a Kryo serializer that handles Android contexts special. It allows dynamic replacement of
26 | * markers indicating a context with the passed in context (or application context if that applies).
27 | */
28 | fun kryoAndroid(context: Context?): Kryo = Kryo(AndroidKotlinResolver(context), MapReferenceResolver()).apply {
29 | registerAndroidSerializers()
30 |
31 | fieldSerializerConfig.isIgnoreSyntheticFields = false
32 | }
33 |
34 | /**
35 | * Extension function
36 | */
37 | fun Kryo.registerAndroidSerializers() {
38 | instantiatorStrategy = KotlinObjectInstantiatorStrategy(Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()))
39 |
40 | //TODO no longer needed
41 | register(_SafeContinuation, SafeContinuationSerializer(this))
42 | /* TODO While this doesn't affect instantiation (The KotlinObjectStantiatorStrategy handles that)
43 | * this may be needed to not serialize/deserialize the actual pool state.
44 | */
45 | val commonPoolClass = Class.forName("kotlinx.coroutines.CommonPool")
46 | register(commonPoolClass, ObjectSerializer(this,commonPoolClass))
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/ContextSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.esotericsoftware.kryo.Kryo
6 | import com.esotericsoftware.kryo.Serializer
7 | import com.esotericsoftware.kryo.io.Input
8 | import com.esotericsoftware.kryo.io.Output
9 | import nl.adaptivity.android.kryo.serializers.KryoAndroidConstants.*
10 |
11 | internal class ContextSerializer(private val context: Context?) : Serializer() {
12 |
13 | override fun read(kryo: Kryo, input: Input, type: Class): Context? {
14 | val result: Context? = when (kryo.readObject(input, KryoAndroidConstants::class.java)) {
15 | CONTEXT -> {
16 | val savedContextType = kryo.readClass(input).type
17 | if (! type.isAssignableFrom(savedContextType)) {
18 | throw ClassCastException("Saved a context of type ${savedContextType}, but asked to inflate as ${type}")
19 | }
20 | type.cast(context)
21 | }
22 | APPLICATIONCONTEXT -> type.cast(context?.applicationContext)
23 | OTHERCONTEXT -> kryo.readClassAndObject(input) as Context?
24 | else -> null
25 | }
26 | return result?.also { kryo.reference(it) }
27 | }
28 |
29 | override fun write(kryo: Kryo, output: Output, obj: Context) {
30 | when (obj) {
31 | context -> {
32 | kryo.writeObject(output, CONTEXT)
33 | kryo.writeClass(output, obj.javaClass)
34 | }
35 | is Application -> kryo.writeObject(output, APPLICATIONCONTEXT)
36 | else -> {
37 | throw IllegalArgumentException("Attempting to serialize context of type ${obj.javaClass}")
38 | kryo.writeObject(output, OTHERCONTEXT)
39 | kryo.writeClassAndObject(output, obj)
40 | }
41 | // else -> throw IllegalArgumentException("Serializing contexts only works for activity, application and service")
42 | }
43 |
44 | }
45 | }
--------------------------------------------------------------------------------
/testapp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.internal.dsl.BuildType
2 | import libraries.*
3 | import versions.coroutinesVersion
4 |
5 | plugins {
6 | id("com.android.application")
7 | id("org.jetbrains.kotlin.android")
8 | id("kotlin-android-extensions")
9 | }
10 |
11 | val reqCompileSdkVersion:String by project
12 | val reqTargetSdkVersion:String by project
13 | val reqMinSdkVersion:String by project
14 |
15 | android {
16 | compileSdkVersion(reqCompileSdkVersion.toInt())
17 |
18 | defaultConfig {
19 | applicationId= "uk.ac.bmth.aprog.testapp"
20 | minSdkVersion(reqMinSdkVersion.toInt())
21 | targetSdkVersion(reqTargetSdkVersion.toInt())
22 | versionCode=1
23 | versionName="1.0"
24 |
25 | testInstrumentationRunner="androidx.test.runner.AndroidJUnitRunner"
26 |
27 | }
28 |
29 | buildTypes {
30 | getByName("release") {
31 | isMinifyEnabled = false
32 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
33 | }
34 | }
35 |
36 | compileOptions {
37 | sourceCompatibility = JavaVersion.VERSION_1_8
38 | targetCompatibility = JavaVersion.VERSION_1_8
39 | }
40 |
41 | packagingOptions {
42 | pickFirst("META-INF/atomicfu.kotlin_module")
43 | pickFirst("META-INF/AL2.0")
44 | pickFirst("META-INF/LGPL2.1")
45 | }
46 | }
47 |
48 | dependencies {
49 | implementation(project(":appcompat"))
50 |
51 | implementation(supportLibSpec)
52 | implementation(androidExtensionRuntimeSpec)
53 |
54 | implementation(constraintLayoutSpec)
55 | implementation(kotlinlibSpec)
56 | implementation(kryoSpec)
57 |
58 | testImplementation(junitSpec)
59 | androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
60 | // testImplementation (kryoSpec)
61 | // androidTestImplementation (kryoSpec)
62 | androidTestRuntimeOnly(androidExtensionRuntimeSpec)
63 | useEspresso(project)
64 | }
65 |
66 | androidExtensions {
67 | isExperimental = true
68 | }
69 |
70 |
71 | projectRepositories()
72 |
73 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/AndroidContextCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Fragment
4 | import android.content.Context
5 | import kotlinx.coroutines.*
6 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
7 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
8 | import java.lang.IllegalStateException
9 | import kotlin.coroutines.CoroutineContext
10 | import kotlin.coroutines.EmptyCoroutineContext
11 | import kotlinx.coroutines.launch as originalLaunch
12 | import kotlinx.coroutines.async as originalAsync
13 |
14 | /**
15 | * Interface for all sources of coroutine scope that can provide an android context
16 | */
17 | interface AndroidContextCoroutineScope> :
18 | CoroutineScope {
19 | fun getAndroidContext(): C
20 |
21 | fun createScopeWrapper(parentScope: CoroutineScope): S
22 |
23 | fun launch(
24 | context: CoroutineContext = EmptyCoroutineContext,
25 | start: CoroutineStart = CoroutineStart.DEFAULT,
26 | block: suspend S.() -> Unit
27 | ): Job {
28 | val extContext = context.ensureAndroidContext()
29 | return originalLaunch(extContext, start) { createScopeWrapper(this).block() }
30 | }
31 |
32 |
33 | fun async(
34 | context: CoroutineContext = EmptyCoroutineContext,
35 | start: CoroutineStart = CoroutineStart.DEFAULT,
36 | block: suspend S.() -> RES
37 | ): Deferred {
38 | val extContext = context.ensureAndroidContext()
39 | return originalAsync(
40 | extContext,
41 | start
42 | ) { createScopeWrapper(this).block() }
43 | }
44 |
45 | fun CoroutineContext.ensureAndroidContext(): CoroutineContext {
46 | val parentFragmentContext = coroutineContext[AndroidContext]
47 | return when {
48 | this[AndroidContext]!=null -> this
49 | parentFragmentContext !=null -> this + parentFragmentContext
50 | this is Context -> this + AndroidContext(this)
51 | else -> throw IllegalStateException("No context present for context scope")
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/FragmentCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.app.Fragment
5 | import android.view.View
6 | import kotlinx.android.extensions.LayoutContainer
7 | import kotlinx.coroutines.*
8 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
9 | import java.lang.IllegalStateException
10 | import kotlin.coroutines.CoroutineContext
11 | import kotlin.coroutines.EmptyCoroutineContext
12 | import kotlinx.coroutines.launch as originalLaunch
13 | import kotlinx.coroutines.async as originalAsync
14 |
15 | /**
16 | * Interface for all sources of coroutine scope for an android fragment
17 | */
18 | interface FragmentCoroutineScope :
19 | CoroutineScope {
20 |
21 | val fragment: F
22 |
23 | fun getAndroidContext(): Activity? = fragment.activity
24 |
25 | fun createScopeWrapper(parentScope: CoroutineScope): FragmentCoroutineScopeWrapper =
26 | FragmentCoroutineScopeWrapper(parentScope)
27 |
28 | fun launch(
29 | context: CoroutineContext = EmptyCoroutineContext,
30 | start: CoroutineStart = CoroutineStart.DEFAULT,
31 | block: suspend FragmentCoroutineScopeWrapper.() -> Unit
32 | ): Job {
33 | val extContext = context.ensureFragmentContext()
34 | return originalLaunch(extContext, start) { createScopeWrapper(this).block() }
35 | }
36 |
37 |
38 | fun async(
39 | context: CoroutineContext = EmptyCoroutineContext,
40 | start: CoroutineStart = CoroutineStart.DEFAULT,
41 | block: suspend FragmentCoroutineScopeWrapper.() -> RES
42 | ): Deferred {
43 | val extContext = context.ensureFragmentContext()
44 | return originalAsync(extContext, start) { createScopeWrapper(this).block() }
45 | }
46 |
47 | fun CoroutineContext.ensureFragmentContext(): CoroutineContext {
48 | val parentFragmentContext = coroutineContext[FragmentContext]
49 | return when {
50 | this[FragmentContext]!=null -> this
51 | parentFragmentContext !=null -> this + parentFragmentContext
52 | this is Fragment -> this + FragmentContext(this)
53 | else -> throw IllegalStateException("No fragment present for fragment scope")
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/AppcompatFragmentCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.app.Activity
4 | import android.support.v4.app.Fragment
5 | import android.view.View
6 | import kotlinx.android.extensions.LayoutContainer
7 | import kotlinx.coroutines.*
8 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
9 | import kotlin.coroutines.CoroutineContext
10 | import kotlin.coroutines.EmptyCoroutineContext
11 | import kotlinx.coroutines.launch as originalLaunch
12 | import kotlinx.coroutines.async as originalAsync
13 |
14 | /**
15 | * Interface for all sources of coroutine scope for an android fragment
16 | */
17 | interface AppcompatFragmentCoroutineScope :
18 | CoroutineScope {
19 |
20 | val fragment: F
21 |
22 | fun getAndroidContext(): Activity? = fragment.activity
23 |
24 | fun createScopeWrapper(parentScope: CoroutineScope): AppcompatFragmentCoroutineScopeWrapper =
25 | AppcompatFragmentCoroutineScopeWrapper(parentScope)
26 |
27 | fun launch(
28 | context: CoroutineContext = EmptyCoroutineContext,
29 | start: CoroutineStart = CoroutineStart.DEFAULT,
30 | block: suspend AppcompatFragmentCoroutineScopeWrapper.() -> Unit
31 | ): Job {
32 | val extContext = context.ensureFragmentContext()
33 | return originalLaunch(extContext, start) { createScopeWrapper(this).block() }
34 | }
35 |
36 |
37 | fun async(
38 | context: CoroutineContext = EmptyCoroutineContext,
39 | start: CoroutineStart = CoroutineStart.DEFAULT,
40 | block: suspend AppcompatFragmentCoroutineScopeWrapper.() -> R
41 | ): Deferred {
42 | val extContext = context.ensureFragmentContext()
43 | return originalAsync(extContext, start) { createScopeWrapper(this).block() }
44 | }
45 |
46 |
47 |
48 | fun CoroutineContext.ensureFragmentContext(): CoroutineContext {
49 | val parentFragmentContext = coroutineContext[AppcompatFragmentContext]
50 | return when {
51 | this[AppcompatFragmentContext]!=null -> this
52 | parentFragmentContext !=null -> this + parentFragmentContext
53 | this is Fragment -> this + AppcompatFragmentContext(this)
54 | else -> throw IllegalStateException("No fragment present for fragment scope")
55 | }
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/FragmentSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import android.app.Activity
4 | import android.app.Fragment
5 | import android.util.Log
6 | import com.esotericsoftware.kryo.Kryo
7 | import com.esotericsoftware.kryo.Serializer
8 | import com.esotericsoftware.kryo.io.Input
9 | import com.esotericsoftware.kryo.io.Output
10 |
11 | internal class FragmentSerializer(private val context: Activity?) : Serializer() {
12 |
13 | override fun read(kryo: Kryo, input: Input, type: Class): Fragment? {
14 | val marker = kryo.readObject(input, KryoAndroidConstants::class.java)
15 | val savedFragmentType:Class<*> = kryo.readClass(input).type
16 | val result: Fragment? = when (marker) {
17 | KryoAndroidConstants.FRAGMENTBYTAG -> {
18 | context?.fragmentManager?.findFragmentByTag(input.readString())
19 | }
20 |
21 | KryoAndroidConstants.FRAGMENTBYID ->
22 | context?.fragmentManager?.findFragmentById(input.readInt())
23 |
24 | KryoAndroidConstants.FRAGMENTWITHOUTHANDLE -> {
25 | // context?.fragmentManager?.fragments?.firstOrNull { savedFragmentType == it.javaClass }
26 | null
27 | }
28 |
29 | else -> return null
30 | }
31 |
32 | if (!type.isAssignableFrom(savedFragmentType)) {
33 | throw ClassCastException("Saved a fragment of type ${savedFragmentType}, but asked to inflate as ${type}")
34 | }
35 | Log.e("FragmentSerializer", "Deserialized fragment $result of type ${result?.javaClass}")
36 | type.cast(result)
37 |
38 | return result?.also { kryo.reference(it) }
39 | }
40 |
41 | override fun write(kryo: Kryo, output: Output, obj: Fragment) {
42 | if (obj.id != 0) {
43 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTBYID)
44 | kryo.writeClass(output, obj.javaClass)
45 | output.writeInt(obj.id)
46 | } else if (obj.tag != null) {
47 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTBYTAG)
48 | kryo.writeClass(output, obj.javaClass)
49 | output.writeString(obj.tag)
50 | } else {
51 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTWITHOUTHANDLE)
52 | kryo.writeClass(output, obj.javaClass)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/SupportFragmentSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import android.support.v4.app.Fragment
4 | import android.support.v4.app.FragmentActivity
5 | import android.util.Log
6 | import com.esotericsoftware.kryo.Kryo
7 | import com.esotericsoftware.kryo.Serializer
8 | import com.esotericsoftware.kryo.io.Input
9 | import com.esotericsoftware.kryo.io.Output
10 |
11 | internal class SupportFragmentSerializer(private val context: FragmentActivity?) : Serializer() {
12 |
13 | override fun read(kryo: Kryo, input: Input, type: Class): Fragment? {
14 | val marker = kryo.readObject(input, KryoAndroidConstants::class.java)
15 | val savedFragmentType: Class<*> = kryo.readClass(input).type
16 | val result: Fragment? = when (marker) {
17 | KryoAndroidConstants.FRAGMENTBYTAG -> {
18 | context?.supportFragmentManager?.findFragmentByTag(input.readString())
19 | }
20 |
21 | KryoAndroidConstants.FRAGMENTBYID ->
22 | context?.supportFragmentManager?.findFragmentById(input.readInt())
23 |
24 | KryoAndroidConstants.FRAGMENTWITHOUTHANDLE -> {
25 | // context?.fragmentManager?.fragments?.firstOrNull { savedFragmentType == it.javaClass }
26 | null
27 | }
28 |
29 | else -> return null
30 | }
31 |
32 | if (!type.isAssignableFrom(savedFragmentType)) {
33 | throw ClassCastException("Saved a fragment of type ${savedFragmentType}, but asked to inflate as ${type}")
34 | }
35 | Log.e("FragmentSerializer", "Deserialized fragment $result of type ${result?.javaClass}")
36 | type.cast(result)
37 |
38 | return result?.also { kryo.reference(it) }
39 | }
40 |
41 | override fun write(kryo: Kryo, output: Output, obj: Fragment) {
42 | if (obj.id != 0) {
43 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTBYID)
44 | kryo.writeClass(output, obj.javaClass)
45 | output.writeInt(obj.id)
46 | } else if (obj.tag != null) {
47 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTBYTAG)
48 | kryo.writeClass(output, obj.javaClass)
49 | output.writeString(obj.tag)
50 | } else {
51 | kryo.writeObject(output, KryoAndroidConstants.FRAGMENTWITHOUTHANDLE)
52 | kryo.writeClass(output, obj.javaClass)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity6.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import android.util.Log
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import kotlinx.android.synthetic.main.fragment_test6.*
10 | import kotlinx.android.synthetic.main.fragment_test6.view.*
11 | import kotlinx.coroutines.CoroutineStart
12 | import kotlinx.coroutines.Dispatchers
13 | import nl.adaptivity.android.coroutinesCompat.AppcompatCoroutineFragment
14 | import nl.adaptivity.android.coroutinesCompat.startActivityForResult
15 |
16 | class TestFragment6: AppcompatCoroutineFragment() {
17 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
18 | return inflater.inflate(R.layout.fragment_test6, container, false).also { root ->
19 | root.button.setOnClickListener { onButtonClick() }
20 | }
21 | }
22 |
23 |
24 | private fun onButtonClick() {
25 | Log.w(TestActivity6.TAG, "Activity is: $this")
26 | launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Main) {
27 | val activityResult = startActivityForResult()
28 |
29 | Log.w(TestActivity6.TAG, "Deserialised Activity is: $activity")
30 | val newText = activityResult.flatMap { it?.getCharSequenceExtra(TestActivity2.KEY_DATA) } ?: getString(R.string.lbl_cancelled)
31 | Log.w(TestActivity6.TAG, "newText: $newText")
32 |
33 | Log.w(TestActivity6.TAG, "textview value: $textView")
34 | textView.text = newText
35 | }
36 | }
37 |
38 | override fun onViewStateRestored(savedInstanceState: Bundle?) {
39 | super.onViewStateRestored(savedInstanceState)
40 | if (savedInstanceState!=null) restoredView.text = getString(R.string.lbl_restored)
41 | }
42 |
43 | }
44 |
45 | /**
46 | * Implementation of an activity that uses an async launch to get a result from an activity using
47 | * coroutines. It uses the "safe" launch function and a synthetic accessor for the contained views.
48 | */
49 | class TestActivity6 : AppCompatActivity() {
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | setContentView(R.layout.activity_test6)
54 | }
55 |
56 | companion object {
57 | const val TAG="TestActivity6"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/serializers/SafeContinuationSerializer.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo.serializers
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.io.Input
5 | import com.esotericsoftware.kryo.io.Output
6 | import com.esotericsoftware.kryo.serializers.FieldSerializer
7 | import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
8 |
9 | internal class SafeContinuationSerializer(kryo: Kryo): FieldSerializer(kryo, _SafeContinuation) {
10 |
11 | /*
12 | override fun write(kryo: Kryo, output: Output, obj: Any?) {
13 | val resultField = getField("result").field.apply { isAccessible=true }
14 | val resultValue = resultField.get(obj)
15 | var changed = true
16 | // If the result field is one of the special objects, map them to the enum instances for
17 | // safe serialization
18 | when (resultValue) {
19 | COROUTINE_SUSPENDED -> resultField.set(obj, KryoAndroidConstants.COROUTINE_SUSPENDED)
20 | _Resumed -> resultField.set(obj, KryoAndroidConstants.RESUMED)
21 | _Undecided -> resultField.set(obj, KryoAndroidConstants.UNDECIDED)
22 | else -> changed = false
23 | }
24 | super.write(kryo, output, obj)
25 | // Undo the changes
26 | if (changed) {
27 | resultField.set(obj, resultValue)
28 | }
29 | }
30 | */
31 |
32 | /*
33 | @Suppress("UNCHECKED_CAST")
34 | override fun read(kryo: Kryo, input: Input, type: Class): Any? {
35 | val obj = super.read(kryo, input, type)
36 | val resultField = getField("result").field.apply { isAccessible=true }
37 | val resultValue = resultField.get(obj)
38 | when (resultValue) {
39 | KryoAndroidConstants.COROUTINE_SUSPENDED -> resultField.set(obj, COROUTINE_SUSPENDED)
40 | KryoAndroidConstants.RESUMED -> resultField.set(obj, _Resumed)
41 | KryoAndroidConstants.UNDECIDED -> resultField.set(obj, _Undecided)
42 | }
43 |
44 | return obj
45 | }
46 | */
47 | }
48 |
49 | @Suppress("ObjectPropertyName")
50 | internal val _SafeContinuation = Class.forName("kotlin.coroutines.SafeContinuation")
51 | /*
52 | @Suppress("ObjectPropertyName")
53 | internal val _Resumed = _SafeContinuation.getDeclaredField("RESUMED").let { f -> f.isAccessible=true; f.get(null) }
54 | @Suppress("ObjectPropertyName")
55 | internal val _Undecided = _SafeContinuation.getDeclaredField("UNDECIDED").let { f -> f.isAccessible=true; f.get(null) }
56 | */
57 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/BaseRetainedContinuationFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Fragment
4 | import android.os.Bundle
5 |
6 | /**
7 | * Base class for fragments that are used to store continuations.
8 | */
9 | open class BaseRetainedContinuationFragment : Fragment() {
10 | private val parcelableContinuations = arrayListOf>()
11 |
12 | @Deprecated("This is quite unsafe")
13 | protected val requestCode: Int get() = parcelableContinuations.firstOrNull()?.requestCode ?: -1
14 | val lastResultCode: Int get() {
15 | return parcelableContinuations.maxByOrNull { it.requestCode }?.requestCode ?: (COROUTINEFRAGMENT_RESULTCODE_START-1)
16 | }
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | retainInstance = true
21 |
22 | if (savedInstanceState!=null) {
23 | savedInstanceState.getParcelableArrayList>(KEY_ACTIVITY_CONTINUATIONS_STATE)?.let {
24 | parcelableContinuations.addAll(it)
25 | }
26 | }
27 | for (parcelableContinuation in parcelableContinuations) {
28 | parcelableContinuation.attachContext2(activity)
29 | }
30 | }
31 |
32 | override fun onSaveInstanceState(outState: Bundle) {
33 | super.onSaveInstanceState(outState)
34 |
35 | // Make sure to store the state now rather than later so that we actually know the fragment id and tags etc.
36 | parcelableContinuations.forEach{ it.detachContext() }
37 |
38 | outState.putParcelableArrayList(KEY_ACTIVITY_CONTINUATIONS_STATE, parcelableContinuations)
39 | }
40 |
41 | fun addContinuation(parcelableContinuation: ParcelableContinuation) {
42 | parcelableContinuations.add(parcelableContinuation)
43 | }
44 |
45 | protected fun dispatchResult(activityResult: T, requestCode: Int) {
46 | fragmentManager.executePendingTransactions()
47 | @Suppress("UNCHECKED_CAST")
48 | val continuation = parcelableContinuations.single { it.requestCode == requestCode } as ParcelableContinuation
49 | continuation.resume(activity, activityResult)
50 | parcelableContinuations.remove(continuation)
51 |
52 | if (parcelableContinuations.isEmpty()) {
53 | // Remove this fragment, it's no longer needed
54 | fragmentManager.beginTransaction().remove(this).commit()
55 | }
56 |
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/testapp/src/main/res/layout/activity_test1.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
24 |
25 |
37 |
38 |
49 |
50 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/test/TestActivity7.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.TextView
10 | import kotlinx.android.synthetic.main.fragment_test6.*
11 | import kotlinx.android.synthetic.main.fragment_test6.view.*
12 | import kotlinx.coroutines.CoroutineStart
13 | import kotlinx.coroutines.Dispatchers
14 | import nl.adaptivity.android.coroutines.CoroutineFragment
15 | import nl.adaptivity.android.coroutines.startActivityForResult
16 |
17 | class TestFragment7: CoroutineFragment() {
18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
19 | return inflater.inflate(R.layout.fragment_test6, container, false).also { root ->
20 | root.button.setOnClickListener { onButtonClick() }
21 | }
22 | }
23 |
24 |
25 | private fun onButtonClick() {
26 | Log.w(TestActivity7.TAG, "Activity is: $this")
27 | launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Main) {
28 | val activityResult = startActivityForResult()
29 |
30 | val textView = findViewById(R.id.textView)!!
31 |
32 | Log.w(TestActivity7.TAG, "Deserialised Activity is: $activity")
33 | val newText = activityResult.flatMap { it?.getCharSequenceExtra(TestActivity2.KEY_DATA) } ?: getString(R.string.lbl_cancelled)
34 | Log.w(TestActivity7.TAG, "newText: $newText")
35 |
36 | Log.w(TestActivity7.TAG, "textview value: $textView")
37 | textView.text = newText
38 | }
39 | }
40 |
41 | override fun onViewStateRestored(savedInstanceState: Bundle?) {
42 | super.onViewStateRestored(savedInstanceState)
43 | if (savedInstanceState!=null) restoredView.text = getString(R.string.lbl_restored)
44 | }
45 |
46 | }
47 |
48 | /**
49 | * Implementation of an activity that uses an async launch to get a result from an activity using
50 | * coroutines. It uses the "safe" launch function and a synthetic accessor for the contained views.
51 | */
52 | class TestActivity7 : Activity() {
53 |
54 | override fun onCreate(savedInstanceState: Bundle?) {
55 | super.onCreate(savedInstanceState)
56 | setContentView(R.layout.activity_test7)
57 | }
58 |
59 | companion object {
60 | const val TAG="TestActivity7"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/KryoParcelable.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import android.content.Context
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 | import com.esotericsoftware.kryo.Kryo
7 | import com.esotericsoftware.kryo.io.Input
8 | import com.esotericsoftware.kryo.io.Output
9 | import java.io.ByteArrayOutputStream
10 |
11 | /**
12 | * A [Parcelable] that can be stored using [Kryo]. For now it is hardcoded to use the Kryo
13 | * object created by [kryoAndroid].
14 | */
15 | class KryoParcelable(val data: T): Parcelable {
16 |
17 | override fun writeToParcel(dest: Parcel, flags: Int) {
18 | dest.writeKryoObject(data)
19 | }
20 |
21 | override fun describeContents() = 0
22 |
23 | companion object CREATOR : Parcelable.Creator> {
24 | override fun createFromParcel(parcel: Parcel): KryoParcelable {
25 | return KryoParcelable(parcel.readKryoObject(kryoAndroid))
26 | }
27 |
28 | override fun newArray(size: Int): Array?> {
29 | return arrayOfNulls(size)
30 | }
31 | }
32 |
33 | }
34 |
35 | inline fun Parcel.readKryoObject(kryo: Kryo) =
36 | readKryoObject(T::class.java, kryo)
37 |
38 | inline fun Parcel.readKryoObject(context: Context) =
39 | readKryoObject(T::class.java, kryoAndroid(context))
40 |
41 |
42 | fun Parcel.writeKryoObject(obj: Any?, kryo: Kryo = kryoAndroid) {
43 | if (obj==null) {
44 | writeInt(-1)
45 | } else {
46 | val baos = UnsafeByteArrayOutputStream()
47 | Output(baos).use { output ->
48 | kryo.writeClassAndObject(output, obj)
49 | }
50 | writeInt(baos.count())
51 | writeByteArray(baos.buf(), 0, baos.count())
52 | }
53 | }
54 |
55 | inline fun Parcel.readKryoObject(type:Class, context: Context) = readKryoObject(type, kryoAndroid(context))
56 |
57 | fun Parcel.readKryoObject(type:Class, kryo: Kryo): T {
58 | val size = readInt()
59 | @Suppress("UNCHECKED_CAST")
60 | if (size<=0) return null as T
61 | val input = ByteArray(size)
62 | val kryoValue = kryo.readClassAndObject(Input(input))
63 | return type.cast(kryoValue)
64 | }
65 |
66 | /**
67 | * Helper class that exposes the buf and count fields. Saves an array copy here when we can control
68 | * things and know that we will not clobber the buffer.
69 | */
70 | private class UnsafeByteArrayOutputStream : ByteArrayOutputStream() {
71 | fun buf(): ByteArray = buf
72 | fun count(): Int = count
73 | }
74 |
--------------------------------------------------------------------------------
/testapp/src/main/res/layout/fragment_test6.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
25 |
26 |
38 |
39 |
50 |
51 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/WrappedContextCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.support.annotation.RequiresApi
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.CoroutineStart
11 | import kotlinx.coroutines.Deferred
12 | import kotlinx.coroutines.async
13 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
14 | import kotlin.coroutines.CoroutineContext
15 | import kotlin.coroutines.suspendCoroutine
16 | import kotlinx.coroutines.launch as originalLaunch
17 | import kotlinx.coroutines.async as originalAsync
18 |
19 | abstract class WrappedContextCoroutineScope>(
20 | private val parentScope: CoroutineScope
21 | ) : AndroidContextCoroutineScope {
22 |
23 | override fun getAndroidContext(): C = coroutineContext[AndroidContext] as C
24 |
25 | override fun async(
26 | context: CoroutineContext,
27 | start: CoroutineStart,
28 | block: suspend S.() -> RES
29 | ): Deferred {
30 | return originalAsync(
31 | context + coroutineContext[AndroidContext]!!,
32 | start
33 | ) { createScopeWrapper(this).block() }
34 | }
35 |
36 | override val coroutineContext: CoroutineContext
37 | get() = parentScope.coroutineContext
38 |
39 | suspend fun startActivityForResult(intent: Intent): ActivityResult {
40 | return suspendCoroutine { continuation ->
41 | val activity =
42 | (continuation.context[AndroidContext]?.androidContext
43 | ?: throw IllegalStateException("Missing activity in context")) as Activity
44 |
45 | val contFragment: RetainedContinuationFragment =
46 | activity.ensureRetainingFragment()
47 | val resultCode: Int = contFragment.lastResultCode + 1
48 |
49 | contFragment.addContinuation(
50 | ParcelableContinuation(
51 | continuation,
52 | activity,
53 | resultCode
54 | )
55 | )
56 |
57 | activity.runOnUiThread {
58 | contFragment.startActivityForResult(intent, resultCode)
59 | }
60 | }
61 |
62 | }
63 |
64 |
65 | fun startActivity(intent: Intent) = getAndroidContext().startActivity(intent)
66 |
67 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
68 | fun startActivity(intent: Intent, options: Bundle) =
69 | getAndroidContext().startActivity(intent, options)
70 |
71 | }
--------------------------------------------------------------------------------
/appcompat/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import libraries.androidExtensionRuntimeSpec
2 | import libraries.supportLibSpec
3 | import versions.selfVersion
4 |
5 | plugins {
6 | id("com.android.library")
7 | kotlin("android")
8 | id("kotlin-android-extensions")
9 | id("maven-publish")
10 | id("org.jetbrains.dokka")
11 | }
12 |
13 | version = selfVersion
14 | group = "net.devrieze"
15 | description = "Extension for android coroutines that supports the appcompat library"
16 |
17 | projectRepositories()
18 |
19 | val reqCompileSdkVersion:String by project
20 | val reqTargetSdkVersion:String by project
21 | val reqMinSdkVersion:String by project
22 |
23 | android {
24 | compileSdkVersion(reqCompileSdkVersion.toInt())
25 |
26 | defaultConfig {
27 | minSdkVersion(reqMinSdkVersion.toInt())
28 | targetSdkVersion(reqTargetSdkVersion.toInt())
29 | versionName = selfVersion
30 | }
31 |
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 |
37 | packagingOptions {
38 | pickFirst("META-INF/atomicfu.kotlin_module")
39 | }
40 |
41 | }
42 |
43 | dependencies {
44 | implementation(supportLibSpec)
45 |
46 | implementation(kotlin("stdlib"))
47 | implementation(androidExtensionRuntimeSpec)
48 |
49 | api(project(":core"))
50 | }
51 |
52 | val sourcesJar = task("androidSourcesJar") {
53 | classifier = "sources"
54 | from(android.sourceSets["main"].java.srcDirs)
55 | }
56 |
57 | androidExtensions {
58 | isExperimental = true
59 | }
60 |
61 | //tasks.withType {
62 | // dokkaSourceSets.all {
63 | //
64 | // }
65 | //// linkMappings.add(LinkMapping().apply {
66 | //// dir="src/main/java"
67 | //// url = "https://github.com/pdvrieze/android-coroutines/tree/master/appcompat/src/main/java"
68 | //// suffix = "#L"
69 | //// })
70 | //// outputFormat = "html"
71 | //}
72 |
73 | afterEvaluate{
74 | publishing {
75 | (publications) {
76 | create("MyPublication") {
77 | artifact(tasks["bundleReleaseAar"])
78 |
79 | groupId = project.group as String
80 | artifactId = "android-coroutines-appcompat"
81 | artifact(sourcesJar).apply {
82 | classifier="sources"
83 | }
84 | pom {
85 | withXml {
86 | dependencies {
87 | dependency("$groupId:android-coroutines:[$version]", type = "aar")
88 | dependency(supportLibSpec)
89 | // all other dependencies are transitive
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/Maybe.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 |
4 | sealed class Maybe {
5 |
6 | data class Error(val e: Exception): Maybe() {
7 | override fun flatMap(function: (Nothing) -> R): Nothing {
8 | throw e
9 | }
10 |
11 | override fun select(ok: T, cancelled: T, error: T) = error
12 | }
13 |
14 | object Cancelled: Maybe() {
15 | override fun flatMap(function: (Nothing) -> R) = null
16 | override fun select(ok: T, cancelled: T, error: T) = cancelled
17 | }
18 |
19 | data class Ok(val data: T): Maybe() {
20 | override fun flatMap(function: (T) -> R): R = function(data)
21 | override fun select(ok: U, cancelled: U, error: U) = ok
22 | }
23 |
24 | abstract fun flatMap(function: (T) -> R): R?
25 |
26 | /**
27 | * Flatmap the identity function. Basically this gives the value for Ok, null when cancelled or
28 | * throws the exception for an error state.
29 | */
30 | fun flatMap(): T? = flatMap { it }
31 |
32 | /**
33 | * Create a new maybe with the function applied to the data (on Ok values only).
34 | * @param The function for the mapping.
35 | */
36 | @Suppress("unused")
37 | fun map(function: (T) -> R): Maybe {
38 | @Suppress("UNCHECKED_CAST")
39 | return when(this) {
40 | is Ok -> Ok(function(data))
41 | else -> this as Maybe
42 | }
43 | }
44 |
45 | /**
46 | * Helper to determine whether the maybe has a value.
47 | */
48 | val isOk get() = this is Ok
49 |
50 | interface ErrorCallback { fun onError(e: Exception) }
51 | interface CancellationCallback { fun onCancelled() }
52 | interface SuccessCallback { fun onOk(d: T) }
53 |
54 | fun onError(function: ErrorCallback) = if (this is Error) function.onError(e) else null
55 | fun onCancelled(function: CancellationCallback) = if (this is Cancelled) function.onCancelled() else null
56 | @Suppress("unused")
57 | fun onOk(function: SuccessCallback) = if (this is Ok) function.onOk(data) else null
58 |
59 | inline fun onError(function: Error.(Exception) -> R):R? = if (this is Error) function(e) else null
60 | inline fun onCancelled(function: Cancelled.() -> R):R? = if (this is Cancelled) function() else null
61 | inline fun onOk(function: Ok<*>.(T) -> R):R? = if (this is Ok) this.function(data) else null
62 |
63 | abstract fun select(ok: T, cancelled:T, error: T):T
64 |
65 | @Suppress("NOTHING_TO_INLINE")
66 | companion object {
67 | inline fun error(e: Exception): Maybe = Error(e)
68 |
69 | inline fun cancelled(): Maybe = Cancelled
70 |
71 | @Suppress("FunctionName")
72 | @Deprecated("Use Maybe.Ok instead", ReplaceWith("Maybe.Okvalue)"))
73 | inline fun Success(value: T) = Ok(value)
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/testapp/src/androidTest/java/nl/adaptivity/android/test/PlainCoroutineTestAndroid.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 | import com.esotericsoftware.kryo.Kryo
4 | import com.esotericsoftware.kryo.io.Input
5 | import com.esotericsoftware.kryo.io.Output
6 | import kotlinx.coroutines.*
7 | import kotlinx.coroutines.test.withTestContext
8 | import nl.adaptivity.android.kryo.kryoAndroid
9 | import org.junit.Test
10 |
11 | import org.junit.Assert.*
12 | import org.objenesis.strategy.StdInstantiatorStrategy
13 | import java.io.ByteArrayOutputStream
14 | import kotlin.coroutines.Continuation
15 | import kotlin.coroutines.suspendCoroutine
16 | import kotlin.coroutines.resume
17 |
18 | /**
19 | * Example local unit test, which will execute on the development machine (host).
20 | *
21 | * @see [Testing documentation](http://d.android.com/tools/testing)
22 | */
23 | class PlainCoroutineTestAndroid {
24 | private suspend fun foo(): String {
25 | yield()
26 | return "2"
27 | }
28 |
29 | @Test
30 | fun testClosureSerialization() = runBlocking {
31 | val def = async {
32 | // val kryo = Kryo().apply { instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) }
33 | val kryo = kryoAndroid
34 |
35 | var coroutine: Continuation? = null
36 |
37 | // Blocking scope cannot be serialized, as it holds a thread reference.
38 | GlobalScope.async(start = CoroutineStart.UNDISPATCHED) {
39 | val s = "Hello"
40 | suspendCoroutine { cont -> coroutine = cont }
41 | s
42 | }
43 |
44 |
45 | val baos = ByteArrayOutputStream()
46 |
47 | Output(baos).use { output ->
48 | kryo.writeClassAndObject(output, coroutine)
49 | }
50 |
51 | val serialized = baos.toByteArray()
52 | // val deserializedCoroutine = coroutine!!
53 | val deserializedCoroutine = kryo.readClassAndObject(Input(serialized)) as Continuation
54 | val resultField = deserializedCoroutine::class.java.getDeclaredField("result").apply { isAccessible=true }
55 | resultField.set(deserializedCoroutine, kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED)
56 |
57 | deserializedCoroutine.resume(Unit) // is not guaranteed to run here
58 |
59 | val deferred = deserializedCoroutine.context[Job] as Deferred
60 |
61 | val result = runBlocking {
62 | System.out.println("5")
63 | deferred.await().apply {
64 | System.out.println("6")
65 | }
66 | }
67 | System.out.println("5")
68 |
69 | assertEquals("Hello", result)
70 |
71 | coroutineContext[Job]!!.cancelAndJoin()
72 | }
73 | if (def.isActive) {
74 | delay(1000)
75 | if (def.isActive) {
76 | def.cancel(CancellationException("timeout"))
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/testapp/src/test/java/uk/ac/bmth/aprog/testapp/PlainCoroutineTest.kt:
--------------------------------------------------------------------------------
1 | package uk.ac.bmth.aprog.testapp
2 |
3 | import com.esotericsoftware.kryo.io.Input
4 | import com.esotericsoftware.kryo.io.Output
5 | import kotlinx.coroutines.*
6 | import nl.adaptivity.android.kryo.kryoAndroid
7 | import org.junit.Assert.assertEquals
8 | import org.junit.Assert.assertTrue
9 | import org.junit.Test
10 | import java.io.ByteArrayOutputStream
11 | import kotlin.coroutines.Continuation
12 | import kotlin.coroutines.resume
13 | import kotlin.coroutines.suspendCoroutine
14 |
15 | /**
16 | * Example local unit test, which will execute on the development machine (host).
17 | *
18 | * @see [Testing documentation](http://d.android.com/tools/testing)
19 | */
20 | class PlainCoroutineTest {
21 | private suspend fun foo(): String {
22 | yield()
23 | return "2"
24 | }
25 |
26 | @Test
27 | fun testClosureImpl() {
28 | lateinit var coroutine: Continuation
29 | val deferred = GlobalScope.async(start = CoroutineStart.UNDISPATCHED) {
30 | val s = "Hello"
31 | val i1 = suspendCoroutine { cont ->
32 | coroutine = cont
33 | }
34 | assertEquals(2, i1)
35 | s
36 | }
37 |
38 | assertTrue(deferred.isActive)
39 |
40 | coroutine.resume(2)
41 |
42 | // assertTrue(deferred.isCompleted)
43 |
44 | val s2 = runBlocking { deferred.await() }
45 | assertEquals("Hello", s2)
46 | }
47 |
48 | @Test
49 | fun testClosureSerialization() {
50 | // val kryo = Kryo().apply { instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) }
51 | val kryo = kryoAndroid
52 |
53 | lateinit var coroutine: Continuation
54 |
55 | // Create a coroutine and the suspend it.
56 | // It needs to run out of the blocking scope as it suspends. Otherwise it will never return.
57 | GlobalScope.async(start = CoroutineStart.UNDISPATCHED) {
58 | val s = "Hello"
59 | val i = suspendCoroutine { cont -> coroutine = cont }
60 | assertEquals(3, i)
61 | s
62 | }
63 |
64 | val baos = ByteArrayOutputStream()
65 |
66 | // Write the coroutine to a bytearray
67 | Output(baos).use { output ->
68 | kryo.writeClassAndObject(output, coroutine)
69 | }
70 |
71 | val serialized = baos.toByteArray()
72 | val deserializedCoroutine = kryo.readClassAndObject(Input(serialized)) as Continuation
73 |
74 | deserializedCoroutine.resume(3) // is not guaranteed to run here
75 | coroutine.resume(3)
76 |
77 | val deferredDeserialized = deserializedCoroutine.context[Job] as Deferred
78 |
79 | val result = run {
80 | System.out.println("5")
81 | runBlocking {
82 | deferredDeserialized.await().apply {
83 | System.out.println("6: ${this}")
84 | }
85 | }
86 | }
87 | System.out.println("7")
88 |
89 | assertEquals("Hello", result)
90 |
91 | }
92 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/FragmentCoroutineScopeWrapper.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.app.Fragment
5 | import android.app.FragmentManager
6 | import android.content.Intent
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.support.annotation.IdRes
10 | import android.support.annotation.RequiresApi
11 | import android.view.View
12 | import kotlinx.android.extensions.LayoutContainer
13 | import kotlinx.coroutines.CoroutineScope
14 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
15 | import nl.adaptivity.android.coroutines.impl.DelegateLayoutContainer
16 | import kotlin.coroutines.CoroutineContext
17 | import kotlin.coroutines.suspendCoroutine
18 |
19 | class FragmentCoroutineScopeWrapper(
20 | private val parentScope: CoroutineScope
21 | ) : FragmentCoroutineScope, LayoutContainer {
22 | val activity: Activity? get() = fragment.activity
23 |
24 | override val fragment: F get() = coroutineContext[FragmentContext]!!.fragment as F
25 |
26 | val fragmentManager: FragmentManager? get() = fragment.fragmentManager
27 |
28 | override fun getAndroidContext() = activity
29 |
30 | override val containerView: View? get() = fragment.view
31 |
32 | fun findViewById(@IdRes id: Int): T? = fragment.view?.findViewById(id)
33 |
34 | suspend fun startActivityForResult(intent: Intent): ActivityResult {
35 | return suspendCoroutine { continuation ->
36 | val fragment =
37 | (continuation.context[FragmentContext]?.fragment
38 | ?: throw IllegalStateException("Missing fragment in context")) as Fragment
39 |
40 | val activity = activity
41 | ?: throw java.lang.IllegalStateException("The fragment must be attached to start another activity")
42 |
43 | val contFragment: RetainedContinuationFragment =
44 | activity.ensureRetainingFragment()
45 | val resultCode: Int = contFragment.lastResultCode + 1
46 |
47 | contFragment.addContinuation(
48 | ParcelableContinuation(
49 | continuation,
50 | fragment.activity,
51 | resultCode
52 | )
53 | )
54 |
55 | activity.runOnUiThread {
56 | contFragment.startActivityForResult(intent, resultCode)
57 | }
58 | }
59 |
60 | }
61 |
62 | fun startActivity(intent: Intent) = fragment.startActivity(intent)
63 |
64 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
65 | fun startActivity(intent: Intent, options: Bundle) =
66 | fragment.startActivity(intent, options)
67 |
68 |
69 | @Suppress("unused")
70 | suspend fun layoutContainer(body: LayoutContainer.() -> R): R {
71 | return DelegateLayoutContainer(fragment.view).body()
72 | }
73 |
74 | @Suppress("unused")
75 | inline fun withFragment(body: F.() -> R): R {
76 | return fragment.body()
77 | }
78 |
79 | override val coroutineContext: CoroutineContext get() = parentScope.coroutineContext
80 | }
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import libraries.*
2 | import versions.selfVersion
3 |
4 | plugins {
5 | id("com.android.library")
6 | id("org.jetbrains.kotlin.android")
7 | id("kotlin-android-extensions")
8 | id("maven-publish")
9 | id("org.jetbrains.dokka")
10 | idea
11 | }
12 |
13 | version = selfVersion
14 | group = "net.devrieze"
15 | description = "Library to add coroutine support for Android flow"
16 |
17 | repositories {
18 | mavenLocal()
19 | mavenCentral()
20 | google()
21 | }
22 |
23 | val reqCompileSdkVersion:String by project
24 | val reqTargetSdkVersion:String by project
25 | val reqMinSdkVersion:String by project
26 |
27 | android {
28 | compileSdkVersion(reqCompileSdkVersion.toInt())
29 |
30 | defaultConfig {
31 | minSdkVersion(reqMinSdkVersion.toInt())
32 | targetSdkVersion(reqTargetSdkVersion.toInt())
33 | versionName = selfVersion
34 | }
35 |
36 | compileOptions {
37 | sourceCompatibility = JavaVersion.VERSION_1_8
38 | targetCompatibility = JavaVersion.VERSION_1_8
39 | }
40 |
41 | packagingOptions {
42 | pickFirst("META-INF/atomicfu.kotlin_module")
43 | }
44 | }
45 |
46 | dependencies {
47 | implementation(supportLibSpec)
48 | implementation(kryoSpec)
49 | implementation(kotlinlibSpec)
50 | implementation(androidExtensionRuntimeSpec)
51 |
52 | api(coroutinesSpec)
53 | api(coroutinesAndroidSpec)
54 | }
55 |
56 | val sourcesJar = task("androidSourcesJar") {
57 | classifier = "sources"
58 | from(android.sourceSets["main"].java.srcDirs)
59 | }
60 |
61 | androidExtensions {
62 | isExperimental = true
63 | }
64 |
65 | /*
66 | tasks.withType {
67 | externalDocumentationLink(delegateClosureOf {
68 | url = URL("https://developer.android.com/reference/")
69 | })
70 | linkMappings.add(LinkMapping().apply {
71 | dir = "src/main/java"
72 | url = "https://github.com/pdvrieze/android-coroutines/tree/master/core/src/main/java"
73 | suffix = "#L"
74 | })
75 | outputFormat = "html"
76 | }
77 | */
78 |
79 |
80 | afterEvaluate {
81 | publishing {
82 | (publications) {
83 | create("MyPublication") {
84 | artifact(tasks["bundleReleaseAar"])
85 |
86 | groupId = project.group as String
87 | artifactId = "android-coroutines"
88 | artifact(sourcesJar).apply {
89 | classifier = "sources"
90 | }
91 | pom {
92 | withXml {
93 | dependencies {
94 | dependency(kryoSpec)
95 | dependency(kotlinlibSpec)
96 | dependency(androidExtensionRuntimeSpec)
97 |
98 | dependency(coroutinesAndroidSpec, type = "jar")
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
106 | }
107 |
108 | idea {
109 | module {
110 | name = "android-coroutines.core"
111 | }
112 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/SuspendableDialog.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.app.DialogFragment
5 | import android.content.DialogInterface
6 | import android.os.Bundle
7 | import kotlinx.coroutines.CancellationException
8 | import kotlinx.coroutines.suspendCancellableCoroutine
9 |
10 | /**
11 | * Base class for dialog fragments that support coroutine based dialog invocation. Direct instantiation
12 | * probably makes no sense, subclassing is expected.
13 | */
14 | open class SuspendableDialog: DialogFragment() {
15 |
16 |
17 | private var callback: ParcelableContinuation>? = null
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | savedInstanceState?.getParcelable>>("continutation").let { callback = it }
22 | }
23 |
24 | /**
25 | * Actually show the fragment and get the result. This requires the dialog
26 | * code to invoke [dispatchResult] on succesful completion.
27 | */
28 | suspend fun show(activity: Activity, tag: String) : Maybe {
29 | super.show(activity.fragmentManager, tag)
30 | val d = this
31 | return suspendCancellableCoroutine { cont ->
32 | callback?.cancel(activity)
33 | callback = ParcelableContinuation(cont, activity)
34 | }
35 | }
36 |
37 | /**
38 | * Not only implement the standard functionality, but also use this as a cancellation on
39 | * the dialog. If the continuation was not cancellable this will equal to resuming with a
40 | * null result.
41 | */
42 | override fun onDismiss(dialog: DialogInterface?) {
43 | super.onDismiss(dialog)
44 | callback?.let { callback ->
45 | this.callback = null // Set the property to null to prevent reinvocation
46 | callback.cancel(activity, CancellationException("Dialog dismissed"))
47 | }
48 | }
49 |
50 | /**
51 | * Not only implement the standard functionality, but also use this as a cancellation on
52 | * the dialog. If the continuation was not cancellable this will equal to resuming with a
53 | * null result. Functionally equivalent to [onDismiss]
54 | */
55 | override fun onCancel(dialog: DialogInterface?) {
56 | super.onCancel(dialog)
57 | callback?.let { callback ->
58 | this.callback = null // Set the property to null to prevent reinvocation
59 | callback.cancel(activity, CancellationException("Dialog dismissed"))
60 | }
61 | }
62 |
63 | /**
64 | * Subclasses must call this to resume [show] with the expected result.
65 | */
66 | protected fun dispatchResult(resultValue: T) {
67 | callback?.let { callback ->
68 | this.callback = null // Set the property to null to prevent reinvocation
69 | callback.resume(activity, Maybe.Ok(resultValue))
70 | }
71 | }
72 |
73 | override fun onSaveInstanceState(outState: Bundle) {
74 | super.onSaveInstanceState(outState)
75 | outState.putParcelable("continutation", callback)
76 | }
77 | }
78 |
79 | @Deprecated("Compatibility alias, as Maybe should be used, not DialogResult", ReplaceWith("Maybe"))
80 | typealias DialogResult = Maybe
--------------------------------------------------------------------------------
/appcompat/src/main/java/nl/adaptivity/android/coroutinesCompat/AppcompatFragmentCoroutineScopeWrapper.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutinesCompat
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.support.annotation.IdRes
8 | import android.support.annotation.RequiresApi
9 | import android.support.v4.app.Fragment
10 | import android.support.v4.app.FragmentActivity
11 | import android.support.v4.app.FragmentManager
12 | import android.view.View
13 | import kotlinx.android.extensions.LayoutContainer
14 | import kotlinx.coroutines.CoroutineScope
15 | import nl.adaptivity.android.coroutines.ActivityResult
16 | import nl.adaptivity.android.coroutines.ParcelableContinuation
17 | import nl.adaptivity.android.coroutines.RetainedContinuationFragment
18 | import nl.adaptivity.android.coroutines.ensureRetainingFragment
19 | import nl.adaptivity.android.coroutines.impl.DelegateLayoutContainer
20 | import kotlin.coroutines.CoroutineContext
21 | import kotlin.coroutines.suspendCoroutine
22 |
23 | class AppcompatFragmentCoroutineScopeWrapper(
24 | private val parentScope: CoroutineScope
25 | ) : AppcompatFragmentCoroutineScope, LayoutContainer {
26 | val activity: FragmentActivity? get() = fragment.activity
27 |
28 | override val fragment: F get() = coroutineContext[AppcompatFragmentContext]!!.fragment as F
29 |
30 | val fragmentManager: FragmentManager? get() = fragment.fragmentManager
31 |
32 | override fun getAndroidContext() = activity
33 |
34 | override val containerView: View? get() = fragment.view
35 |
36 | fun findViewById(@IdRes id: Int): T? = fragment.view?.findViewById(id)
37 |
38 | suspend fun startActivityForResult(intent: Intent): ActivityResult {
39 | return suspendCoroutine { continuation ->
40 | val fragment =
41 | (continuation.context[AppcompatFragmentContext]?.fragment
42 | ?: throw IllegalStateException("Missing fragment in context")) as Fragment
43 |
44 | val activity = activity
45 | ?: throw java.lang.IllegalStateException("The fragment must be attached to start another activity")
46 |
47 | val contFragment: RetainedContinuationFragment =
48 | activity.ensureRetainingFragment()
49 | val resultCode: Int = contFragment.lastResultCode + 1
50 |
51 | contFragment.addContinuation(
52 | ParcelableContinuation(
53 | continuation,
54 | fragment.activity,
55 | resultCode
56 | )
57 | )
58 |
59 | activity.runOnUiThread {
60 | contFragment.startActivityForResult(intent, resultCode)
61 | }
62 | }
63 |
64 | }
65 |
66 | fun startActivity(intent: Intent) = fragment.startActivity(intent)
67 |
68 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
69 | fun startActivity(intent: Intent, options: Bundle) =
70 | fragment.startActivity(intent, options)
71 |
72 |
73 | @Suppress("unused")
74 | suspend fun layoutContainer(body: LayoutContainer.() -> R): R {
75 | return DelegateLayoutContainer(fragment.view).body()
76 | }
77 |
78 | @Suppress("unused")
79 | inline fun withFragment(body: F.() -> R): R {
80 | return fragment.body()
81 | }
82 |
83 | override val coroutineContext: CoroutineContext get() = parentScope.coroutineContext
84 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **This library works, but it's API is not yet stable. It was developed as proof of concept, the best API (esp names) was not a core consideration.**
2 |
3 | # android-coroutines [](http://www.apache.org/licenses/LICENSE-2.0)
4 | While Android is powerful it's activity model suffers from callback hell. Kotlin coroutines are supposed to fix this, but
5 | Android is special. Your code can be kicked out of memory at any moment so serialization interferes with coroutines.
6 | Getting events back to the coroutine depends on your activity (actually we can use fragments instead - fragments can
7 | be difficult, but great here)
8 |
9 | [API documentation](https://pdvrieze.github.io/android-coroutines/android-coroutines/)
10 |
11 | ## Core features
12 | The system supports (using [Kryo](https://github.com/EsotericSoftware/kryo)) serialization of
13 | coroutines and other functions. It knows about Kotlin and Android and will handle (accidental or
14 | convenience) capture of Context and Kotlin objects. It doesn't yet support all
15 | Android state, in particular Fragment and View objects (which would need to be looked up through the
16 | context).
17 |
18 | ## Help wanted
19 | This is a side-effect of my main (academic work). Any help people want to provide is more than
20 | welcome. In particular, the following help is more than welcome.
21 | - Documentation
22 | - Example code
23 | - More features
24 | - Feedback on design and API
25 | - Test feedback
26 | - Support presence of Fragments and Views in the capture context (referring back to the activity to)
27 | resolve them.
28 |
29 | # Currently supported functionality
30 | ## startActivityForResult
31 | In your activity you can use a coroutine to just get the result of invoking another one:
32 |
33 | ```kotlin
34 | fun onButtonClick(v:View) {
35 | launch {
36 | activityResult(Intent(MyDelegateActivity::class.java)).onOk { resultIntent ->
37 | runOnUiThread { textView.text = newText }
38 | }
39 | }
40 | }
41 | ```
42 |
43 | There are some variations of this, and the `Maybe` implementation used has many options.
44 |
45 | ## `SuspendableDialog`
46 | In many cases you have a dialog to get some input from the user. From yes-no questions to input of values. SuspendableDialog is
47 | a subclass of `DialogFragment` that provides the building blocks to have a dialog that is used in a coroutine. The actual dialog
48 | implementation just has to invoke `dispatchResult` with the appropriate result value and the dialog is handled. Dismissal or
49 | cancellation are handled by default.
50 |
51 | ## DownloadManager
52 | **Warning - not quite complete**
53 |
54 | Using the download manager is not quite straightforward even though you get a lot for free. The `DownloadFragment.download(Activity, Uri)`
55 | function will download your file and resume your coroutine when complette.
56 |
57 | ### TODO(DownloadManager)
58 | - Handle download completion when the activity/fragment is not visible (on resume check the status and invoke the continuation
59 | as appropriate)
60 |
61 | ## AccountManager
62 | AccountManager is not pretty, but this class makes it a bit prettier. In particular it implements a suspending wrapper
63 | around getAuthToken that will even invoke a permission intent if needed (as was possible before Kitkat, but will no longer work).
64 |
65 | ### TODO(AccountManager)
66 | - Implement extension functions for all the client-side API of `AccountManager`.
67 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/ParcelableContinuationCompat.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.os.Parcel
6 | import android.os.Parcelable
7 | import android.util.Log
8 | import com.esotericsoftware.kryo.io.Input
9 | import nl.adaptivity.android.kryo.kryoAndroid
10 | import kotlin.coroutines.Continuation
11 | import kotlin.coroutines.resume
12 | import kotlin.coroutines.resumeWithException
13 |
14 | /**
15 | * Java compatibility helper factory method
16 | */
17 | @Suppress("FunctionName")
18 | @JvmOverloads
19 | fun ParcelableContinuation(handler: SerializableHandler, requestCode: Int = -1)
20 | = ParcelableContinuationCompat({ handler(this, it) }, requestCode)
21 |
22 | @Suppress("FunctionName")
23 | fun ParcelableContinuation(handler: A.(T) -> Unit, requestCode: Int = -1)
24 | = ParcelableContinuationCompat({ handler(this, it) }, requestCode)
25 |
26 | /**
27 | * [ParcelableContinuation] subclass that not only works with continuations, but also handles
28 | * Java and Kotlin callback lambdas.
29 | *
30 | * @param requestCode The request code that this continuation should resume on.
31 | * @param handlerOrContinuation The executable object that handles the result.
32 | */
33 | class ParcelableContinuationCompat private constructor(requestCode: Int, handlerOrContinuation: Any): ParcelableContinuation(requestCode, handlerOrContinuation) {
34 |
35 | /**
36 | * Create a new continuation with a lambda function callback.
37 | */
38 | constructor(handler: A.(T) -> Unit, requestCode: Int = -1): this(requestCode, handlerOrContinuation = handler)
39 |
40 | /**
41 | * Create a new continuation with a continuation callback.
42 | */
43 | @Suppress("unused")
44 | constructor(handler: Continuation, requestCode: Int = -1): this(requestCode, handlerOrContinuation = handler)
45 |
46 |
47 | /**
48 | * Inflate the object from the parcel.
49 | * @param parcel The parcel to inflate from
50 | *
51 | * @see Parcelable.Creator.createFromParcel
52 | */
53 | @Suppress("UNCHECKED_CAST")
54 | constructor(parcel: Parcel) :
55 | this(parcel.readInt(), handlerOrContinuation = ByteArray(parcel.readInt()).also { parcel.readByteArray(it) } ) {
56 | Log.d(TAG, "Read continuation from parcel")
57 | }
58 |
59 | /**
60 | * Helper function that performs the delayed deflation (from the byte array Kryo creates).
61 | */
62 | private fun resolve(context: Context): Any {
63 | val h = continuation
64 | return when (h) {
65 | is ByteArray -> kryoAndroid(context).readClassAndObject(Input(h)).also { continuation = it }
66 | else -> h
67 | }
68 | }
69 |
70 | override fun resume(context: Context, value: T) {
71 | val h = resolve(context)
72 | @Suppress("UNCHECKED_CAST")
73 | when (h) {
74 | is Continuation<*> -> (h as Continuation).resume(value)
75 | is Function<*> -> (h as Context.(T?)->Unit).invoke(context, value)
76 | else -> throw IllegalStateException("Invalid continuation: ${h::class.java.name}")
77 | }
78 | }
79 |
80 | override fun resumeWithException(context: Context, exception: Throwable) {
81 | val h = resolve(context)
82 | @Suppress("UNCHECKED_CAST")
83 | when (h) {
84 | is Continuation<*> -> h.resumeWithException(exception)
85 | is Function<*> -> (h as Context.(T?)->Unit).invoke(context, null)
86 | else -> throw IllegalStateException("Invalid continuation: ${h::class.java.name}")
87 | }
88 | }
89 |
90 | /**
91 | * Helper class for [Parcelable]
92 | * @see Parcelable.Creator
93 | */
94 | companion object CREATOR : Parcelable.Creator> {
95 | override fun createFromParcel(parcel: Parcel): ParcelableContinuationCompat {
96 | return ParcelableContinuationCompat(parcel)
97 | }
98 |
99 | override fun newArray(size: Int): Array?> {
100 | return arrayOfNulls(size)
101 | }
102 |
103 | @JvmStatic
104 | val TAG: String = ParcelableContinuationCompat::class.java.simpleName
105 | }
106 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/kryo/AndroidKotlinResolver.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import android.accounts.AccountManager
4 | import android.app.Activity
5 | import android.app.Fragment
6 | import android.content.Context
7 | import android.support.v4.app.FragmentActivity
8 | import com.esotericsoftware.kryo.Registration
9 | import com.esotericsoftware.kryo.serializers.FieldSerializer
10 | import com.esotericsoftware.kryo.util.DefaultClassResolver
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.android.HandlerDispatcher
13 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
14 | import nl.adaptivity.android.coroutines.contexts.FragmentContext
15 | import nl.adaptivity.android.kryo.serializers.*
16 | import java.lang.ref.Reference
17 | import kotlin.coroutines.CoroutineContext
18 |
19 | open class AndroidKotlinResolver(protected val context: Context?) : DefaultClassResolver() {
20 |
21 | override fun getRegistration(type: Class<*>): Registration? {
22 | val c = context
23 | val superReg = super.getRegistration(type)
24 | return when {
25 | superReg!=null -> superReg
26 | type.superclass==null -> superReg
27 | // For now this is actually unique, but this is not very stable.
28 | HandlerDispatcher::class.java.isAssignableFrom(type) ->
29 | register(Registration(type, kryo.pseudoObjectSerializer(Dispatchers.Main), NAME))
30 | AndroidContext.Key::class.java == type ->
31 | register(Registration(type, kryo.pseudoObjectSerializer(AndroidContext.Key), NAME))
32 | FragmentContext.Key::class.java == type ->
33 | register(Registration(type, kryo.pseudoObjectSerializer(FragmentContext.Key), NAME))
34 | "nl.adaptivity.android.coroutinesCompat.AppcompatFragmentContext\$Key" == type.name -> {
35 | register(Registration(type, kryo.pseudoObjectSerializer(APPCOMPATFRAGMENTCONTEXT_KEY), NAME))
36 | }
37 | c!=null && c.javaClass == type ->
38 | register(Registration(type, ContextSerializer(context), NAME))
39 | Thread::class.java.isAssignableFrom(type) ->
40 | throw IllegalArgumentException("Serializing threads is never valid")
41 | Context::class.java.isAssignableFrom(type.superclass) ->
42 | register(Registration(type, ContextSerializer(context), NAME))
43 | context is Activity && Fragment::class.java.isAssignableFrom(type.superclass) ->
44 | register(Registration(type, FragmentSerializer(context), NAME))
45 | context is FragmentActivity && android.support.v4.app.Fragment::class.java.isAssignableFrom(type.superclass) ->
46 | register(Registration(type, SupportFragmentSerializer(context), NAME))
47 | Reference::class.java.isAssignableFrom(type) ->
48 | register(Registration(type, ReferenceSerializer(kryo, type.asSubclass(Reference::class.java)), NAME))
49 | Function::class.java.isAssignableFrom(type.superclass) ->
50 | register(Registration(type, FieldSerializer(kryo, type).apply { setIgnoreSyntheticFields(false) }, NAME))
51 | AccountManager::class.java.isAssignableFrom(type) ->
52 | register(Registration(type, AccountManagerSerializer(kryo, type, c), NAME))
53 | type.superclass?.name=="kotlin.coroutines.jvm.internal.ContinuationImpl" ->
54 | register(Registration(type, ContinuationImplSerializer(kryo, type), NAME))
55 | type.superclass?.name=="kotlin.coroutines.experimental.jvm.internal.CoroutineImpl" ->
56 | register(Registration(type, CoroutineImplSerializer(kryo, type), NAME))
57 | // Requires the reflection library
58 | // type.kotlin.isCompanion -> register(Registration(type, kryo.pseudoObjectSerializer(type.kotlin.objectInstance), NAME))
59 | type.isKObject -> register(Registration(type, ObjectSerializer(kryo, type), NAME))
60 | else -> null
61 | }
62 | }
63 |
64 | companion object {
65 | const val TAG = "AndroidKotlinResolver"
66 | const val NAME = DefaultClassResolver.NAME.toInt()
67 | val APPCOMPATFRAGMENTCONTEXT_CLASS = try {
68 | Class.forName("nl.adaptivity.android.coroutinesCompat.AppcompatFragmentContext")
69 | } catch (e: ClassNotFoundException) { null }
70 | val APPCOMPATFRAGMENTCONTEXT_KEY: CoroutineContext.Key<*>? = APPCOMPATFRAGMENTCONTEXT_CLASS?.let { cl ->
71 | val inst = cl.getDeclaredField("Key")
72 | inst.get(null) as CoroutineContext.Key<*>
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/accountmanager.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("AccountManagerUtil")
2 |
3 | package nl.adaptivity.android.coroutines
4 |
5 | import android.accounts.Account
6 | import android.accounts.AccountManager
7 | import android.accounts.AccountManagerCallback
8 | import android.accounts.AccountManagerFuture
9 | import android.app.Activity
10 | import android.content.Context
11 | import android.content.Intent
12 | import android.os.Bundle
13 | import android.support.annotation.RequiresPermission
14 | import kotlinx.coroutines.CancellableContinuation
15 | import kotlinx.coroutines.CancellationException
16 | import kotlinx.coroutines.InternalCoroutinesApi
17 | import kotlinx.coroutines.suspendCancellableCoroutine
18 | import kotlin.coroutines.resume
19 |
20 | // TODO This class is far from complete. Various account manager operations could be added.
21 |
22 | /**
23 | * Get an authentication token from the account manager asynchronously. If required it will
24 | * take care of launching the permissions dialogs as needed.
25 | *
26 | * @see [AccountManager.getAuthToken]
27 | */
28 | @RequiresPermission("android.permission.USE_CREDENTIALS")
29 | @Deprecated("Use the safer one that takes an ActivityCoroutineScope")
30 | suspend fun AccountManager.getAuthToken(activity: A, account: Account, authTokenType: String, options: Bundle? = null): String? {
31 | @Suppress("DEPRECATION")
32 | val resultBundle = callAsync { callback -> getAuthToken(account, authTokenType, options, false, callback, null) }
33 | if (resultBundle.containsKey(AccountManager.KEY_INTENT)) {
34 | val intent = resultBundle.get(AccountManager.KEY_INTENT) as Intent
35 | val activityResult = activity.activityResult(intent)
36 | @Suppress("DEPRECATION")
37 | return activityResult.onOk { AccountManager.get(activity).getAuthToken(activity, account, authTokenType, options) }
38 | } else {
39 | return resultBundle.getString(AccountManager.KEY_AUTHTOKEN)
40 | }
41 | }
42 |
43 | /**
44 | * Get an authentication token from the account manager asynchronously. If required it will
45 | * take care of launching the permissions dialogs as needed.
46 | *
47 | * @see [AccountManager.getAuthToken]
48 | */
49 | @RequiresPermission("android.permission.USE_CREDENTIALS")
50 | suspend fun AndroidContextCoroutineScope.getAuthToken(account: Account, authTokenType: String, options: Bundle? = null): String? {
51 | val resultBundle = callAccountManagerAsync { callback -> getAuthToken(account, authTokenType, options, false, callback, null) }
52 | if (resultBundle.containsKey(AccountManager.KEY_INTENT)) {
53 | val intent = resultBundle.get(AccountManager.KEY_INTENT) as Intent
54 | val activityResult = getAndroidContext().activityResult(intent)
55 | return activityResult.onOk { getAuthToken(account, authTokenType, options) }
56 | } else {
57 | return resultBundle.getString(AccountManager.KEY_AUTHTOKEN)
58 | }
59 | }
60 |
61 | /**
62 | * Callback class that uses a continuation as the callback for the account manager. Note that
63 | * this callback is NOT designed to survive the destruction of the [Context] ([Activity]).
64 | *
65 | * @property cont The continuation that will be invoked on completion.
66 | */
67 | class CoroutineAccountManagerCallback(private val cont: CancellableContinuation) : AccountManagerCallback {
68 | @UseExperimental(InternalCoroutinesApi::class)
69 | override fun run(future: AccountManagerFuture) {
70 | try {
71 | if (future.isCancelled) {
72 | cont.cancel()
73 | } else {
74 | cont.resume(future.result)
75 | }
76 | } catch (e: Exception) {
77 | if (e is CancellationException) {
78 | cont.cancel(e)
79 | } else {
80 | cont.tryResumeWithException(e)
81 | }
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * Helper function that helps with calling account manager operations asynchronously.
88 | *
89 | * @receiver The account manager. This is actually not stable.
90 | */
91 | @Deprecated("Use a special ContextCoroutine that doesn't put a context in the capture", ReplaceWith("callAccountManagerAsync(context, operation)"))
92 | suspend inline fun AccountManager.callAsync(crossinline operation: AccountManager?.(CoroutineAccountManagerCallback) -> Unit): R {
93 | return suspendCancellableCoroutine { cont ->
94 | operation(CoroutineAccountManagerCallback(cont))
95 | }
96 | }
97 |
98 | /**
99 | * Helper function that helps with calling account manager operations asynchronously.
100 | */
101 | suspend inline fun AndroidContextCoroutineScope<*,*>.callAccountManagerAsync(crossinline operation: AccountManager.(CoroutineAccountManagerCallback) -> Unit): R {
102 | val androidContext = getAndroidContext()
103 | return suspendCancellableCoroutine { cont ->
104 | AccountManager.get(androidContext).operation(CoroutineAccountManagerCallback(cont))
105 | }
106 | }
107 |
108 | /**
109 | * Determine whether the account manager has the given features. This is the suspending equivalent of
110 | * [AccountManager.hasFeatures].
111 | *
112 | * @see [AccountManager.hasFeatures].
113 | */
114 | @Suppress("DEPRECATION")
115 | @Deprecated("Use ActivityCoroutineScope version")
116 | suspend fun AccountManager.hasFeatures(account: Account, features: Array): Boolean {
117 | return callAsync { callback -> hasFeatures(account, features, callback, null) }
118 | }
119 |
120 | /**
121 | * Determine whether the account manager has the given features. This is the suspending equivalent of
122 | * [AccountManager.hasFeatures].
123 | *
124 | * @see [AccountManager.hasFeatures].
125 | */
126 | suspend fun AndroidContextCoroutineScope<*,*>.accountHasFeatures(account: Account, features: Array): Boolean {
127 | return callAccountManagerAsync { callback -> hasFeatures(account, features, callback, null) }
128 | }
129 |
--------------------------------------------------------------------------------
/testapp/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/testapp/src/main/java/nl/adaptivity/android/kryo/LineOutput.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.kryo
2 |
3 | import com.esotericsoftware.kryo.io.Output
4 | import java.io.OutputStream
5 |
6 | class LineOutput(outStream: OutputStream) : Output(outStream) {
7 |
8 | var writer = outStream.bufferedWriter()
9 |
10 | override fun writeShort(value: Int) {
11 | super.writeShort(value)
12 | }
13 |
14 | override fun clear() {
15 | throw UnsupportedOperationException()
16 | }
17 |
18 | fun rawByte(value: Byte) {
19 | writer.write(value.toInt())
20 | }
21 |
22 | override fun writeString(value: String?) {
23 | if (value == null) {
24 | rawByte(0)
25 | } else {
26 | writer.write(value)
27 | }
28 | writer.write('\n'.toInt())
29 | }
30 |
31 | override fun writeString(value: CharSequence?) {
32 | writeString(value?.toString())
33 | }
34 |
35 | override fun writeBytes(bytes: ByteArray) {
36 | writer.flush()
37 | outputStream.write(bytes)
38 | }
39 |
40 | override fun writeBytes(bytes: ByteArray, offset: Int, count: Int) {
41 | writer.flush()
42 | outputStream.write(bytes, offset, count)
43 | }
44 |
45 | override fun writeFloats(value: FloatArray?) {
46 | if (value == null) {
47 | rawByte(0)
48 | } else {
49 | value.joinTo(writer) { it.toString() }
50 | }
51 | writer.write('\n'.toInt())
52 | }
53 |
54 | override fun writeDoubles(value: DoubleArray?) {
55 | if (value == null) {
56 | rawByte(0)
57 | } else {
58 | value.joinTo(writer) { it.toString() }
59 | }
60 | writer.write('\n'.toInt())
61 | }
62 |
63 | override fun write(value: Int) {
64 | super.write(value)
65 | }
66 |
67 | override fun write(bytes: ByteArray?) {
68 | super.write(bytes)
69 | }
70 |
71 | override fun write(bytes: ByteArray?, offset: Int, length: Int) {
72 | super.write(bytes, offset, length)
73 | }
74 |
75 | override fun flush() {
76 | writer.flush()
77 | super.flush()
78 | }
79 |
80 | override fun writeChar(value: Char) {
81 | writer.write(value.toString())
82 | writer.write('\n'.toInt())
83 | }
84 |
85 | override fun writeBoolean(value: Boolean) {
86 | writer.write(value.toString())
87 | writer.write('\n'.toInt())
88 | }
89 |
90 | override fun writeInt(value: Int) {
91 | writer.write(value.toString())
92 | writer.write('\n'.toInt())
93 | }
94 |
95 | override fun writeInt(value: Int, optimizePositive: Boolean): Int {
96 | writer.write(value.toString())
97 | writer.write('\n'.toInt())
98 | return 1
99 | }
100 |
101 | override fun writeShorts(value: ShortArray?) {
102 | if (value == null) {
103 | rawByte(0)
104 | } else {
105 | value.joinTo(writer) { it.toString() }
106 | }
107 | writer.write('\n'.toInt())
108 | }
109 |
110 | override fun writeLongs(value: LongArray?, optimizePositive: Boolean) {
111 | if (value == null) {
112 | rawByte(0)
113 | } else {
114 | value.joinTo(writer) { it.toString() }
115 | }
116 | writer.write('\n'.toInt())
117 | }
118 |
119 | override fun writeLongs(value: LongArray?) {
120 | if (value == null) {
121 | rawByte(0)
122 | } else {
123 | value.joinTo(writer) { it.toString() }
124 | }
125 | writer.write('\n'.toInt())
126 | }
127 |
128 | override fun close() {
129 | writer.close()
130 | outputStream.close()
131 | }
132 |
133 | override fun writeInts(value: IntArray?, optimizePositive: Boolean) {
134 | if (value == null) {
135 | rawByte(0)
136 | } else {
137 | value.joinTo(writer) { it.toString() }
138 | }
139 | writer.write('\n'.toInt())
140 | }
141 |
142 | override fun writeInts(value: IntArray?) {
143 | if (value == null) {
144 | rawByte(0)
145 | } else {
146 | value.joinTo(writer) { it.toString() }
147 | }
148 | writer.write('\n'.toInt())
149 | }
150 |
151 | override fun writeLong(value: Long) {
152 | writer.write(value.toString())
153 | writer.write('\n'.toInt())
154 | }
155 |
156 | override fun writeLong(value: Long, optimizePositive: Boolean): Int {
157 | writeLong(value)
158 | return 1
159 | }
160 |
161 | override fun writeDouble(value: Double) {
162 | writer.write(value.toString())
163 | writer.write('\n'.toInt())
164 | }
165 |
166 | override fun writeDouble(value: Double, precision: Double, optimizePositive: Boolean): Int {
167 | writer.write(value.toString())
168 | writer.write('\n'.toInt())
169 | return 1
170 | }
171 |
172 | override fun writeByte(value: Byte) {
173 | writer.write(value.toString(16).padStart(2, '0'))
174 | writer.write('\n'.toInt())
175 | }
176 |
177 | override fun writeByte(value: Int) {
178 | writeByte(value.toByte())
179 | }
180 |
181 | override fun setOutputStream(outputStream: OutputStream) {
182 | writer.flush()
183 | writer = outputStream.bufferedWriter()
184 | super.setOutputStream(outputStream)
185 | }
186 |
187 | override fun writeFloat(value: Float) {
188 | writer.write(value.toString())
189 | writer.write('\n'.toInt())
190 | }
191 |
192 | override fun writeFloat(value: Float, precision: Float, optimizePositive: Boolean): Int {
193 | writer.write(value.toString())
194 | writer.write('\n'.toInt())
195 | return 1
196 | }
197 |
198 | override fun writeChars(value: CharArray?) {
199 | writeString(value?.let { String(it) })
200 | }
201 |
202 | override fun writeAscii(value: String?) {
203 | writeString(value)
204 | }
205 |
206 | override fun writeVarInt(value: Int, optimizePositive: Boolean): Int {
207 | writer.write(value.toString())
208 | writer.write('\n'.toInt())
209 | return 1
210 | }
211 |
212 | override fun writeVarLong(value: Long, optimizePositive: Boolean): Int {
213 | writer.write(value.toString())
214 | writer.write('\n'.toInt())
215 | return 1
216 | }
217 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/testapp/src/androidTest/java/nl/adaptivity/android/test/TestActivity1Test.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.test
2 |
3 |
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.Espresso.pressBack
6 | import androidx.test.espresso.action.ViewActions.*
7 | import androidx.test.espresso.assertion.ViewAssertions.matches
8 | import androidx.test.espresso.matcher.ViewMatchers.*
9 | import android.view.View
10 | import androidx.test.filters.LargeTest
11 | import androidx.test.rule.ActivityTestRule
12 | import androidx.test.runner.AndroidJUnit4
13 | import com.esotericsoftware.kryo.io.Input
14 | import com.esotericsoftware.kryo.io.Output
15 | import junit.framework.Assert.assertEquals
16 | import nl.adaptivity.android.kryo.LineOutput
17 | import nl.adaptivity.android.kryo.kryoAndroid
18 | import org.hamcrest.Matchers.allOf
19 | import org.junit.Before
20 | import org.junit.Rule
21 | import org.junit.Test
22 | import org.junit.runner.RunWith
23 | import java.io.ByteArrayOutputStream
24 | import com.esotericsoftware.minlog.Log as KryoLog
25 |
26 | @LargeTest
27 | @RunWith(AndroidJUnit4::class)
28 | class TestActivity1Test {
29 |
30 | @Before
31 | fun setLogging() {
32 | KryoLog.set(KryoLog.LEVEL_TRACE)
33 | }
34 |
35 | @Rule
36 | @JvmField
37 | val activity1TestRule = ActivityTestRule(TestActivity1::class.java, false, false)
38 |
39 | @Rule
40 | @JvmField
41 | val activity3TestRule = ActivityTestRule(TestActivity3::class.java, false, false)
42 |
43 | @Rule
44 | @JvmField
45 | val activity4TestRule = ActivityTestRule(TestActivity4::class.java, false, false)
46 |
47 | @Rule
48 | @JvmField
49 | val activity5TestRule = ActivityTestRule(TestActivity5::class.java, false, false)
50 |
51 | @Rule
52 | @JvmField
53 | val activity6TestRule = ActivityTestRule(TestActivity6::class.java, false, false)
54 |
55 | @Rule
56 | @JvmField
57 | val activity7TestRule = ActivityTestRule(TestActivity7::class.java, false, false)
58 |
59 | @Test
60 | @Throws(Throwable::class)
61 | fun testActivity1Test1() {
62 | textActivity(activity1TestRule)
63 |
64 | }
65 |
66 | @Test
67 | @Throws(Throwable::class)
68 | fun testActivity3Test1() {
69 | textActivity(activity3TestRule)
70 | }
71 |
72 | @Test
73 | @Throws(Throwable::class)
74 | fun testActivity4Test1() {
75 | textActivity(activity4TestRule)
76 | }
77 |
78 | @Test
79 | @Throws(Throwable::class)
80 | fun testActivity5Test1() {
81 | textActivity(activity5TestRule)
82 | }
83 |
84 | @Test
85 | @Throws(Throwable::class)
86 | fun testActivity6Test1() {
87 | textActivity(activity6TestRule)
88 | }
89 |
90 | @Test
91 | @Throws(Throwable::class)
92 | fun testActivity7Test1() {
93 | textActivity(activity7TestRule)
94 | }
95 |
96 | @Test
97 | fun testActivity7TestSerializeFragment() {
98 | activity7TestRule.launchActivity(null)
99 | activity7TestRule.runOnUiThread {
100 | val frag7 = activity7TestRule.activity.fragmentManager.findFragmentByTag("frag7outer")
101 | val baos = ByteArrayOutputStream()
102 | LineOutput(baos).use { lineOutput ->
103 | val kryo = kryoAndroid(activity7TestRule.activity)
104 | kryo.writeObject(lineOutput, frag7)
105 | }
106 | /*
107 | * 1 -> not null frag7
108 | * 1 -> not null FRAGMENTBYID
109 | * 8 -> FRAGMENTBYID enum ordinal
110 | * 1 -> writeName (not null?)
111 | * 0 -> nameId
112 | * ....... -> Name
113 | * 0x7f..... -> fragment id
114 | */
115 | val expected="1\n1\n8\n1\n0\nnl.adaptivity.android.test.TestFragment7\n${frag7.id}\n"
116 | assertEquals(expected, baos.toString("UTF8"))
117 |
118 | baos.reset()
119 | val kryo = kryoAndroid(activity7TestRule.activity)
120 | Output(baos).use { out ->
121 | kryo.writeObject(out, frag7)
122 | }
123 | val frag7cpy = kryo.readObject(Input(baos.toByteArray()), TestFragment7::class.java)
124 | assertEquals(frag7, frag7cpy)
125 | }
126 | }
127 |
128 | @Test
129 | @Throws(Throwable::class)
130 | fun testActivity1Test2() {
131 | KryoLog.set(KryoLog.LEVEL_TRACE)
132 |
133 | activity1TestRule.launchActivity(null)
134 | run {
135 | // Activity 1
136 | launchActivity2()
137 | }
138 | activity1TestRule.runOnUiThread { activity1TestRule.activity.recreate() }
139 |
140 | run {
141 | // Activity 2
142 | activity2EnterText()
143 |
144 | pressBack()
145 | }
146 |
147 | run {
148 | checkRestored()
149 |
150 | val textView2 = onView(allOf(withId(R.id.textView), isDisplayed()))
151 | textView2.check(matches(withText("Cancelled")))
152 | }
153 |
154 | }
155 |
156 | private fun textActivity(testRule: ActivityTestRule<*>) {
157 | testRule.launchActivity(null)
158 | run {
159 | // Activity 1
160 | launchActivity2()
161 | }
162 |
163 | testRule.runOnUiThread { testRule.activity.recreate() }
164 |
165 | run {
166 | // Activity 2
167 | activity2EnterText()
168 |
169 | val button = onView(allOf(withId(R.id.button2), withText("Submit"), isDisplayed()))
170 | button.perform(click())
171 |
172 | }
173 |
174 | run {
175 |
176 | checkRestored()
177 |
178 | val textView2 = onView(allOf(withId(R.id.textView), isDisplayed()))
179 | textView2.check(matches(withText("ghgh")))
180 | }
181 | }
182 |
183 | private fun checkRestored() {
184 | val restoredView = onView(withId(R.id.restoredView))
185 | restoredView.check(matches(allOf(withText("Restored"), isDisplayed())))
186 | }
187 |
188 | private fun activity2EnterText() {
189 | val editText = onView(withId(R.id.textView2))
190 | editText.check(matches(allOf(withHint("Provide some text here"), isDisplayed())))
191 | editText.perform(replaceText("ghgh"), closeSoftKeyboard())
192 |
193 | editText.check(matches(allOf(withText("ghgh"), isDisplayed())))
194 | }
195 |
196 | private fun launchActivity2() {
197 | val textView = onView(withId(R.id.textView))
198 | textView.check(matches(allOf(withText("TextView"), isDisplayed())))
199 |
200 | val restoredView = onView(withId(R.id.restoredView))
201 | restoredView.check(matches(withText("")))
202 |
203 | val button = onView(allOf(withId(R.id.button), withText("Button"), isDisplayed()))
204 | button.perform(click())
205 | }
206 |
207 | }
208 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/CoroutineActivity.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.support.annotation.RequiresApi
8 | import kotlinx.coroutines.*
9 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
10 | import nl.adaptivity.android.util.GrantResult
11 | import kotlin.coroutines.CoroutineContext
12 | import kotlin.coroutines.suspendCoroutine
13 | import kotlin.coroutines.resume
14 |
15 | /**
16 | * Extension method for activity that invokes [Activity.startActivityForResult] and invokes the
17 | * callback in [body] when complete. For use in Kotlin consider [activityResult] as a suspending
18 | * function instead.
19 | *
20 | * @receiver The activity that is extended.
21 | * @param intent The intent to invoke.
22 | * @param body The callback invoked on completion.
23 | */
24 | fun A.withActivityResult(intent: Intent, body: A.(ActivityResult)->Unit) {
25 | // Horrible hack to fix generics
26 | @Suppress("UNCHECKED_CAST")
27 | val contFragment = RetainedContinuationFragment(ParcelableContinuation(body, COROUTINEFRAGMENT_RESULTCODE_START))
28 | fragmentManager.beginTransaction().add(contFragment, RetainedContinuationFragment.TAG).commit()
29 | runOnUiThread {
30 | fragmentManager.executePendingTransactions()
31 | contFragment.startActivityForResult(intent, COROUTINEFRAGMENT_RESULTCODE_START)
32 | }
33 | }
34 |
35 | /**
36 | * Asynchronously invoke [Activity.startActivityForResult] returning the result on completion.
37 | */
38 | suspend fun Activity.activityResult(intent:Intent): ActivityResult {
39 | return suspendCoroutine { continuation ->
40 | val contFragment = ensureRetainingFragment()
41 | contFragment.addContinuation(ParcelableContinuation(continuation, this, COROUTINEFRAGMENT_RESULTCODE_START))
42 |
43 | runOnUiThread {
44 | contFragment.startActivityForResult(intent, COROUTINEFRAGMENT_RESULTCODE_START)
45 | }
46 | }
47 | }
48 |
49 | /**
50 | * Suspending function to request permissions. To avoid a dependency on the support library it
51 | * reimplements the fallback behaviour for pre-marshmallow devices. On later devices the coroutine
52 | * will be retained across activity restarts.
53 | *
54 | * @receiver The activity to use for the request.
55 | * @param The permissions to request
56 | * @return Either null when the request was cancelled by the user or a data class wrapping the parameters of [Activity.onRequestPermissionsResult]
57 | */
58 | suspend fun Activity.requestPermissions(permissions: Array): GrantResult? {
59 | return suspendCancellableCoroutine { continuation ->
60 |
61 | runOnUiThread {
62 | if (Build.VERSION.SDK_INT < 23) {
63 | // TODO consider whether this should be "retained". Probably not.
64 | val pm = packageManager
65 | val packageName = packageName
66 | val grantResults = IntArray(permissions.size) { idx -> pm.checkPermission(permissions[idx], packageName) }
67 | continuation.resume(GrantResult(permissions, grantResults))
68 | } else {
69 | val fragment = RequestPermissionContinuationFragment(ParcelableContinuation(continuation, this, COROUTINEFRAGMENT_RESULTCODE_START))
70 | val fm = fragmentManager
71 | fm.beginTransaction().apply {
72 | fm.findFragmentByTag(RequestPermissionContinuationFragment.TAG)?.let { remove(it) }
73 | add(fragment, RequestPermissionContinuationFragment.TAG)
74 | }.commit()
75 |
76 | fm.executePendingTransactions()
77 | fragment.requestPermissions(permissions, COROUTINEFRAGMENT_RESULTCODE_START)
78 | }
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * Extension method for activity that invokes [Activity.startActivityForResult] and invokes the
85 | * callback in [body] when complete. For use in Kotlin consider [activityResult] as a suspending
86 | * function instead.
87 | *
88 | * @receiver The activity that is extended.
89 | * @param intent The intent to invoke.
90 | * @param options The options to pass on the activity start.
91 | * @param body The callback invoked on completion.
92 | */
93 | @Suppress("unused")
94 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
95 | fun A.withActivityResult(intent: Intent, options: Bundle?, body: SerializableHandler) {
96 | // Horrible hack to fix generics
97 | @Suppress("UNCHECKED_CAST")
98 | val contFragment = RetainedContinuationFragment(ParcelableContinuation(body, COROUTINEFRAGMENT_RESULTCODE_START))
99 | fragmentManager.beginTransaction().add(contFragment, RetainedContinuationFragment.TAG).commit()
100 | runOnUiThread {
101 | fragmentManager.executePendingTransactions()
102 | contFragment.startActivityForResult(intent, COROUTINEFRAGMENT_RESULTCODE_START, options)
103 | }
104 | }
105 |
106 | /**
107 | * The starting (and for now only) result code that is used to start the activity. As it happens
108 | * from a special fragment the result code is actually ignored and should be safe from conflict.
109 | */
110 | const val COROUTINEFRAGMENT_RESULTCODE_START = 0xf00
111 |
112 | /**
113 | * The [Bundle] key under which the continuation is stored.
114 | */
115 | const val KEY_ACTIVITY_CONTINUATIONS_STATE = "parcelableContinuations"
116 |
117 | /**
118 | * Java compatibility interface to make the asynchronous use of [withActivityResult] with a callback
119 | * much friendlier.
120 | */
121 | interface SerializableHandler {
122 | operator fun invoke(activty: A, data:T)
123 | }
124 |
125 | typealias ActivityResult = Maybe
126 |
127 | open class CoroutineActivity>: Activity(), AndroidContextCoroutineScope> {
128 |
129 | @Suppress("UNCHECKED_CAST")
130 | override fun getAndroidContext(): A {
131 | return this as A
132 | }
133 |
134 | override val coroutineContext: CoroutineContext =
135 | Job() + Dispatchers.Default + AndroidContext(this)
136 |
137 | override fun createScopeWrapper(parentScope: CoroutineScope): ActivityCoroutineScopeWrapper {
138 | return ActivityCoroutineScopeWrapper(parentScope)
139 | }
140 |
141 | override fun onDestroy() {
142 | super.onDestroy()
143 | coroutineContext.cancel()
144 | }
145 |
146 |
147 | /*
148 |
149 | inline fun retainingCoroutineScope(body: RetainingCoroutineScope.() -> R):R {
150 | return retainingScope().body()
151 | }
152 | */
153 |
154 | }
155 |
156 | /*
157 | fun Activity.retainingCoroutineScope(body: RetainingCoroutineScope.() ->R):R {
158 | val fragment = ensureRetainingFragment()
159 | return fragment.body()
160 | return retainingScope().body
161 | }
162 |
163 |
164 |
165 | interface RetainingCoroutineContext: CoroutineContext
166 |
167 | interface RetainingCoroutineScope: CoroutineScope {
168 | val retainingContext: RetainingCoroutineContext
169 | }
170 | */
171 |
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/ParcelableContinuation.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.os.Parcel
6 | import android.os.Parcelable
7 | import android.util.Log
8 | import com.esotericsoftware.kryo.io.Input
9 | import com.esotericsoftware.kryo.io.Output
10 | import kotlinx.coroutines.CancellableContinuation
11 | import nl.adaptivity.android.coroutines.contexts.AndroidContext
12 | import nl.adaptivity.android.kryo.kryoAndroid
13 | import nl.adaptivity.android.kryo.writeKryoObject
14 | import java.io.ByteArrayOutputStream
15 | import kotlin.coroutines.Continuation
16 | import kotlin.coroutines.resume
17 | import kotlin.coroutines.resumeWithException
18 |
19 | /**
20 | * This class is part of the magic of serializing continuations in Android. This class only works
21 | * with continuations, but [ParcelableContinuationCompat] extends it to work with callback lambda's
22 | * as well (for regular async code).
23 | *
24 | * While the code will serialize from the actual continuation, the deserialization will happen
25 | * in stages. This is required to support capture of android [Context] values in a sensible way
26 | * (actually serializing them is invalid).
27 | *
28 | * @property requestCode When started with [Activity.startActivityForResult] this is the request code that may be
29 | * used to match the continuation with it's start point. Currently ignored.
30 | * @property continuation The actual continuation that is stored/wrapped.
31 | */
32 | open class ParcelableContinuation protected constructor(val requestCode: Int, protected var continuation: Any, private var attachedContext: Context? = null): Parcelable {
33 |
34 | /**
35 | * Create a new instance for the given handler.
36 | */
37 | constructor(handler: Continuation, attachedContext: Context?, requestCode: Int = -1): this(requestCode, handler, attachedContext)
38 |
39 |
40 | /**
41 | * Read the continuation from the parcel. This will merely store the continuation data as a byte array for
42 | * Kryo to deserialize later. (Note that the parcel cannot be validly stored).
43 | */
44 | @Suppress("UNCHECKED_CAST")
45 | constructor(parcel: Parcel) :
46 | this(parcel.readInt(), continuation = ByteArray(parcel.readInt()).also {
47 | parcel.readByteArray(it) } ) {
48 | Log.d(TAG, "Read continuation from parcel")
49 | }
50 |
51 | /**
52 | * Write the continuation (and requestCode) to a parcel for safe storage. This will handle the
53 | * case that the actual kryo data was still not deserialized and merely write it back to the new
54 | * parcel.
55 | */
56 | override fun writeToParcel(dest: Parcel, flags: Int) {
57 | Log.d(TAG, "Writing continuation to parcel")
58 | dest.writeInt(requestCode)
59 | val h = continuation
60 | if (h is ByteArray) {
61 | try {
62 | dest.writeInt(h.size)
63 | dest.writeByteArray(h)
64 | } catch (e: Exception) {
65 | Log.e(TAG, "Error writing bytearray of previous continuation: ${e.message}", e)
66 | throw e
67 | }
68 | } else {
69 | try {
70 | dest.writeKryoObject(h, kryoAndroid(attachedContext))
71 | } catch (e: Exception) {
72 | Log.e(TAG, "Error writing continuation: ${e.message}", e)
73 | throw e
74 | }
75 | }
76 | }
77 |
78 | /**
79 | * Cancel the continuation. This wraps [CancellableContinuation.cancel] but also reflates (when
80 | * needed) the continuation given the context. If the continuation is not a [CancellableContinuation]
81 | * this code will invoke [Continuation.resume]`(null)`.
82 | *
83 | * @param context The context to use for reflation. If not parcelled this is ignored.
84 | * @param cause The cause of the cancellation.
85 | * @see CancellableContinuation.cancel
86 | */
87 | @JvmOverloads
88 | open fun cancel(context: Context, cause: Throwable?=null) {
89 | val continuation = resolve(context)
90 | if (continuation is CancellableContinuation<*>) {
91 | continuation.cancel(cause)
92 | } else {
93 | @Suppress("UNCHECKED_CAST")
94 | (continuation as Continuation).resume(null)
95 | }
96 | }
97 |
98 | /**
99 | * Resume the continuation while also using the context to reflate if needed.
100 | *
101 | * @param context The context to use for reflation. If not parcelled this is ignored.
102 | * @param value The result value to use for resumption.
103 | * @see Continuation.resume
104 | *
105 | */
106 | open fun resume(context: Context, value: T) {
107 | resolve(context).resume(value)
108 | }
109 |
110 | /**
111 | * Resume the continuation with an exception while also using the context to reflate if needed.
112 |
113 | * @param context The context to use for reflation. If not parcelled this is ignored.
114 | * @param exception The cause of the failure.
115 | * @see Continuation.resume
116 | */
117 | open fun resumeWithException(context: Context, exception: Throwable) {
118 | resolve(context).resumeWithException(exception)
119 | }
120 |
121 | /**
122 | * Helper function that does the deserialization.
123 | */
124 | private fun resolve(context: Context): Continuation {
125 | if (attachedContext!=context) attachContext2(context)
126 |
127 | val h = continuation
128 |
129 | val continuation = when (h) {
130 | is ByteArray -> (kryoAndroid(context).readClassAndObject(Input(h)) as Continuation).also { continuation = it }
131 | else -> h as Continuation
132 | }
133 |
134 | when (context) {
135 | is Activity -> (continuation.context[AndroidContext] as AndroidContext?)?.run { androidContext = context }
136 | }
137 |
138 |
139 | @Suppress("UNCHECKED_CAST")
140 | return continuation
141 | }
142 |
143 | override fun describeContents() = 0
144 |
145 | fun attachContext2(context: Context?) {
146 | val attachedContext = this.attachedContext
147 | when(attachedContext) {
148 | null -> this.attachedContext = context
149 |
150 | context -> Unit // do nothing
151 |
152 | else -> {
153 | if (continuation is Continuation<*>) {
154 | val baos = ByteArrayOutputStream()
155 | Output(baos).use { out -> kryoAndroid(attachedContext).writeClassAndObject(out, continuation) }
156 |
157 | continuation = baos.toByteArray()
158 | }
159 | this.attachedContext = null
160 | }
161 | }
162 | }
163 |
164 | fun detachContext() {
165 | val attachedContext = this.attachedContext
166 | if (continuation is Continuation<*>) {
167 | val baos = ByteArrayOutputStream()
168 | Output(baos).use { out -> kryoAndroid(attachedContext).writeClassAndObject(out, continuation) }
169 |
170 | continuation = baos.toByteArray()
171 | }
172 | this.attachedContext = null
173 | }
174 |
175 | /**
176 | * Helper for [Parcelable]
177 | * @see [Parcelable.Creator]
178 | */
179 | companion object CREATOR : Parcelable.Creator> {
180 | override fun createFromParcel(parcel: Parcel): ParcelableContinuation {
181 | return ParcelableContinuation(parcel)
182 | }
183 |
184 | override fun newArray(size: Int): Array?> {
185 | return arrayOfNulls(size)
186 | }
187 |
188 | @JvmStatic
189 | private val TAG = ParcelableContinuation::class.java.simpleName
190 | }
191 | }
--------------------------------------------------------------------------------
/core/src/main/java/nl/adaptivity/android/coroutines/DownloadFragment.kt:
--------------------------------------------------------------------------------
1 | package nl.adaptivity.android.coroutines
2 |
3 | import android.app.Activity
4 | import android.app.DownloadManager
5 | import android.app.Fragment
6 | import android.content.BroadcastReceiver
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.IntentFilter
10 | import android.database.Cursor
11 | import android.net.Uri
12 | import android.os.Bundle
13 | import android.widget.Toast
14 | import kotlinx.coroutines.CancellationException
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.suspendCancellableCoroutine
18 | import java.io.File
19 | import java.net.URI
20 | import kotlin.coroutines.Continuation
21 |
22 | /**
23 | * Fragment that encapsulates the state of downloading a file.
24 | *
25 | * TODO Actually handle the case where download completed when the activity is in the background.
26 | */
27 | class DownloadFragment() : Fragment() {
28 | var downloadReference = -1L
29 | private var continuation: ParcelableContinuation? = null
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | arguments?.let { continuation = it.getParcelable(KEY_CONTINUATION) }
34 | savedInstanceState?.apply { downloadReference = getLong(KEY_DOWNLOAD_REFERENCE, -1L) }
35 | }
36 |
37 | override fun onSaveInstanceState(outState: Bundle) {
38 | super.onSaveInstanceState(outState)
39 | outState.putLong(KEY_DOWNLOAD_REFERENCE, downloadReference)
40 | }
41 |
42 | private val broadcastReceiver = object : BroadcastReceiver() {
43 | override fun onReceive(context: Context, intent: Intent) {
44 | if (intent.isActionDownloadComplete) {
45 | if (intent.downloadId == downloadReference) {
46 | context.unregisterReceiver(this)
47 | val downloadManager =
48 | context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
49 | val query = DownloadManager.Query()
50 | query.setFilterById(downloadReference)
51 | downloadManager.query(query).use { data ->
52 | val cont = continuation
53 | if (data.moveToNext()) {
54 | val status = data.getInt(DownloadManager.COLUMN_STATUS)
55 | if (status == DownloadManager.STATUS_SUCCESSFUL) {
56 | cont?.resume(context, data.getUri(DownloadManager.COLUMN_LOCAL_URI))
57 | continuation = null
58 | } else if (status == DownloadManager.STATUS_FAILED) {
59 | cont?.cancel(context)
60 | continuation = null
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 |
70 | private fun doDownload(
71 | activity: Activity,
72 | downloadUri: Uri,
73 | fileName: String,
74 | description: String = fileName,
75 | title: String = fileName
76 | ) {
77 | val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
78 | if (downloadReference >= 0) {
79 | val query = DownloadManager.Query()
80 | query.setFilterById(downloadReference)
81 | val data = downloadManager.query(query)
82 | if (data.moveToNext()) {
83 | val status = data.getInt(data.getColumnIndex(DownloadManager.COLUMN_STATUS))
84 | if (status == DownloadManager.STATUS_FAILED) {
85 | downloadReference = -1
86 | } else {// do something better
87 | Toast.makeText(activity, "Download already in progress", Toast.LENGTH_SHORT)
88 | .show()
89 | }
90 |
91 | } else {
92 | downloadReference = -1
93 | }
94 | }
95 | val request = DownloadManager.Request(downloadUri).apply {
96 | setDescription(description)
97 | setTitle(title)
98 | }
99 | val cacheDir = activity.externalCacheDir
100 | val downloadFile = File(cacheDir, fileName)
101 | if (downloadFile.exists()) {
102 | downloadFile.delete()
103 | }
104 |
105 | request.setDestinationUri(Uri.fromFile(downloadFile))
106 | downloadReference = downloadManager.enqueue(request)
107 | activity.registerReceiver(
108 | broadcastReceiver,
109 | IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
110 | )
111 | }
112 |
113 | companion object {
114 | private const val KEY_DOWNLOAD_REFERENCE = "DOWNLOAD_REFERENCE"
115 | private const val KEY_CONTINUATION = "_CONTINUATION_"
116 | private var fragNo = 0
117 |
118 | /**
119 | * Create a new instance of the fragment with the given continuation as parameter.
120 | */
121 | @Deprecated(
122 | "This should be private. Use download directly instead",
123 | level = DeprecationLevel.WARNING
124 | )
125 | fun newInstance(continuation: Continuation): DownloadFragment {
126 | return DownloadFragment().apply {
127 | arguments = Bundle(1).apply {
128 | putParcelable(
129 | KEY_CONTINUATION,
130 | ParcelableContinuation(continuation, activity)
131 | )
132 | }
133 | }
134 | }
135 |
136 | /**
137 | * Download the resource at [downloadUri] and return a URI of the local location
138 | */
139 | suspend fun download(activity: Activity, downloadUri: Uri): URI {
140 | return suspendCancellableCoroutine { cont ->
141 | @Suppress("DEPRECATION")
142 | val frag = newInstance(cont)
143 | activity.fragmentManager.beginTransaction().add(frag, nextTag()).commit()
144 | activity.runOnUiThread {
145 | activity.fragmentManager.executePendingTransactions()
146 | frag.doDownload(activity, downloadUri, fileName = "darwin-auth.apk")
147 | }
148 | }
149 | }
150 |
151 | /**
152 | * Async version of [download] that has a callback instead of being a suspend function.
153 | */
154 | @JvmStatic
155 | fun CoroutineScope.download(activity: Activity, downloadUri: Uri, callback: (Maybe) -> Unit) {
156 | launch {
157 | try {
158 | download(activity, downloadUri).also { callback(Maybe.Ok(it)) }
159 | } catch (e: CancellationException) {
160 | callback(Maybe.cancelled())
161 | } catch (e: Exception) {
162 | callback(Maybe.error(e))
163 | }
164 | }
165 | }
166 |
167 | private fun nextTag(): String? {
168 | fragNo++
169 | return "__DOWNLOAD_FRAGMENT_$fragNo"
170 | }
171 | }
172 |
173 | }
174 |
175 | /* Helper function to get an integer by name from a cursor. */
176 | private fun Cursor.getInt(columnName: String) = getInt(getColumnIndex(columnName))
177 |
178 | /* Helper function to get a string by name from a cursor. */
179 | private fun Cursor.getString(columnName: String) = getString(getColumnIndex(columnName))
180 |
181 | /* Helper function to get an uri by name from a cursor. */
182 | private fun Cursor.getUri(columnName: String) = URI.create(getString(getColumnIndex(columnName)))
183 |
184 | private inline var Intent.downloadId: Long
185 | get() = getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
186 | set(value) {
187 | putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, value)
188 | }
189 |
190 | private inline val Intent.isActionDownloadComplete get() = action == DownloadManager.ACTION_DOWNLOAD_COMPLETE
191 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------