Skip to content
This repository has been archived by the owner on Oct 18, 2022. It is now read-only.

Commit

Permalink
#34: basic android implementation
Browse files Browse the repository at this point in the history
- storage adapter using external storage
- controller/model adapter for android
- base64 platform independent
- picture selector
- some refactoring
  • Loading branch information
bvolkmer committed Sep 26, 2018
1 parent 59c0218 commit 4fd93d2
Show file tree
Hide file tree
Showing 70 changed files with 617 additions and 253 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ dependencies {
exclude group: 'org.threeten', module: 'threetenbp'
}
implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5'
implementation 'com.couchbase.lite:couchbase-lite-android:1.4.0'
implementation 'com.couchbase.lite:couchbase-lite-android:1.4.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"
Expand Down
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.x4fyr.paiman">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.x4fyr.paiman.app

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.webkit.WebView
import de.x4fyr.paiman.app.service.AndroidWebViewService
Expand All @@ -12,11 +13,13 @@ import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch

/** Activity that only wraps a [WebView] and loads ui in it. */
class WebViewWrapperActivity: Activity() {
class WebViewWrapperActivity : Activity() {

private lateinit var webViewService: AndroidWebViewService
private lateinit var entryViewController: EntryController

private var resultHandlerMap: MutableMap<Any, ((requestCode: Int, resultCode: Int, data: Intent?) -> Unit)> = HashMap()


override fun onCreate(savedInstanceState: Bundle?) {
val component: MainComponent = DaggerMainComponent.builder()
Expand All @@ -30,4 +33,22 @@ class WebViewWrapperActivity: Activity() {
entryViewController.loadView()
}
}

/** Add a handler to be invoked on [Activity.onActivityResult] */
fun addActivityResultHandler(identifier: Any, handler: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit) {
resultHandlerMap.put(identifier, handler)
}

/** Remove a handler not to be invoked on [Activity.onActivityResult] */
fun removeActivityResultHandler(identifier: Any) {
resultHandlerMap.remove(identifier)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultHandlerMap.forEach { _, handler ->
handler.invoke(requestCode, resultCode, data)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.x4fyr.paiman.app.adapter

import android.util.Base64

/**
* Encoder using [android.util.Base64]
*/
class AndroidBase64Encoder : Base64Encoder() {
override fun encodeToString(byteArray: ByteArray): String = Base64.encodeToString(byteArray, Base64.DEFAULT)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,114 @@
package de.x4fyr.paiman.app.service

import android.app.Activity
import android.content.Context
import android.content.CursorLoader
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import android.provider.MediaStore
import android.util.Log
import de.x4fyr.paiman.app.WebViewWrapperActivity
import de.x4fyr.paiman.app.services.PictureSelectorService
import java.io.File
import java.io.InputStream

class AndroidPictureSelectorService: PictureSelectorService {
/** Open system dependent dialog to get a jpeg picture */
override fun pickPicture(onReturn: (InputStream?) -> Unit) {
TODO("not implemented")
/**
* Taken an modified from https://gist.github.com/Mariovc/f06e70ebe8ca52fbbbe2
*/
class AndroidPictureSelectorService(private var owningActivity: WebViewWrapperActivity) : PictureSelectorService {

companion object {
private const val TAG = "ImagePicker"
private const val TEMP_IMAGE_NAME = "tempImage"
private const val PICK_IMAGE_ID = 234 //the number doesn't matter
}

/** Action on return of the picker */
private var onReturn: ((stream: InputStream?) -> Unit)? = null


init {
owningActivity.addActivityResultHandler(this) { requestCode, resultCode, intent ->
if (requestCode == PICK_IMAGE_ID) {
val url = getUrlFromResult(owningActivity, resultCode, intent)
val stream = getInputStreamFromUrl(url)
onReturn?.invoke(stream)
}
}
}

override fun pickPicture(onReturn: (stream: InputStream?) -> Unit) {
owningActivity.startActivityForResult(getPickImageIntent(), PICK_IMAGE_ID)
this.onReturn = onReturn
}


private fun getPickImageIntent(): Intent? {
var chooserIntent: Intent? = null

var intentList: MutableList<Intent> = ArrayList()

val pickIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
val takePhotoIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
takePhotoIntent.putExtra("return-data", true)
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(getTempFile(owningActivity)))
intentList = addIntentsToList(owningActivity, intentList, pickIntent)
intentList = addIntentsToList(owningActivity, intentList, takePhotoIntent)

if (intentList.size > 0) {
chooserIntent = Intent.createChooser(intentList.removeAt(intentList.size - 1), "Pick Image")
chooserIntent!!.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentList.toTypedArray<Parcelable>())
}

return chooserIntent
}
}

private fun addIntentsToList(context: Context, list: MutableList<Intent>, intent: Intent): MutableList<Intent> {
val resInfo = context.packageManager.queryIntentActivities(intent, 0)
for (resolveInfo in resInfo) {
val packageName = resolveInfo.activityInfo.packageName
val targetedIntent = Intent(intent)
targetedIntent.`package` = packageName
list.add(targetedIntent)
Log.d(TAG, "Intent: " + intent.action + " package: " + packageName)
}
return list
}

private fun getTempFile(context: Context): File {
val imageFile = File(context.externalCacheDir ?: context.cacheDir, TEMP_IMAGE_NAME)
imageFile.parentFile.mkdirs()
return imageFile
}

private fun getUrlFromResult(context: Context, resultCode: Int, imageReturnedIntent: Intent?): String {
val imageFile: File = getTempFile(context)
var selectedImage: String? = null
if (resultCode == Activity.RESULT_OK) {
val intentDataString = imageReturnedIntent?.data?.toString()
if (intentDataString == null || intentDataString.contains(imageFile.toString())) {
/** CAMERA **/
selectedImage = imageFile.path
} else {
/** ALBUM **/
selectedImage = if (imageReturnedIntent.data.toString().contains("content:/")) {
val projection: Array<String> = Array(1) { MediaStore.Images.Media.DATA }
val cursor = CursorLoader(context, imageReturnedIntent.data, projection, null, null,
null).loadInBackground()
val idx = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
cursor.getString(idx)
} else {
imageReturnedIntent.data.path
}
}
}
return selectedImage ?: ""
}


private fun getInputStreamFromUrl(url: String): InputStream? = File(url)
.takeIf { it.exists() && it.canRead() }
?.inputStream()?.takeIf { it.available() > 0 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import android.util.Log
import android.webkit.WebView
import de.x4fyr.paiman.app.services.WebViewService
import de.x4fyr.paiman.app.ui.Controller
import de.x4fyr.paiman.app.ui.Model
import de.x4fyr.paiman.app.ui.produceString
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import org.w3c.dom.Element

/** Android implementations of [WebViewService] */
class AndroidWebViewService(context: Context): WebViewService {
class AndroidWebViewService(private val context: Context) : WebViewService {

/** WebView instance */
val webView: WebView = WebView(context)
Expand All @@ -34,17 +35,36 @@ class AndroidWebViewService(context: Context): WebViewService {
}
}

/** Set Controller for callbacks in ui ]
*
* Suppress JavascriptInterface because the interface itself never has annotated methods, but the implementations might have.
*/

/** Load html file into WebView */
override fun loadHtml(html: String, controller: Controller, model: Model?) {
launch(UI) {
webView.loadUrl(viewResourcePrefix + html)
setControllerAndModel(controller, model)
}
}

/** Set Controller for callbacks in ui */
@SuppressLint("JavascriptInterface")
override fun setCallbackController(controller: Controller) {
override fun setControllerAndModel(controller: Controller, model: Model?) {
launch(UI) {
webView.removeJavascriptInterface(WebViewService.javascriptControllerModuleName)
webView.removeJavascriptInterface(WebViewService.javascriptModelModuleName)
webView.addJavascriptInterface(controller, WebViewService.javascriptControllerModuleName)
if (model != null) webView.addJavascriptInterface(model, WebViewService.javascriptModelModuleName)
}
}

/** execute javascript in current document */
override fun executeJS(script: String) {
launch(UI) {
webView.evaluateJavascript(script) {}
}
}

/**
* Like [WebViewService.viewResourcePrefix] but specific for android
*/
private val viewResourcePrefix: String = "file:///android_asset" + WebViewService.viewResourcePrefix.removePrefix("/assets")

}
68 changes: 68 additions & 0 deletions android/src/main/java/de/x4fyr/paiman/app/ui/controller.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.x4fyr.paiman.app.ui

import android.webkit.JavascriptInterface
import de.x4fyr.paiman.app.services.PictureSelectorService
import de.x4fyr.paiman.app.services.WebViewService
import de.x4fyr.paiman.app.ui.views.entry.EntryController
import de.x4fyr.paiman.app.ui.views.overview.OverviewController
import de.x4fyr.paiman.app.ui.views.overview.OverviewModel
import de.x4fyr.paiman.app.ui.views.paintingDetail.PaintingDetailController
import de.x4fyr.paiman.app.ui.views.paintingDetail.PaintingDetailFactory
import de.x4fyr.paiman.app.ui.views.paintingDetail.PaintingDetailModel

class PaintingDetailControllerAndroidAdapter(
webViewService: WebViewService,
model: PaintingDetailModel,
returnController: Controller,
pictureSelectorService: PictureSelectorService
) : PaintingDetailController( webViewService, model, returnController, pictureSelectorService ) {

/** Callback: return to [returnController] */
@JavascriptInterface
override fun back() = super.back()

/** Callback: add tag */
@JavascriptInterface
override fun addTag(tag: String) = super.addTag(tag)

/** Callback: add wip */
@JavascriptInterface
override fun addWIP() = super.addWIP()

/** Callback: add ref */
@JavascriptInterface
override fun addRef() = super.addRef()

/** Callback: finishing */
@JavascriptInterface
override fun finishing(year: Int, month: Int) = super.finishing(year, month)
}

/** Adapter for [OverviewController] to make functions available as javascript interface */
class OverviewControllerAndroidAdapter(private val webViewService: WebViewService,
private val model: OverviewModel,
private val paintingDetailFactory: PaintingDetailFactory,
private val pictureSelectorService: PictureSelectorService)
: OverviewController(webViewService, model, paintingDetailFactory, pictureSelectorService) {

/** Callback: Refresh previews */
@JavascriptInterface
override fun refresh() = super.refresh()

/** Callback: Open detail view of given painting by [id] */
@JavascriptInterface
override fun openPainting(id: String) = super.openPainting(id)

@JavascriptInterface
override fun addPainting(title: String?) = super.addPainting(title)

@JavascriptInterface
override fun selectImage() = super.selectImage()
}

/** Adapter for [EntryController] to make functions available as javascript interface */
class EntryControllerAndroidAdapter(webViewService: WebViewService, mainViewController: OverviewController): EntryController(webViewService, mainViewController) {

@JavascriptInterface
override fun openNext() = super.openNext()
}

This file was deleted.

This file was deleted.

26 changes: 26 additions & 0 deletions android/src/main/java/de/x4fyr/paiman/app/ui/model.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.x4fyr.paiman.app.ui.model

import android.webkit.JavascriptInterface
import de.x4fyr.paiman.app.adapter.Base64Encoder
import de.x4fyr.paiman.app.ui.views.overview.OverviewModel
import de.x4fyr.paiman.app.ui.views.paintingDetail.PaintingDetailModel
import de.x4fyr.paiman.lib.services.PaintingService
import de.x4fyr.paiman.lib.services.QueryService

class OverviewModelAndroidAdapter(
paintingService: PaintingService,
queryService: QueryService,
base64Encoder: Base64Encoder
) : OverviewModel(paintingService, queryService, base64Encoder) {
@JavascriptInterface
override fun getPreviews(): String = super.getPreviews()
}

class PaintingDetailModelAndroidAdapter(
paintingService: PaintingService,
id: String,
base64Encoder: Base64Encoder
) : PaintingDetailModel(paintingService, id, base64Encoder) {
@JavascriptInterface
override fun getHolder(): String = super.getHolder()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import dagger.Module;
import de.x4fyr.paiman.dagger.android.ControllerModule;
import de.x4fyr.paiman.dagger.ui.FactoryModule;
import de.x4fyr.paiman.dagger.ui.ModelModule;
import de.x4fyr.paiman.dagger.android.FactoryModule;
import de.x4fyr.paiman.dagger.android.ModelModule;

@Module(includes = {ControllerModule.class, ModelModule.class, FactoryModule.class})
enum AndroidUIModule {
Expand Down
Loading

0 comments on commit 4fd93d2

Please sign in to comment.