├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── appcompat ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nl │ └── adaptivity │ └── android │ └── coroutinesCompat │ ├── AppcompatCoroutineFragment.kt │ ├── AppcompatFragmentContext.kt │ ├── AppcompatFragmentCoroutineScope.kt │ ├── AppcompatFragmentCoroutineScopeWrapper.kt │ ├── AppcompatLaunchers.kt │ └── CompatCoroutineActivity.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ ├── MavenDepUtils.kt │ ├── globals.kt │ ├── libraries │ └── Libraries.kt │ └── versions │ └── Versions.kt ├── core ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nl │ └── adaptivity │ └── android │ ├── coroutines │ ├── ActivityCoroutineScopeWrapper.kt │ ├── AndroidContextCoroutineScope.kt │ ├── BaseRetainedContinuationFragment.kt │ ├── CoroutineActivity.kt │ ├── CoroutineFragment.kt │ ├── DownloadFragment.kt │ ├── FragmentCoroutineScope.kt │ ├── FragmentCoroutineScopeWrapper.kt │ ├── Maybe.kt │ ├── ParcelableContinuation.kt │ ├── ParcelableContinuationCompat.kt │ ├── RequestPermissionContinuationFragment.kt │ ├── RetainedContinuationFragment.kt │ ├── SimpleContextCoroutineScopeWrapper.kt │ ├── SuspendableDialog.kt │ ├── WrappedContextCoroutineScope.kt │ ├── accountmanager.kt │ ├── contexts │ │ ├── AndroidContext.kt │ │ └── FragmentContext.kt │ ├── impl │ │ └── DelegateLayoutContainer.kt │ └── launchers.kt │ ├── kryo │ ├── AndroidKotlinResolver.kt │ ├── KotlinObjectInstantiatorStrategy.kt │ ├── KryoIO.kt │ ├── KryoParcelable.kt │ ├── ParcelInput.kt │ ├── ParcelOutput.kt │ └── serializers │ │ ├── ContextSerializer.kt │ │ ├── ContinuationImplSerializer.kt │ │ ├── CoroutineImplSerializer.kt │ │ ├── FragmentSerializer.kt │ │ ├── InitialResultSerializer.kt │ │ ├── KryoAndroidConstants.kt │ │ ├── ObjectSerializer.kt │ │ ├── PseudoObjectSerializer.kt │ │ ├── ReferenceSerializer.kt │ │ ├── SafeContinuationSerializer.kt │ │ ├── StandaloneCoroutineSerializer.kt │ │ └── SupportFragmentSerializer.kt │ └── util │ └── GrantResult.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-project.txt ├── settings.gradle.kts └── testapp ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src ├── androidTest └── java │ └── nl │ └── adaptivity │ └── android │ └── test │ ├── PlainCoroutineTestAndroid.kt │ └── TestActivity1Test.kt ├── main ├── AndroidManifest.xml ├── java │ └── nl │ │ └── adaptivity │ │ └── android │ │ ├── kryo │ │ └── LineOutput.kt │ │ └── test │ │ ├── TestActivity1.kt │ │ ├── TestActivity2.kt │ │ ├── TestActivity3.kt │ │ ├── TestActivity4.kt │ │ ├── TestActivity5.kt │ │ ├── TestActivity6.kt │ │ └── TestActivity7.kt └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_test1.xml │ ├── activity_test2.xml │ ├── activity_test6.xml │ ├── activity_test7.xml │ └── fragment_test6.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── 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 └── test └── java └── uk └── ac └── bmth └── aprog └── testapp └── PlainCoroutineTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | local.properties 2 | /.gradle 3 | /build 4 | /gen 5 | /bin 6 | *.iml 7 | /.idea/* 8 | /wiki 9 | /pages 10 | /testapp/build 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](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 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /appcompat/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /appcompat/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016. 3 | * 4 | * This file is part of ProcessManager. 5 | * 6 | * ProcessManager is free software: you can redistribute it and/or modify it under the terms of version 2.1 of the 7 | * GNU Lesser General Public License as published by the Free Software Foundation. 8 | * 9 | * ProcessManager is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even 10 | * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with Foobar. If not, 14 | * see . 15 | */ 16 | 17 | plugins { 18 | id("com.android.library") apply false 19 | 20 | kotlin("android") apply false 21 | } 22 | 23 | tasks.named("wrapper") { 24 | gradleVersion = "7.1" 25 | } 26 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/MavenDepUtils.kt: -------------------------------------------------------------------------------- 1 | import groovy.util.Node 2 | import org.gradle.api.XmlProvider 3 | 4 | inline fun XmlProvider.dependencies(config: Node.() -> Unit): Unit { 5 | asNode().dependencies(config) 6 | } 7 | 8 | inline fun Node.dependencies(config: Node.() -> Unit): Node { 9 | @Suppress("UNCHECKED_CAST") 10 | val ch = children() as List 11 | val node: Node = ch.firstOrNull { it.name() == "dependencies" } ?: appendNode("dependencies") 12 | return node.apply(config) 13 | } 14 | 15 | fun Node.dependency(spec: String, type: String = "jar", scope: String = "compile", optional: Boolean = false): Node { 16 | return spec.split(':', limit = 3).run { 17 | val groupId = get(0) 18 | val artifactId = get(1) 19 | val version = get(2) 20 | dependency(groupId, artifactId, version, type, scope, optional) 21 | } 22 | } 23 | 24 | fun Node.dependency(groupId: String, artifactId: String, version: String, type: String = "jar", scope: String = "compile", optional: Boolean = false): Node { 25 | return appendNode("dependency").apply { 26 | appendNode("groupId", groupId) 27 | appendNode("artifactId", artifactId) 28 | appendNode("version", version) 29 | appendNode("type", type) 30 | if (scope != "compile") appendNode("scope", scope) 31 | if (optional) appendNode("optional", "true") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/globals.kt: -------------------------------------------------------------------------------- 1 | import libraries.androidTestRulesSpec 2 | import libraries.androidTestRunnerSpec 3 | import libraries.espressoCoreSpec 4 | import org.gradle.api.Project 5 | import org.gradle.api.artifacts.Dependency 6 | import org.gradle.api.artifacts.dsl.DependencyHandler 7 | import org.gradle.kotlin.dsl.DependencyHandlerScope 8 | import org.gradle.kotlin.dsl.maven 9 | import org.gradle.kotlin.dsl.repositories 10 | 11 | private fun DependencyHandler.androidTestImplementation(dependencyNotation: Any): Dependency? = 12 | add("androidTestImplementation", dependencyNotation) 13 | 14 | 15 | fun DependencyHandlerScope.useEspresso(project: Project) { 16 | with(project) { 17 | androidTestImplementation("androidx.test.ext:junit:1.0.0") 18 | androidTestImplementation(androidTestRunnerSpec) 19 | androidTestImplementation(androidTestRulesSpec) 20 | androidTestImplementation(espressoCoreSpec) 21 | } 22 | } 23 | 24 | fun Project.projectRepositories() { 25 | repositories { 26 | mavenLocal() 27 | mavenCentral() 28 | google() 29 | maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/libraries/Libraries.kt: -------------------------------------------------------------------------------- 1 | package libraries 2 | 3 | import org.gradle.api.Project 4 | import versions.* 5 | 6 | val Project.supportLibSpec get() = "com.android.support:appcompat-v7:$androidCompatVersion" 7 | val Project.junitSpec get() = "junit:junit:$junitVersion" 8 | val Project.kryoSpec get() = "com.esotericsoftware:kryo:$kryoVersion" 9 | val Project.androidTestRunnerSpec get() = "androidx.test:runner:$androidTestSupportVersion" 10 | val Project.androidTestRulesSpec get() = "androidx.test:rules:$androidTestSupportVersion" 11 | val Project.espressoCoreSpec get() ="androidx.test.espresso:espresso-core:$espressoCoreVersion" 12 | val Project.constraintLayoutSpec get() = "com.android.support.constraint:constraint-layout:$constraintLayoutVersion" 13 | val Project.kotlinlibSpec get() = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 14 | val Project.kotlinlib8Spec get() = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 15 | val Project.androidExtensionRuntimeSpec get() = "org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlinVersion" 16 | val Project.coroutinesSpec get() = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" 17 | val Project.coroutinesAndroidSpec get() = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" 18 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/versions/Versions.kt: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import org.gradle.api.Project 4 | 5 | val Project.kotlinVersion: String get() = property("kotlinVersion") as String 6 | val Project.kryoVersion: String get() = property("kryoVersion") as String 7 | val Project.coroutinesVersion: String get() = property("coroutinesVersion") as String 8 | val Project.androidBuildToolsVersion: String get() = property("androidBuildToolsVersion") as String 9 | val Project.dokkaVersion: String get() = property("dokkaVersion") as String 10 | val Project.bintrayVersion: String get() = property("bintrayVersion") as String 11 | val Project.selfVersion: String get() = property("selfVersion") as String 12 | val Project.constraintLayoutVersion: String get() = property("constraintLayoutVersion") as String 13 | val Project.androidCompatVersion: String get() = property("androidCompatVersion") as String 14 | val Project.junitVersion: String get() = property("junitVersion") as String 15 | val Project.espressoCoreVersion: String get() = property("espressoCoreVersion") as String 16 | val Project.androidTestSupportVersion: String get() = property("androidTestSupportVersion") as String 17 | 18 | val Project.minSdk: Int get() = (property("minSdk") as String).toInt() 19 | val Project.targetSdk: Int get() = (property("targetSdk") as String).toInt() 20 | val Project.compileSdk: Int get() = (property("compileSdk") as String).toInt() 21 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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/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 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdvrieze/android-coroutines/3e380a07e4dd3412db391cb90fe06a8adb01f1a9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /testapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/res/layout/activity_test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 24 | 25 |