diff --git a/.gitignore b/.gitignore index 4e566ec..6ffa34a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,5 +134,5 @@ crashlytics-build.properties fabric.properties /.idea/ /versioner.gradle -/app/data/ -/archive/ \ No newline at end of file +/archive/ +**/data \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..5ee0a96 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,68 @@ +def gradleParams = "-Psnapshot=false -Pbranch=${env.BRANCH_NAME}" +pipeline{ + agent any + stages { + stage('Build lib') { + agent any + steps { + sh "git fetch https://github.com/bvolkmer/PaiMan.git +refs/heads/master:refs/remotes/origin/master" + sh "./gradlew libpaiman:build $gradleParams" + } + } + stage('Test lib') { + agent any + steps { + sh "./gradlew libpaiman:check $gradleParams" + } + } + stage('Build client') { + agent {label "android-sdk"} + steps { + parallel javafx: { + sh "./gradlew app:distZip $gradleParams" + }, + android: { + sh "./gradlew android:assemble $gradleParams" + } + } + } + stage('Test clients') { + agent {label "android-emulator"} + steps { + parallel javafx: { + sh "./gradlew app:check $gradleParams" + }, + android: { + echo "Start emulators" + sh '$ANDROID_HOME/emulator/emulator @jenkins-paiman-19 -no-audio -no-window -wipe-data &' + sh '$ANDROID_HOME/emulator/emulator @jenkins-paiman-21 -no-audio -no-window -wipe-data &' + sh '$ANDROID_HOME/emulator/emulator @jenkins-paiman-24 -no-audio -no-window -wipe-data &' + echo "Wait for emulators" + waitUntil { + script { + try { + sh 'if [ `$ANDROID_HOME/platform-tools/adb devices | grep emulator | cut -f1 | wc -l` -ne 3 ]; then exit 1; fi' + return true + } catch (exception) { + return false + } + } + } + sh './android-wait-for-emulator.sh `$ANDROID_HOME/platform-tools/adb devices | grep emulator | cut -f1 `' + echo "RunCheck" + sh "./gradlew android:connectedCheck $gradleParams" + } + } + } + stage ("Deploy") { + agent { label "deploy" } + steps { + sh "mkdir -p archive/" + sh "rm -f archive/*" + sh "git fetch https://github.com/bvolkmer/PaiMan.git +refs/heads/master:refs/remotes/origin/master" + sh "./gradlew copyArtifacts $gradleParams" + sh "cp -f archive/* /srv/http/develop/downloads/PaiMan" + } + } + } +} \ No newline at end of file diff --git a/android-wait-for-emulator.sh b/android-wait-for-emulator.sh new file mode 100755 index 0000000..a91d9db --- /dev/null +++ b/android-wait-for-emulator.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Originally written by Ralf Kistner , but placed in the public domain + +set +e + +bootanim="" +failcounter=0 +timeout_in_sec=360 + +for serial in "$@" +do + +echo "Probing $serial" + +until [[ "$bootanim" =~ "stopped" ]]; do + bootanim=`$ANDROID_HOME/platform-tools/adb -s $serial -e shell getprop init.svc.bootanim 2>&1 &` + if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" + || "$bootanim" =~ "running" ]]; then + let "failcounter += 1" + echo "Waiting for emulator to start" + if [[ $failcounter -gt timeout_in_sec ]]; then + echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" + exit 1 + fi + fi + sleep 1 +done + +done + +echo "Emulators are ready" \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..4e1058d --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,84 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +version = parent.version + +android { + signingConfigs { + debug { + keyAlias 'debugkey' + keyPassword 'debugpass' + storeFile file('/home/x4fyr/Android/keystore/paiman.jks') + storePassword 'storepass' + } + } + compileSdkVersion 26 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "de.x4fyr.paiman" + minSdkVersion 19 + targetSdkVersion 26 + versionCode gitVersion.version * 10000 + gitVersion.branchVersion + versionName version + //versionCode 1 + //versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + multiDexEnabled true + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + signingConfig signingConfigs.debug + } + } + packagingOptions { + exclude 'META-INF/ASL2.0' + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + exclude 'META-INF/MANIFEST.MF' + } + compileOptions.incremental = false +} + +repositories { + jcenter() + mavenCentral() + maven { + url "https://maven.google.com" + } +} + +ext.anko_version = "0.10.0" + +dependencies { + compile 'com.android.support:support-v4:26.0.1' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:design:26.0.1' + compile 'com.android.support:gridlayout-v7:26.0.1' + compile 'com.android.support:cardview-v7:26.0.1' + compile("org.jetbrains.anko:anko:$anko_version") { + exclude group: 'com.google.android', module: 'android' + } + compile project(':libpaiman'), { + exclude group: 'org.threeten', module: 'threetenbp' + } + compile 'com.jakewharton.threetenabp:threetenabp:1.0.5' + compile 'com.couchbase.lite:couchbase-lite-android:1.4.0' + testCompile 'junit:junit:4.12' + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile 'com.google.android.gms:play-services-drive:11.2.0' + compile 'com.android.support:multidex:1.0.1' + compile 'com.google.dagger:dagger:2.11' + compile 'com.google.dagger:dagger-android-support:2.11' + kapt 'com.google.dagger:dagger-compiler:2.11' + provided 'org.glassfish:javax.annotation:10.0-b28' + kapt 'com.google.dagger:dagger-android-processor:2.11' +} diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000..f8cce0e --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,30 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/x4fyr/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class com.couchbase.** +-keep class com.google.android.gms.** + +-dontwarn \ No newline at end of file diff --git a/android/src/androidTest/java/de/x4fyr/paiman/ExampleInstrumentedTest.java b/android/src/androidTest/java/de/x4fyr/paiman/ExampleInstrumentedTest.java new file mode 100644 index 0000000..5c99d73 --- /dev/null +++ b/android/src/androidTest/java/de/x4fyr/paiman/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package de.x4fyr.paiman; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("de.x4fyr.paiman", appContext.getPackageName()); + } +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..462aa2a --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/java/de/x4fyr/paiman/Application.kt b/android/src/main/java/de/x4fyr/paiman/Application.kt new file mode 100644 index 0000000..02a46d8 --- /dev/null +++ b/android/src/main/java/de/x4fyr/paiman/Application.kt @@ -0,0 +1,26 @@ +package de.x4fyr.paiman + +import android.app.Activity +import android.support.multidex.MultiDexApplication +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import javax.inject.Inject + +/** + * A template for any Activity used in this app needed for dependency injection + */ +class Application: MultiDexApplication(), HasActivityInjector { + + /** Injected by dagger */ + @Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector + + /** Returns an [AndroidInjector] of [Activity]s. */ + override fun activityInjector(): AndroidInjector = dispatchingActivityInjector + + /** See [MultiDexApplication] complemented with dependency injection */ + override fun onCreate() { + super.onCreate() + DaggerAppComponent.builder().application(this).build().inject(this) + } +} \ No newline at end of file diff --git a/android/src/main/java/de/x4fyr/paiman/MainActivity.kt b/android/src/main/java/de/x4fyr/paiman/MainActivity.kt new file mode 100644 index 0000000..56a4cef --- /dev/null +++ b/android/src/main/java/de/x4fyr/paiman/MainActivity.kt @@ -0,0 +1,395 @@ +package de.x4fyr.paiman + +import android.app.Activity +import android.app.Dialog +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.app.DialogFragment +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.CardView +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.StaggeredGridLayoutManager +import android.support.v7.widget.Toolbar +import android.util.Log +import android.util.TypedValue +import android.view.* +import android.widget.ImageView +import android.widget.TextView +import dagger.android.AndroidInjection +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import de.x4fyr.paiman.app.ApplyingDialogFragment +import de.x4fyr.paiman.app.BaseActivity +import de.x4fyr.paiman.app.errorDialog +import de.x4fyr.paiman.app.getInputStreamFromUrl +import de.x4fyr.paiman.lib.provider.AndroidPictureProvider +import de.x4fyr.paiman.lib.provider.AndroidServiceProvider +import de.x4fyr.paiman.lib.provider.PictureProvider +import de.x4fyr.paiman.lib.services.DesignService +import de.x4fyr.paiman.lib.services.PaintingService +import de.x4fyr.paiman.lib.services.QueryService +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.content_main.* +import kotlinx.android.synthetic.main.fragment_add_painting.* +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.launch +import org.jetbrains.anko.displayMetrics +import org.jetbrains.anko.image +import org.jetbrains.anko.imageResource +import org.jetbrains.anko.support.v4.onRefresh +import java.util.concurrent.locks.ReentrantLock +import javax.inject.Inject +import kotlin.concurrent.withLock + +/** Main Activity of the app, which is launched first and shows a list of current paintings + * + * It initialises the different providers. + */ +class MainActivity: BaseActivity(), HasActivityInjector { + + /** [DispatchingAndroidInjector] for dependency injection */ + @Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector + + /** See [HasActivityInjector.activityInjector] */ + override fun activityInjector(): AndroidInjector = dispatchingActivityInjector + + /** Initial [AndroidServiceProvider] */ + @Inject lateinit var serviceProvider: AndroidServiceProvider + /** Initial [AndroidPictureProvider] */ + lateinit var pictureProvider: AndroidPictureProvider + /** [QueryService] instance*/ + @Inject lateinit var queryService: QueryService + /** [PaintingService] instance */ + @Inject lateinit var paintingService: PaintingService + /** [DesignService] instance */ + @Inject lateinit var designService: DesignService + + private var activeDialogFragment: ApplyingDialogFragment? = null + + private lateinit var gridAdapter: GridAdapter + private var models: MutableList = mutableListOf() + private var modelsLock = ReentrantLock() + private var selectedPositions: MutableSet = hashSetOf() + + private val actionModeCallback = object: ActionMode.Callback { + /** See [ActionMode.Callback] */ + override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean { + when { + menuItem.itemId == R.id.main_selection_delete -> { + launch(CommonPool) { + selectedPositions.forEach { paintingService.delete(models[it].id) } + launch(UI) { + mode.finish() + Snackbar.make(main_layout, R.string.main_delete_notification, Snackbar.LENGTH_SHORT).show() + reloadContent() + } + } + return true + } + } + return false + } + + /** See [ActionMode.Callback] */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean { + mode.menuInflater.inflate(R.menu.selection_menu_main, menu) + return true + } + + /** See [ActionMode.Callback] */ + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false + + /** See [ActionMode.Callback] */ + override fun onDestroyActionMode(mode: ActionMode?) { + selectedPositions.clear() + gridAdapter.notifyDataSetChanged() + actionMode = null + } + + + } + private var actionMode: ActionMode? = null + + /** Actions that happen on long click on a painting in the grid. + * + * Mainly selecting the item and enter/exit selection mode + */ + val onPaintingLongClick: (id: Int) -> Unit = { id -> + //if (actionMode != null) return + if (selectedPositions.contains(id)) + selectedPositions.remove(id) + else + selectedPositions.add(id) + gridAdapter.notifyDataSetChanged() + val selectedCount = selectedPositions.size + if (selectedCount > 0) { + if (actionMode == null) { + actionMode = startActionMode(actionModeCallback) + } + } else + actionMode?.finish() + actionMode?.title = getString(R.string.main_selection_title, selectedCount) + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setSupportActionBar(toolbar) + pictureProvider = AndroidPictureProvider(this) + gridAdapter = GridAdapter(models, selectedPositions, resources) + + paintingGrid.apply { + val spanCount = Math.floor( + displayMetrics.widthPixels/TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 106.toFloat(), + displayMetrics).toDouble()).toInt() + layoutManager = StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL) + adapter = gridAdapter + } + + fab.setOnClickListener { _ -> + val fragment: DialogFragment = AddPaintingFragment() + // if (designService.isLargeDevice) { + fragment.show(supportFragmentManager, "dialog") + /*} else { + supportFragmentManager.beginTransaction().apply { + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + add(android.R.id.content, fragment) + addToBackStack(null) + commit() + } + }*/ + } + // Register query listener + queryService.allPaintingsQuery.toLiveQuery().apply { + addChangeListener { + async(UI) { + swipeRefreshLayout.isRefreshing = true + val newModels = if (it.rows != null) { + paintingService.getFromQueryResult(it.rows!!).map { + Model(id = it.id, mainImage = async(CommonPool) { + designService.getOrLoadThumbnailBitmap(it.mainPicture) + }, title = it.title, mainImageId = it.mainPicture.id) + } + } else listOf() + modelsLock.withLock { + models.clear() + models.addAll(newModels) + } + gridAdapter.notifyDataSetChanged() + swipeRefreshLayout.isRefreshing = false + } + } + start() + } + swipeRefreshLayout.onRefresh { reloadContent(completionHandler = { swipeRefreshLayout.isRefreshing = false }) } + } + + override fun onResume() { + super.onResume() + reloadContent { } + } + + /** + * + */ + fun reloadContent(completionHandler: () -> Unit = {}) { + async(UI) { + val newModels = paintingService.getFromQueryResult(queryService.allPaintingsQuery.run()).map { + Model(id = it.id, title = it.title, mainImageId = it.mainPicture.id, + mainImage = async(CommonPool) { designService.getOrLoadThumbnailBitmap(it.mainPicture) }) + } + modelsLock.withLock { + models.clear() + models.addAll(newModels) + } + gridAdapter.notifyDataSetChanged() + completionHandler() + } + } + + /** See [AppCompatActivity] */ + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + /** Set the current active dialog fragment */ + fun setActiveDialogFragment(dialogFragment: ApplyingDialogFragment) { + activeDialogFragment = dialogFragment + } + + /** The apply action on the current active dialog fragment */ + fun onApply(menuItem: MenuItem) { + activeDialogFragment?.onApply(menuItem) + } + + /** Button action on the current active dialog fragment */ + fun onDialogButton(view: View) { + activeDialogFragment?.onDialogButton(view) + } + + /** Handler for menu clicks */ + fun onMenuClick(item: MenuItem) { + when { + item.itemId == R.id.action_settings -> TODO("Not implemented") //TODO: Not implemented + } + } + + /** Open detail activity */ + fun openDetailActivity(pos: Int) { + startActivity(Intent(this, PaintingDetailActivity::class.java).apply { + putExtra(PaintingDetailActivity.ID, models[pos].id) + }) + } + +} + + +private class GridAdapter(val models: MutableList, val selectedPositions: Set, val resources: Resources) + : RecyclerView.Adapter() { + + /** See [RecyclerView.Adapter] */ + override fun onBindViewHolder(holder: ModelViewHolder, position: Int) { + val model = models[position] + holder.title.text = model.title + if (selectedPositions.contains(position)) { + holder.container.cardElevation = holder.container.maxCardElevation + //holder.container.setCardBackgroundColor(resources.getColor(R.color.cardview_dark_background)) + } else { + holder.container.cardElevation = 5.toFloat() + //holder.container.setCardBackgroundColor(resources.getColor(R.color.cardview_light_background)) + } + launch(CommonPool) { + val image = BitmapDrawable(resources, model.mainImage.await()) + launch(UI) { + holder.mainImage.image = image + } + } + } + + /** See [RecyclerView.Adapter] */ + override fun getItemCount(): Int = models.size + + /** See [RecyclerView.Adapter] */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModelViewHolder + = ModelViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.card_main, parent, false)) + + private class ModelViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + + /** [CardView] container */ + var container: CardView = itemView.findViewById(R.id.painting_preview_card) + /** [ImageView] containing the main image preview */ + var mainImage: ImageView = itemView.findViewById(R.id.painting_preview_image) + /** [TextView] containing the title */ + var title: TextView = itemView.findViewById(R.id.painting_preview_title) + + init { + container.setOnLongClickListener { + (container.context as MainActivity).onPaintingLongClick(adapterPosition) + false + } + + container.setOnClickListener { + (container.context as MainActivity).openDetailActivity(adapterPosition) + } + } + + } + +} + + +private data class Model(val id: String, val mainImage: Deferred, val title: String, val mainImageId: +String) + + +/** Dialog to add new paintings */ +class AddPaintingFragment: ApplyingDialogFragment() { + + private lateinit var paintingService: PaintingService + private lateinit var pictureProvider: PictureProvider + + private var model = Model(null, null) + + private val picturePickHandler: (String?) -> Unit = { + Log.i("IMAGE_SELECT", "Got image $it") + model.imageUrl = it + if (it != null) { + add_painting_main_image.image = Drawable.createFromPath(it) + } else { + add_painting_main_image.imageResource = android.R.drawable.ic_menu_report_image + } + } + + /** See [ApplyingDialogFragment] */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_add_painting, container, false).also { + val toolbar = it.findViewById(R.id.add_painting_toolbar) + toolbar.inflateMenu(R.menu.menu_add_painting) + toolbar.setTitle(R.string.add_painting_title) + } + } + + /** See [ApplyingDialogFragment] */ + override fun onStart() { + (context as MainActivity).setActiveDialogFragment(this) + if (model.imageUrl == null) pictureProvider.pickPicture(picturePickHandler) + super.onStart() + } + + /** See [ApplyingDialogFragment] */ + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState) + .also { + it.requestWindowFeature(Window.FEATURE_NO_TITLE) + paintingService = (context as MainActivity).serviceProvider.paintingService + pictureProvider = (context as MainActivity).pictureProvider + } + + /** See [ApplyingDialogFragment] */ + override fun onCreate(savedInstanceState: Bundle?) { + paintingService = (context as MainActivity).serviceProvider.paintingService + pictureProvider = (context as MainActivity).pictureProvider + super.onCreate(savedInstanceState) + } + + override fun onApply(menuItem: MenuItem) { + model.title = add_painting_name.text!!.toString() + + when { + model.title.isNullOrEmpty() -> activity.errorDialog(R.string.add_painting_warning_name_missing) + model.imageUrl.isNullOrEmpty() -> activity.errorDialog(R.string.add_painting_warning_image_missing) + else -> launch(CommonPool) { + val inputStream = activity.getInputStreamFromUrl(model.imageUrl!!) + if (inputStream != null) { + paintingService.composeNewPainting(title = model.title!!, mainPicture = inputStream).id + dismiss() + } else { + activity.errorDialog(R.string.add_painting_warning_no_id) + Log.w("IMAGE_LOADING", "Saving finished unsuccessful") + } + } + } + } + + override fun onDialogButton(view: View) { + when { + view.id == R.id.add_painting_main_image -> pictureProvider.pickPicture(picturePickHandler) + } + } + + /** Model containing data of the to be created painting */ + data class Model(var title: String?, var imageUrl: String?) + +} \ No newline at end of file diff --git a/android/src/main/java/de/x4fyr/paiman/PaintingDetailActivity.kt b/android/src/main/java/de/x4fyr/paiman/PaintingDetailActivity.kt new file mode 100644 index 0000000..c01860e --- /dev/null +++ b/android/src/main/java/de/x4fyr/paiman/PaintingDetailActivity.kt @@ -0,0 +1,528 @@ +package de.x4fyr.paiman + +import android.app.Activity +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v7.app.AlertDialog +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.StaggeredGridLayoutManager +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.* +import dagger.android.AndroidInjection +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import dagger.android.support.HasSupportFragmentInjector +import de.x4fyr.paiman.app.BaseActivity +import de.x4fyr.paiman.app.errorDialog +import de.x4fyr.paiman.app.getInputStreamFromUrl +import de.x4fyr.paiman.lib.domain.Purchaser +import de.x4fyr.paiman.lib.domain.SavedPainting +import de.x4fyr.paiman.lib.provider.AndroidPictureProvider +import de.x4fyr.paiman.lib.provider.PictureProvider +import de.x4fyr.paiman.lib.services.DesignService +import de.x4fyr.paiman.lib.services.PaintingService +import de.x4fyr.paiman.lib.services.ServiceException +import kotlinx.android.synthetic.main.activity_painting_detail.* +import kotlinx.android.synthetic.main.content_painting_detail.* +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.Deferred +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.launch +import org.jetbrains.anko.displayMetrics +import org.jetbrains.anko.image +import org.jetbrains.anko.layoutInflater +import org.jetbrains.anko.sdk25.coroutines.onClick +import org.jetbrains.anko.sdk25.coroutines.onLongClick +import org.jetbrains.anko.support.v4.onRefresh +import org.threeten.bp.LocalDate +import javax.inject.Inject +import kotlin.properties.Delegates + +/** Activity to show and edit a paintings details */ +class PaintingDetailActivity: BaseActivity(), HasActivityInjector, HasSupportFragmentInjector { + + /** [DispatchingAndroidInjector] for activity dependency injection */ + @Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector + /** [DispatchingAndroidInjector] for fragment dependency injection */ + @Inject lateinit var dispatchingFragmentInjector: DispatchingAndroidInjector + + /** See [HasActivityInjector.activityInjector] */ + override fun activityInjector(): AndroidInjector = dispatchingActivityInjector + + /** Returns an [AndroidInjector] of [Fragment]s. */ + override fun supportFragmentInjector(): AndroidInjector = dispatchingFragmentInjector + + /** Injected */ + @Inject lateinit var paintingService: PaintingService + /** Injected */ + @Inject lateinit var designService: DesignService + private lateinit var pictureProvider: PictureProvider + + companion object { + /** ID identifier string for intents */ + const val ID = "id" + } + + private lateinit var id: String + private lateinit var model: DetailModel + + private lateinit var wipGridAdapter: WIPGridAdapter + private lateinit var refGridAdapter: RefGridAdapter + + private var loadLocked: Boolean by Delegates.observable(false) { _, _, newValue -> + if (!newValue && lockWaiter.size > 0) { + loadLocked = true + lockWaiter.removeAt(0).invoke() + } + } + private val lockWaiter = mutableListOf<() -> Unit>() + + private fun actUnderLoadLock(action: () -> Unit) { + if (loadLocked) { + lockWaiter.add(action) + } else { + loadLocked = true + action.invoke() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + id = intent.getStringExtra(ID) + pictureProvider = AndroidPictureProvider(this) + loadModel() + setContentView(R.layout.activity_painting_detail) + setSupportActionBar(toolbar) + fab.setOnClickListener { _ -> + AlertDialog.Builder(this@PaintingDetailActivity).apply { + setTitle(R.string.edit_painting_title) + var imageDirty = false + var url = "" + val contentView = layoutInflater.inflate(R.layout.dialog_edit_painting, null) as ScrollView + contentView.findViewById( + R.id.edit_painting_main_image_button).apply { + launch(UI) { + image = BitmapDrawable(resources, model.mainImage.await()) + } + setOnClickListener { button -> + pictureProvider.pickPicture { + if (it != null) { + imageDirty = true + url = it + (button as ImageButton).image = Drawable.createFromPath(url) + } + } + } + } + val titleEditView = contentView.findViewById(R.id.edit_painting_title).apply { + launch(UI) { + setText(model.title) + } + } + val oldToWipView = contentView.findViewById(R.id.edit_painting_old_to_wip) + val datePicker = contentView.findViewById(R.id.edit_painting_finished_date).apply { + launch(UI) { + if (model.finished) { + visibility = View.VISIBLE + updateDate(model.finishingDate!!.year, model.finishingDate!!.monthValue, + model.finishingDate!!.dayOfMonth) + + } else { + visibility = View.GONE + } + } + } + val finishedToggle = contentView.findViewById(R.id.edit_painting_finished_toggle).apply { + launch(UI) { + isChecked = model.finished + setOnCheckedChangeListener { _, isChecked -> + datePicker.visibility = if (isChecked) View.VISIBLE else View.GONE + } + } + } + setView(contentView) + setCancelable(true) + setPositiveButton(R.string.edit_painting_apply) { dialog, _ -> + launch(UI) { + val newTitle = titleEditView.text.toString() + val isFinished = finishedToggle.isChecked + val finishedDate = LocalDate.of(datePicker.year, datePicker.month, datePicker.dayOfMonth) + val successTitle: Deferred + val successImage: Deferred + val successFinished: Deferred + successTitle = if (newTitle != model.title) { + async(CommonPool) { + try { + paintingService.changePainting(model.painting.copy(title = newTitle)) + true + } catch (e: ServiceException) { + Log.e(this::class.simpleName, "Error changing title", e) + errorDialog(R.string.edit_painting_error_changing_title) + false + } + } + } else async(CommonPool) { true } + successImage = if (imageDirty) { + val oldToWip = oldToWipView.isChecked + async(CommonPool) { + val inputStream = getInputStreamFromUrl(url) + if (inputStream != null) { + try { + paintingService.replaceMainPicture(painting = model.painting, + newPicture = inputStream, moveOldToWip = oldToWip) + true + } catch (e: ServiceException) { + Log.e(this::class.simpleName, "Error replacing main image", e) + errorDialog(R.string.edit_painting_error_replacing_main_image) + false + } + } else false + } + } else async(CommonPool) { true } + successFinished = async(CommonPool) { + when { + isFinished -> { + try { + paintingService.changePainting(model.painting.copy(finished = true, + finishingDate = finishedDate)) + true + } catch (e: ServiceException) { + Log.e(this::class.simpleName, "Error setting finished") + errorDialog(R.string.edit_painting_error_finished) + false + } + } + model.finished -> try { + paintingService.changePainting( + model.painting.copy(finished = false, finishingDate = null)) + true + } catch (e: ServiceException) { + Log.e(this::class.simpleName, "Error setting finished") + errorDialog(R.string.edit_painting_error_finished) + false + } + else -> true + } + } + if (successImage.await() && successTitle.await() && successFinished.await()) { + dialog.dismiss() + loadModel() + } else { + throw error( + "Failed editing: ${successImage.await()}, ${successTitle.await()}, ${successFinished.await()}") + } + } + } + }.create().show() + } + wipGridAdapter = WIPGridAdapter(mutableListOf(), resources, this, paintingService) + wip.apply { + val spanCount = Math.floor( + displayMetrics.widthPixels/TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 106.toFloat(), + displayMetrics).toDouble()).toInt() + layoutManager = StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL) + adapter = wipGridAdapter + } + add_wip.setOnClickListener { + pictureProvider.pickPicture { + if (it != null) { + launch(CommonPool) { + val stream = getInputStreamFromUrl(it) + if (stream != null) { + paintingService.addWipPicture(model.painting, setOf(stream)) + } else { + errorDialog(R.string.error_image_not_readable) + } + } + } + } + } + refGridAdapter = RefGridAdapter(mutableListOf(), resources, this, paintingService) + refs.apply { + val spanCount = Math.floor( + displayMetrics.widthPixels/TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 106.toFloat(), + displayMetrics).toDouble()).toInt() + layoutManager = StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL) + adapter = refGridAdapter + } + add_ref.setOnClickListener { + pictureProvider.pickPicture { + if (it != null) { + launch(CommonPool) { + val stream = getInputStreamFromUrl(it) + if (stream != null) { + paintingService.addReferences(model.painting, setOf(stream)) + } else { + errorDialog(R.string.error_image_not_readable) + } + } + } + } + } + sell_painting.setOnClickListener { + AlertDialog.Builder(this).apply { + val contentView = layoutInflater.inflate(R.layout.dialog_sell_painting, null, false) + val purchaserInput = contentView.findViewById(R.id.sell_painting_purchaser) + val priceInput = contentView.findViewById(R.id.sell_painting_price) + val datePicker = contentView.findViewById(R.id.sell_painting_datePicker) + setView(contentView) + setCancelable(true) + setPositiveButton(R.string.sell_painting_apply) { dialog, _ -> + val purchaser = purchaserInput.text.toString().trim() + val price = priceInput.text.toString().toDouble() + val date = LocalDate.of(datePicker.year, datePicker.month, datePicker.dayOfMonth) + launch(CommonPool) { + paintingService.sellPainting(model.painting, Purchaser(name = purchaser), date, price) + } + dialog.dismiss() + loadModel() + } + }.create().show() + } + swipeRefreshLayout.onRefresh { + Log.d(this::class.simpleName, "Start refreshing") + loadModel() + } + } + + /** Load model and update view */ + fun loadModel() { + swipeRefreshLayout?.isRefreshing = true + Log.d(this::class.simpleName, "Started loading of model") + actUnderLoadLock { + launch(UI) { + Log.d("${this@PaintingDetailActivity::class.simpleName}::loadModel", "1 Get Painting") + val painting = paintingService.get(id) + Log.d("${this@PaintingDetailActivity::class.simpleName}::loadModel", "2 Assemble model") + model = DetailModel(mainImage = async(CommonPool) { + designService.getOrLoadFullSizeBitmap(painting.mainPicture) + }, painting = painting, + title = painting.title, + tags = painting.tags, + finished = painting.finished, + finishingDate = painting.finishingDate, + sold = painting.sellingInfo != null, + purchaser = painting.sellingInfo?.purchaser?.name, + sellingDate = painting.sellingInfo?.date, + price = painting.sellingInfo?.price, + wip = async(CommonPool) { + painting.wip.map { + ImageModel(id = it.id, painting = painting, + image = async(CommonPool) { designService.getOrLoadThumbnailBitmap(it) }) + } + }, + ref = async(CommonPool) { + painting.references.map { + ImageModel(id = it.id, painting = painting, + image = async(CommonPool) { designService.getOrLoadThumbnailBitmap(it) }) + } + }) + Log.d("${this@PaintingDetailActivity::class.simpleName}::loadModel", "3 Successfully assembled model") + Log.d("${this@PaintingDetailActivity::class.simpleName}::loadModel", "4 Updating view") + toolbar_layout.title = model.title + finishing.text = if (model.finished) getString(R.string.painting_detail_finished, model + .finishingDate) + else getString(R.string.painting_detail_unfinished) + selling.text = if (model.sold) getString(R.string.painting_detail_sold, model.purchaser, + model.sellingDate, model.price) + else getString(R.string.painting_detail_unsold) + tagLayout.removeAllViews() + model.tags.forEach { tag -> + tagLayout.addView((layoutInflater.inflate(R.layout.tag, tagLayout, false) as LinearLayout).apply { + findViewById