From 99f753713386984efff062546e2b2ec5ef2cebe1 Mon Sep 17 00:00:00 2001 From: Florian Fournier Date: Wed, 20 Nov 2024 16:26:33 +0100 Subject: [PATCH 1/2] Pass the code on Kotlin --- README.md | 41 +-- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 2 +- .../com/tonytangandroid/wood/sample/App.java | 15 - .../com/tonytangandroid/wood/sample/App.kt | 14 + .../wood/sample/GeneratingLogActivity.java | 46 --- .../wood/sample/GeneratingLogActivity.kt | 42 +++ .../wood/sample/HomeActivity.java | 76 ---- .../wood/sample/HomeActivity.kt | 74 ++++ build.gradle | 8 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 3 +- wood-no-op/build.gradle | 7 + wood-no-op/src/main/AndroidManifest.xml | 4 +- .../wood/LeafListActivity.java | 13 - .../wood/LeavesCollectionFragment.java | 16 - .../java/com/tonytangandroid/wood/Wood.java | 26 -- .../com/tonytangandroid/wood/WoodTree.java | 53 --- .../woodnoop/LeafListActivity.kt | 11 + .../woodnoop/LeavesCollectionFragment.kt | 18 + .../java/com/tonytangandroid/woodnoop/Wood.kt | 23 ++ .../com/tonytangandroid/woodnoop/WoodTree.kt | 52 +++ .../res/layout/wood_activity_leaf_list.xml | 6 +- wood/build.gradle | 11 +- .../example/test/ExampleInstrumentedTest.java | 26 -- .../example/test/ExampleInstrumentedTest.kt | 22 ++ wood/src/main/AndroidManifest.xml | 12 +- .../com/tonytangandroid/wood/Callback.java | 5 - .../java/com/tonytangandroid/wood/Callback.kt | 5 + .../wood/ClearTransactionsService.java | 20 - .../wood/ClearTransactionsService.kt | 15 + .../com/tonytangandroid/wood/Debouncer.java | 37 -- .../com/tonytangandroid/wood/Debouncer.kt | 21 ++ .../wood/DismissNotificationService.java | 18 - .../wood/DismissNotificationService.kt | 12 + .../com/tonytangandroid/wood/FormatUtils.java | 92 ----- .../com/tonytangandroid/wood/FormatUtils.kt | 84 +++++ .../tonytangandroid/wood/HighlightSpan.java | 30 -- .../com/tonytangandroid/wood/HighlightSpan.kt | 21 ++ .../com/tonytangandroid/wood/JobExecutor.java | 45 --- .../com/tonytangandroid/wood/JobExecutor.kt | 48 +++ .../java/com/tonytangandroid/wood/Leaf.java | 78 ---- .../java/com/tonytangandroid/wood/Leaf.kt | 51 +++ .../com/tonytangandroid/wood/LeafAdapter.java | 66 ---- .../com/tonytangandroid/wood/LeafAdapter.kt | 56 +++ .../com/tonytangandroid/wood/LeafDao.java | 48 --- .../java/com/tonytangandroid/wood/LeafDao.kt | 49 +++ .../wood/LeafDetailFragment.java | 334 ----------------- .../wood/LeafDetailFragment.kt | 343 ++++++++++++++++++ .../wood/LeafDetailViewModel.java | 18 - .../wood/LeafDetailViewModel.kt | 14 + .../wood/LeafDetailsActivity.java | 49 --- .../wood/LeafDetailsActivity.kt | 50 +++ .../wood/LeafListActivity.java | 24 -- .../tonytangandroid/wood/LeafListActivity.kt | 21 ++ .../wood/LeafListViewModel.java | 74 ---- .../tonytangandroid/wood/LeafListViewModel.kt | 62 ++++ .../tonytangandroid/wood/LeafViewHolder.java | 44 --- .../tonytangandroid/wood/LeafViewHolder.kt | 40 ++ .../wood/LeavesCollectionFragment.java | 155 -------- .../wood/LeavesCollectionFragment.kt | 141 +++++++ .../tonytangandroid/wood/ListDiffUtil.java | 40 -- .../com/tonytangandroid/wood/ListDiffUtil.kt | 37 ++ .../java/com/tonytangandroid/wood/Logger.java | 21 -- .../java/com/tonytangandroid/wood/Logger.kt | 22 ++ .../wood/NotificationHelper.java | 131 ------- .../wood/NotificationHelper.kt | 133 +++++++ .../wood/RetentionManager.java | 77 ---- .../tonytangandroid/wood/RetentionManager.kt | 77 ++++ .../com/tonytangandroid/wood/Sampler.java | 65 ---- .../java/com/tonytangandroid/wood/Sampler.kt | 72 ++++ .../com/tonytangandroid/wood/TextUtil.java | 73 ---- .../java/com/tonytangandroid/wood/TextUtil.kt | 63 ++++ .../java/com/tonytangandroid/wood/Wood.java | 61 ---- .../java/com/tonytangandroid/wood/Wood.kt | 56 +++ .../tonytangandroid/wood/WoodColorUtil.java | 61 ---- .../com/tonytangandroid/wood/WoodColorUtil.kt | 42 +++ .../tonytangandroid/wood/WoodDatabase.java | 24 -- .../com/tonytangandroid/wood/WoodDatabase.kt | 23 ++ .../com/tonytangandroid/wood/WoodTree.java | 210 ----------- .../java/com/tonytangandroid/wood/WoodTree.kt | 205 +++++++++++ .../tonytangandroid/wood/FormatUtilsTest.java | 14 - .../tonytangandroid/wood/FormatUtilsTest.kt | 12 + 83 files changed, 2089 insertions(+), 2225 deletions(-) delete mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/App.java create mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/App.kt delete mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.java create mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.kt delete mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.java create mode 100644 app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt delete mode 100644 wood-no-op/src/main/java/com/tonytangandroid/wood/LeafListActivity.java delete mode 100644 wood-no-op/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java delete mode 100644 wood-no-op/src/main/java/com/tonytangandroid/wood/Wood.java delete mode 100644 wood-no-op/src/main/java/com/tonytangandroid/wood/WoodTree.java create mode 100644 wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeafListActivity.kt create mode 100644 wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeavesCollectionFragment.kt create mode 100644 wood-no-op/src/main/java/com/tonytangandroid/woodnoop/Wood.kt create mode 100644 wood-no-op/src/main/java/com/tonytangandroid/woodnoop/WoodTree.kt delete mode 100644 wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.java create mode 100644 wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Callback.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Callback.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Debouncer.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/JobExecutor.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Leaf.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Leaf.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDao.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Logger.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Logger.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/RetentionManager.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Sampler.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Sampler.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/TextUtil.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/TextUtil.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/Wood.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/Wood.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt delete mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodTree.java create mode 100644 wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt delete mode 100644 wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.java create mode 100644 wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.kt diff --git a/README.md b/README.md index bcfc757..21e66b9 100644 --- a/README.md +++ b/README.md @@ -59,17 +59,16 @@ import timber.log.Timber object WoodIntegrationUtil { - @JvmStatic - fun initWood(application: Application) { - Timber.plant( - WoodTree(application) - .retainDataFor(WoodTree.Period.FOREVER) - .logLevel(Log.VERBOSE) - .autoScroll(false) - .maxLength(100000) - .showNotification(true) - ) - } + fun initWood(application: Application) { + Timber.plant( + WoodTree(application) + .retainDataFor(WoodTree.Period.FOREVER) + .logLevel(Log.VERBOSE) + .autoScroll(false) + .maxLength(100000) + .showNotification(true) + ) + } } ``` @@ -77,32 +76,32 @@ That's it! Wood will now record all Timber log. ##### Show Sticky/Normal Notification Sticky => true and Normal => false -```java -new WoodTree(context).showNotification(true/false) +```kotlin +WoodTree(context).showNotification(true/false) ``` ### Other Settings ##### Check stored data Launch the Wood UI directly within your app with the intent from `Wood.getLaunchIntent()`. -```java -startActivity(Wood.getLaunchIntent(this)); +```kotlin +startActivity(Wood.getLaunchIntent(this)) ``` ##### Add app shortcut to your app -```java -Wood.addAppShortcut(this); +```kotlin +Wood.addAppShortcut(this) ``` ##### Max Length Set Response Max length to store -```java -new WoodTree(context).maxContentLength(10240L)//the maximum length (in bytes) +```kotlin +WoodTree(context).maxContentLength(10240L)//the maximum length (in bytes) ``` ##### Retention Period Set the retention period for Timber log data captured -```java -new WoodTree(context).retainDataFor(Period.ONE_WEEK) +```kotlin +WoodTree(context).retainDataFor(Period.ONE_WEEK) ``` ## Acknowledgements diff --git a/app/build.gradle b/app/build.gradle index d571510..15481e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'com.android.application' apply plugin: "org.jetbrains.kotlin.android" android { + namespace = "com.tonytangandroid.wood.sample" + buildTypes { debug { testCoverageEnabled true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fcbef72..56a2a1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ package="com.tonytangandroid.wood.sample"> generateTimberLog()); - findViewById(R.id.btn_test_extreme_log).setOnClickListener(view -> keepGenerateLog()); - findViewById(R.id.launch_wood_directly).setOnClickListener(view -> launchWoodDirectly()); - Wood.addAppShortcut(this); - } - - private void keepGenerateLog() { - startActivity(new Intent(this, GeneratingLogActivity.class)); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.sample_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_menu_github: - String url = "https://github.com/TonyTangAndroid/Wood"; - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse(url)); - startActivity(i); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void launchWoodDirectly() { - startActivity(Wood.getLaunchIntent(this)); - } - - private void generateTimberLog() { - - Timber.v("This is a VERBOSE message."); - Timber.d("This is an DEBUG message."); - Timber.i("This is an INFO message."); - Timber.w("This is an WARNING message."); - Timber.e("This is an ERROR message."); - logError(); - } - - public static void logInBackground() { - new Thread(() -> Timber.i("This is an INFO message triggered in background thread.")).start(); - } - - private void logError() { - try { - String shortSrc = ""; - String substring = shortSrc.substring(10); - Timber.v(substring + substring + substring); - } catch (Exception e) { - Timber.e(e); - } - } -} diff --git a/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt b/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt new file mode 100644 index 0000000..4ebb4b5 --- /dev/null +++ b/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt @@ -0,0 +1,74 @@ +package com.tonytangandroid.wood.sample + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.tonytangandroid.wood.Wood.addAppShortcut +import com.tonytangandroid.wood.Wood.getLaunchIntent +import timber.log.Timber +import java.lang.Exception + +class HomeActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_home) + findViewById(R.id.btn_generate_log).setOnClickListener(View.OnClickListener { view: View? -> generateTimberLog() }) + findViewById(R.id.btn_test_extreme_log).setOnClickListener(View.OnClickListener { view: View? -> keepGenerateLog() }) + findViewById(R.id.launch_wood_directly).setOnClickListener(View.OnClickListener { view: View? -> launchWoodDirectly() }) + addAppShortcut(this) + } + + private fun keepGenerateLog() { + startActivity(Intent(this, GeneratingLogActivity::class.java)) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.sample_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.action_menu_github) { + val url = "https://github.com/TonyTangAndroid/Wood" + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(url) + startActivity(i) + return true + } + return super.onOptionsItemSelected(item) + } + + private fun launchWoodDirectly() { + startActivity(getLaunchIntent(this)) + } + + private fun generateTimberLog() { + Timber.v("This is a VERBOSE message.") + Timber.d("This is an DEBUG message.") + Timber.i("This is an INFO message.") + Timber.w("This is an WARNING message.") + Timber.e("This is an ERROR message.") + logError() + } + + private fun logError() { + try { + val shortSrc = "" + val substring = shortSrc.substring(10) + Timber.v("$substring$substring$substring") + } catch (e: Exception) { + Timber.e(e) + } + } + + companion object { + @JvmStatic + fun logInBackground() { + Thread(Runnable { Timber.i("This is an INFO message triggered in background thread.") }).start() + } + } +} diff --git a/build.gradle b/build.gradle index b4fa9f9..c658f53 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,8 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" + classpath 'com.android.tools.build:gradle:8.7.2' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20' classpath "nl.neotech.plugin:android-root-coverage-plugin:${neotech_coverage}" } } @@ -36,8 +36,8 @@ allprojects { ext { minSdkVersion = 21 - targetSdkVersion = 33 - compileSdkVersion = 33 + targetSdkVersion = 35 + compileSdkVersion = 35 versionCode = 19 versionName = "2.0.0" diff --git a/gradle.properties b/gradle.properties index f0223aa..d42c9a7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ neotech_coverage=1.5.3 android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx6144M -android.jetifier.ignorelist=bcprov-jdk15on-1.68.jar org.gradle.unsafe.configuration-cache=false # Use this flag sparingly, in case some of the plugins are not fully compatible org.gradle.unsafe.configuration-cache-problems=warn +#android.parallel=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8fad3f5..66d5593 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Nov 20 13:55:53 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/wood-no-op/build.gradle b/wood-no-op/build.gradle index bfb64fc..7e8edc4 100644 --- a/wood-no-op/build.gradle +++ b/wood-no-op/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' ext { bintrayRepo = 'maven' @@ -25,12 +26,18 @@ ext { } android { + namespace = "com.tonytangandroid.woodnoop" compileSdk rootProject.ext.compileSdkVersion defaultConfig { minSdk rootProject.ext.minSdkVersion } + kotlinOptions { + jvmTarget = '1.8' + } } dependencies { implementation "com.jakewharton.timber:timber:$timberVersion" + implementation 'androidx.core:core-ktx:1.13.1' + implementation "androidx.fragment:fragment-ktx:1.8.3" } \ No newline at end of file diff --git a/wood-no-op/src/main/AndroidManifest.xml b/wood-no-op/src/main/AndroidManifest.xml index 3cb1354..531b26d 100644 --- a/wood-no-op/src/main/AndroidManifest.xml +++ b/wood-no-op/src/main/AndroidManifest.xml @@ -1,11 +1,11 @@ + package="com.tonytangandroid.woodnoop"> + android:taskAffinity="com.tonytangandroid.woodnoop.task"/> \ No newline at end of file diff --git a/wood-no-op/src/main/java/com/tonytangandroid/wood/LeafListActivity.java b/wood-no-op/src/main/java/com/tonytangandroid/wood/LeafListActivity.java deleted file mode 100644 index 4ff0d39..0000000 --- a/wood-no-op/src/main/java/com/tonytangandroid/wood/LeafListActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.Activity; -import android.os.Bundle; - -public class LeafListActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.wood_activity_leaf_list); - } -} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java b/wood-no-op/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java deleted file mode 100644 index 7074f70..0000000 --- a/wood-no-op/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -public class LeavesCollectionFragment extends Fragment { - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.wood_fragment_leaves_collection, container, false); - } -} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/wood/Wood.java b/wood-no-op/src/main/java/com/tonytangandroid/wood/Wood.java deleted file mode 100644 index e53d9e3..0000000 --- a/wood-no-op/src/main/java/com/tonytangandroid/wood/Wood.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.tonytangandroid.wood; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.os.Build; - -public class Wood { - - /** - * Get an Intent to launch the Wood UI directly. - * - * @param context A Context. - * @return An Intent for the main Wood Activity that can be started with {@link - * Context#startActivity(Intent)}. - */ - public static Intent getLaunchIntent(Context context) { - return new Intent(context, LeafListActivity.class); - } - - @TargetApi(Build.VERSION_CODES.N_MR1) - @SuppressWarnings("WeakerAccess") - public static String addAppShortcut(Context context) { - return null; - } -} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/wood/WoodTree.java b/wood-no-op/src/main/java/com/tonytangandroid/wood/WoodTree.java deleted file mode 100644 index 0044282..0000000 --- a/wood-no-op/src/main/java/com/tonytangandroid/wood/WoodTree.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import timber.log.Timber; - -public class WoodTree extends Timber.Tree { - - public WoodTree(Context context) {} - - public WoodTree(Context context, String threadTagPrefix) {} - - public WoodTree showNotification(boolean sticky) { - return this; - } - - public WoodTree retainDataFor(Period period) { - return this; - } - - public WoodTree maxLength(int max) { - return this; - } - - @Override - protected void log( - int priority, @Nullable String tag, @NotNull String message, @Nullable Throwable t) {} - - public WoodTree limitToTheseTaggerList(List supportedTaggerList) { - return this; - } - - public WoodTree logLevel(int logLevel) { - return this; - } - - public WoodTree autoScroll(boolean autoScroll) { - return this; - } - - public static boolean autoScroll(Context context) { - return false; - } - - public enum Period { - ONE_HOUR, - ONE_DAY, - ONE_WEEK, - FOREVER - } -} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeafListActivity.kt b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeafListActivity.kt new file mode 100644 index 0000000..48efde0 --- /dev/null +++ b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeafListActivity.kt @@ -0,0 +1,11 @@ +package com.tonytangandroid.woodnoop + +import android.app.Activity +import android.os.Bundle + +class LeafListActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.wood_activity_leaf_list) + } +} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeavesCollectionFragment.kt b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeavesCollectionFragment.kt new file mode 100644 index 0000000..9baa591 --- /dev/null +++ b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/LeavesCollectionFragment.kt @@ -0,0 +1,18 @@ +package com.tonytangandroid.woodnoop + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +class LeavesCollectionFragment: Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.wood_fragment_leaves_collection, container, false) + } +} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/Wood.kt b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/Wood.kt new file mode 100644 index 0000000..e078910 --- /dev/null +++ b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/Wood.kt @@ -0,0 +1,23 @@ +package com.tonytangandroid.woodnoop + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.os.Build + +object Wood { + /** + * Get an Intent to launch the Wood UI directly. + * + * @param context A Context. + * @return An Intent for the main Wood Activity that can be started with [Context.startActivity]. + */ + fun getLaunchIntent(context: Context?): Intent { + return Intent(context, LeafListActivity::class.java) + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + fun addAppShortcut(context: Context?): String? { + return null + } +} diff --git a/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/WoodTree.kt b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/WoodTree.kt new file mode 100644 index 0000000..13daff0 --- /dev/null +++ b/wood-no-op/src/main/java/com/tonytangandroid/woodnoop/WoodTree.kt @@ -0,0 +1,52 @@ +package com.tonytangandroid.woodnoop + +import android.content.Context +import timber.log.Timber + +class WoodTree : Timber.Tree { + constructor(context: Context?) + + constructor(context: Context?, threadTagPrefix: String?) + + fun showNotification(sticky: Boolean): WoodTree { + return this + } + + fun retainDataFor(period: Period?): WoodTree { + return this + } + + fun maxLength(max: Int): WoodTree { + return this + } + + protected override fun log( + priority: Int, tag: String?, message: String, t: Throwable? + ) { + } + + fun limitToTheseTaggerList(supportedTaggerList: List?): WoodTree { + return this + } + + fun logLevel(logLevel: Int): WoodTree { + return this + } + + fun autoScroll(autoScroll: Boolean): WoodTree { + return this + } + + enum class Period { + ONE_HOUR, + ONE_DAY, + ONE_WEEK, + FOREVER + } + + companion object { + fun autoScroll(context: Context?): Boolean { + return false + } + } +} diff --git a/wood-no-op/src/main/res/layout/wood_activity_leaf_list.xml b/wood-no-op/src/main/res/layout/wood_activity_leaf_list.xml index 023462a..fc5b552 100644 --- a/wood-no-op/src/main/res/layout/wood_activity_leaf_list.xml +++ b/wood-no-op/src/main/res/layout/wood_activity_leaf_list.xml @@ -1,15 +1,13 @@ + android:orientation="vertical"> diff --git a/wood/build.gradle b/wood/build.gradle index 62f3b01..c39aa91 100644 --- a/wood/build.gradle +++ b/wood/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'kotlin-kapt' ext { bintrayRepo = 'maven' @@ -25,6 +27,7 @@ ext { } android { + namespace = "com.tonytangandroid.wood" compileSdk rootProject.ext.compileSdkVersion defaultConfig { @@ -45,6 +48,9 @@ android { sourceCompatibility = '1.8' targetCompatibility = '1.8' } + kotlinOptions { + jvmTarget = '1.8' + } } @@ -57,16 +63,19 @@ dependencies { api "androidx.appcompat:appcompat:$appcompatVersion" api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-rxjava2:$roomVersion" + api "androidx.fragment:fragment-ktx:1.8.3" api "com.uber.autodispose:autodispose:1.4.0" api "com.uber.autodispose:autodispose-android:1.4.0" api "com.uber.autodispose:autodispose-android-archcomponents:1.4.0" - annotationProcessor "androidx.room:room-compiler:$roomVersion" + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' api "androidx.paging:paging-rxjava2:$pagingVersion" api "androidx.paging:paging-runtime:$pagingVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleModelVersion" api "com.google.android.material:material:$materialVersioon" + kapt "androidx.room:room-compiler:$roomVersion" api "com.google.dagger:dagger:${daggerVersion}" diff --git a/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.java b/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.java deleted file mode 100644 index 1192922..0000000 --- a/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.test; - -import static org.junit.Assert.*; - -import android.content.Context; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; -import com.google.common.truth.Truth; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Truth.assertThat(appContext.getPackageName()).isEqualTo("com.tonytangandroid.wood.test"); - } -} diff --git a/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.kt b/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..5e8ebac --- /dev/null +++ b/wood/src/androidTest/java/com/example/test/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.example.test + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * @see [Testing documentation](http://d.android.com/tools/testing) + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + Truth.assertThat(appContext.packageName).isEqualTo("com.tonytangandroid.wood.test") + } +} diff --git a/wood/src/main/AndroidManifest.xml b/wood/src/main/AndroidManifest.xml index 9ae958d..0fe5815 100644 --- a/wood/src/main/AndroidManifest.xml +++ b/wood/src/main/AndroidManifest.xml @@ -2,24 +2,26 @@ + + diff --git a/wood/src/main/java/com/tonytangandroid/wood/Callback.java b/wood/src/main/java/com/tonytangandroid/wood/Callback.java deleted file mode 100644 index 5528c2b..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Callback.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.tonytangandroid.wood; - -interface Callback { - void onEmit(T event); -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Callback.kt b/wood/src/main/java/com/tonytangandroid/wood/Callback.kt new file mode 100644 index 0000000..24a6368 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Callback.kt @@ -0,0 +1,5 @@ +package com.tonytangandroid.wood + +internal interface Callback { + fun onEmit(event: T) +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.java b/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.java deleted file mode 100644 index ee54436..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.IntentService; -import android.content.Intent; -import androidx.annotation.Nullable; - -public class ClearTransactionsService extends IntentService { - - public ClearTransactionsService() { - super("Wood-ClearTransactionsService"); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - int deletedTransactionCount = WoodDatabase.getInstance(this).leafDao().clearAll(); - Logger.i(deletedTransactionCount + " transactions deleted"); - NotificationHelper notificationHelper = new NotificationHelper(this); - notificationHelper.dismiss(); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.kt b/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.kt new file mode 100644 index 0000000..bc5e2c6 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/ClearTransactionsService.kt @@ -0,0 +1,15 @@ +package com.tonytangandroid.wood + +import android.app.IntentService +import android.content.Intent + +class ClearTransactionsService : IntentService("Wood-ClearTransactionsService") { + + override fun onHandleIntent(intent: Intent?) { + val deletedTransactionCount = WoodDatabase.getInstance(this).leafDao().clearAll() + Logger.i("$deletedTransactionCount transactions deleted") + val notificationHelper = NotificationHelper(this) + notificationHelper.dismiss() + } + +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Debouncer.java b/wood/src/main/java/com/tonytangandroid/wood/Debouncer.java deleted file mode 100644 index 9eff565..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Debouncer.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.tonytangandroid.wood; - -import android.os.Handler; -import androidx.annotation.NonNull; - -class Debouncer { - - private final int mInterval; - private final Callback mCallback; - private final Handler mHandler; - - public Debouncer(int intervalInMills, @NonNull Callback callback) { - mInterval = intervalInMills; - mCallback = callback; - mHandler = new Handler(); - } - - public void consume(V event) { - mHandler.removeCallbacksAndMessages(null); - mHandler.postDelayed(new Counter<>(event, mCallback), mInterval); - } - - public static class Counter implements Runnable { - private final T mEvent; - private final Callback mCallback; - - Counter(T event, Callback callback) { - mEvent = event; - mCallback = callback; - } - - @Override - public void run() { - mCallback.onEmit(mEvent); - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt b/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt new file mode 100644 index 0000000..97f6ab4 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt @@ -0,0 +1,21 @@ +package com.tonytangandroid.wood + +import android.os.Handler + +internal class Debouncer(private val mInterval: Int, private val mCallback: Callback) { + private val mHandler = Handler() + + fun consume(event: T) { + mHandler.removeCallbacksAndMessages(null) + mHandler.postDelayed(Counter(event, mCallback), mInterval.toLong()) + } + + class Counter internal constructor( + private val mEvent: T, + private val mCallback: Callback + ) : Runnable { + override fun run() { + mCallback.onEmit(mEvent) + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.java b/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.java deleted file mode 100644 index 48e1572..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.IntentService; -import android.content.Intent; -import androidx.annotation.Nullable; - -public class DismissNotificationService extends IntentService { - - public DismissNotificationService() { - super("Wood-DismissNotificationService"); - } - - @Override - protected void onHandleIntent(@Nullable Intent intent) { - NotificationHelper notificationHelper = new NotificationHelper(this); - notificationHelper.dismiss(); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.kt b/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.kt new file mode 100644 index 0000000..c2cb44f --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/DismissNotificationService.kt @@ -0,0 +1,12 @@ +package com.tonytangandroid.wood + +import android.app.IntentService +import android.content.Intent + +class DismissNotificationService : IntentService("Wood-DismissNotificationService") { + + override fun onHandleIntent(intent: Intent?) { + val notificationHelper = NotificationHelper(this) + notificationHelper.dismiss() + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java deleted file mode 100644 index 7cb1f43..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.tonytangandroid.wood; - -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.widget.TextView; -import androidx.annotation.NonNull; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -class FormatUtils { - - public static CharSequence formatTextHighlight(String text, String searchKey) { - if (TextUtil.isNullOrWhiteSpace(text) || TextUtil.isNullOrWhiteSpace(searchKey)) { - return text; - } else { - List startIndexes = indexOf(text, searchKey); - SpannableString spannableString = new SpannableString(text); - applyHighlightSpan(spannableString, startIndexes, searchKey.length()); - return spannableString; - } - } - - @NonNull - public static List indexOf(CharSequence charSequence, String criteria) { - String text = charSequence.toString().toLowerCase(); - criteria = criteria.toLowerCase(); - - List startPositions = new ArrayList<>(); - int index = text.indexOf(criteria); - while (index >= 0) { - startPositions.add(index); - index = text.indexOf(criteria, index + 1); - } - return startPositions; - } - - public static void applyHighlightSpan( - Spannable spannableString, List indexes, int length) { - for (Integer position : indexes) { - spannableString.setSpan( - new HighlightSpan( - WoodColorUtil.HIGHLIGHT_BACKGROUND_COLOR, - WoodColorUtil.HIGHLIGHT_TEXT_COLOR, - WoodColorUtil.HIGHLIGHT_UNDERLINE), - position, - position + length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - public static CharSequence getShareText(Leaf transaction) { - return transaction.body(); - } - - private static CharSequence v(CharSequence charSequence) { - return (charSequence != null) ? charSequence : ""; - } - - public static List highlightSearchKeyword(TextView textView, String searchKey) { - - CharSequence body = textView.getText(); - if (body instanceof Spannable) { - Spannable spannableBody = (Spannable) body; - // remove old HighlightSpans - HighlightSpan spansToRemove[] = - spannableBody.getSpans(0, spannableBody.length() - 1, HighlightSpan.class); - for (Object span : spansToRemove) { - spannableBody.removeSpan(span); - } - // add spans only if searchKey size is > 0 - if (searchKey != null && searchKey.length() > 0) { - // get indices of new search - List startIndexes = indexOf(body.toString(), searchKey); - // add spans - applyHighlightSpan(spannableBody, startIndexes, searchKey.length()); - return startIndexes; - } - } - - return new ArrayList<>(0); - } - - public static String timeDesc(long nowInMilliseconds) { - Date date = new Date(nowInMilliseconds); - SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss.SSS MMM-dd", Locale.US); - return formatter.format(date); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt new file mode 100644 index 0000000..624c3fe --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt @@ -0,0 +1,84 @@ +package com.tonytangandroid.wood + +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.widget.TextView +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +internal object FormatUtils { + + fun formatTextHighlight(text: String, searchKey: String): CharSequence { + if (TextUtil.isNullOrWhiteSpace(text) || TextUtil.isNullOrWhiteSpace(searchKey)) { + return text + } else { + val startIndexes = indexOf(text, searchKey) + val spannableString = SpannableString(text) + applyHighlightSpan(spannableString, startIndexes, searchKey.length) + return spannableString + } + } + + fun indexOf(charSequence: CharSequence, criteria: String): List { + val criteriaLowerCase = criteria.lowercase(Locale.getDefault()) + val text = charSequence.toString().lowercase(Locale.getDefault()) + + val startPositions: MutableList = ArrayList() + var index = text.indexOf(criteriaLowerCase) + while (index >= 0) { + startPositions.add(index) + index = text.indexOf(criteriaLowerCase, index + 1) + } + return startPositions + } + + fun applyHighlightSpan( + spannableString: Spannable, indexes: List, length: Int + ) { + for (position in indexes) { + spannableString.setSpan( + HighlightSpan( + WoodColorUtil.HIGHLIGHT_BACKGROUND_COLOR, + WoodColorUtil.HIGHLIGHT_TEXT_COLOR, + WoodColorUtil.HIGHLIGHT_UNDERLINE + ), + position, + position + length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + + fun getShareText(transaction: Leaf): CharSequence { + return transaction.body().orEmpty() + } + + fun highlightSearchKeyword(textView: TextView, searchKey: String?): List { + val body = textView.text + if (body is Spannable) { + // remove old HighlightSpans + val spansToRemove = body.getSpans(0, body.length - 1, HighlightSpan::class.java) + for (span in spansToRemove) { + body.removeSpan(span) + } + // add spans only if searchKey size is > 0 + if (!searchKey.isNullOrEmpty()) { + // get indices of new search + val startIndexes = indexOf(body.toString(), searchKey) + // add spans + applyHighlightSpan(body, startIndexes, searchKey.length) + return startIndexes + } + } + + return ArrayList(0) + } + + fun timeDesc(nowInMilliseconds: Long): String { + val date = Date(nowInMilliseconds) + val formatter = SimpleDateFormat("HH:mm:ss.SSS MMM-dd", Locale.US) + return formatter.format(date) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.java b/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.java deleted file mode 100644 index a899aca..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.tonytangandroid.wood; - -import android.text.TextPaint; -import android.text.style.CharacterStyle; -import android.text.style.UpdateAppearance; -import androidx.annotation.ColorInt; - -class HighlightSpan extends CharacterStyle implements UpdateAppearance { - private final int mBackgroundColor; - private final int mTextColor; - private final boolean mUnderLineText; - private final boolean mApplyBackgroundColor; - private final boolean mApplyTextColor; - - HighlightSpan(int backgroundColor, @ColorInt int textColor, boolean underLineText) { - super(); - this.mBackgroundColor = backgroundColor; - this.mTextColor = textColor; - this.mUnderLineText = underLineText; - this.mApplyBackgroundColor = backgroundColor != 0; - this.mApplyTextColor = textColor != 0; - } - - @Override - public void updateDrawState(TextPaint ds) { - if (mApplyTextColor) ds.setColor(mTextColor); - if (mApplyBackgroundColor) ds.bgColor = mBackgroundColor; - ds.setUnderlineText(mUnderLineText); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt b/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt new file mode 100644 index 0000000..3cdc62e --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt @@ -0,0 +1,21 @@ +package com.tonytangandroid.wood + +import android.text.TextPaint +import android.text.style.CharacterStyle +import android.text.style.UpdateAppearance +import androidx.annotation.ColorInt + +internal class HighlightSpan( + private val mBackgroundColor: Int, + @param:ColorInt private val mTextColor: Int, + private val mUnderLineText: Boolean +) : CharacterStyle(), UpdateAppearance { + private val mApplyBackgroundColor = mBackgroundColor != 0 + private val mApplyTextColor = mTextColor != 0 + + override fun updateDrawState(ds: TextPaint) { + if (mApplyTextColor) ds.color = mTextColor + if (mApplyBackgroundColor) ds.bgColor = mBackgroundColor + ds.isUnderlineText = mUnderLineText + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.java b/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.java deleted file mode 100644 index b4524a7..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.tonytangandroid.wood; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -class JobExecutor implements Executor { - - static final int INITIAL_POOL_SIZE = 2; - static final int MAX_POOL_SIZE = 5; - static final int KEEP_ALIVE_TIME = 3; - static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; - - ThreadPoolExecutor threadPoolExecutor; - - public JobExecutor() { - BlockingQueue workQueue = new LinkedBlockingQueue<>(); - this.threadPoolExecutor = - new ThreadPoolExecutor( - INITIAL_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - KEEP_ALIVE_TIME_UNIT, - workQueue, - new JobThreadFactory()); - } - - @Override - public void execute(Runnable runnable) { - this.threadPoolExecutor.execute(runnable); - } - - static class JobThreadFactory implements ThreadFactory { - static final String THREAD_NAME = "log_"; - private int counter = 0; - - @Override - public Thread newThread(Runnable runnable) { - return new Thread(runnable, THREAD_NAME + counter++); - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt b/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt new file mode 100644 index 0000000..fc5e4cb --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt @@ -0,0 +1,48 @@ +package com.tonytangandroid.wood + +import java.util.concurrent.BlockingQueue +import java.util.concurrent.Executor +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadFactory +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +internal class JobExecutor : Executor { + private var threadPoolExecutor: ThreadPoolExecutor + + init { + val workQueue: BlockingQueue = LinkedBlockingQueue() + this.threadPoolExecutor = + ThreadPoolExecutor( + INITIAL_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME.toLong(), + KEEP_ALIVE_TIME_UNIT, + workQueue, + JobThreadFactory() + ) + } + + override fun execute(runnable: Runnable) { + threadPoolExecutor.execute(runnable) + } + + internal class JobThreadFactory : ThreadFactory { + private var counter = 0 + + override fun newThread(runnable: Runnable): Thread { + return Thread(runnable, THREAD_NAME + counter++) + } + + companion object { + const val THREAD_NAME: String = "log_" + } + } + + companion object { + private const val INITIAL_POOL_SIZE: Int = 2 + private const val MAX_POOL_SIZE: Int = 5 + private const val KEEP_ALIVE_TIME: Int = 3 + private val KEEP_ALIVE_TIME_UNIT: TimeUnit = TimeUnit.SECONDS + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Leaf.java b/wood/src/main/java/com/tonytangandroid/wood/Leaf.java deleted file mode 100644 index 7a8b2a7..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Leaf.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.tonytangandroid.wood; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.PrimaryKey; - -@Entity(tableName = "Leaf") -class Leaf { - - @Ignore public String searchKey; - - @PrimaryKey(autoGenerate = true) - private long id; - - @ColumnInfo(name = "createAt") - private long createAt; - - @ColumnInfo(name = "tag") - private String tag; - - @ColumnInfo(name = "priority") - private int priority; - - @ColumnInfo(name = "length") - private int length; - - @ColumnInfo(name = "body", typeAffinity = ColumnInfo.TEXT) - private String body; - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public long getCreateAt() { - return createAt; - } - - public void setCreateAt(long createAt) { - this.createAt = createAt; - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - - public int length() { - return length; - } - - public void setLength(int length) { - this.length = length; - } - - public String body() { - return body; - } - - public void setBody(String body) { - this.body = body; - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt b/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt new file mode 100644 index 0000000..e32e3eb --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt @@ -0,0 +1,51 @@ +package com.tonytangandroid.wood + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey + +@Entity(tableName = "Leaf") +class Leaf { + @JvmField + @Ignore + var searchKey: String? = null + + @JvmField + @PrimaryKey(autoGenerate = true) + var id: Long = 0 + + @JvmField + @ColumnInfo(name = "createAt") + var createAt: Long = 0 + + @JvmField + @ColumnInfo(name = "tag") + var tag: String? = null + + @JvmField + @ColumnInfo(name = "priority") + var priority: Int = 0 + + @ColumnInfo(name = "length") + private var length = 0 + + @ColumnInfo(name = "body", typeAffinity = ColumnInfo.TEXT) + private var body: String? = null + + fun length(): Int { + return length + } + + fun setLength(length: Int) { + this.length = length + } + + fun body(): String? { + return body + } + + fun setBody(body: String?) { + this.body = body + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.java b/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.java deleted file mode 100644 index 9ba9265..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.paging.PagedListAdapter; -import androidx.recyclerview.widget.RecyclerView; -import javax.inject.Provider; - -class LeafAdapter extends PagedListAdapter implements Provider { - - private final Context context; - private final Listener listener; - private final LayoutInflater layoutInflater; - - private String searchKey; - - LeafAdapter(Context context, ListDiffUtil listDiffUtil, Listener listener) { - super(listDiffUtil); - this.context = context; - this.listener = listener; - layoutInflater = LayoutInflater.from(this.context); - registerAdapterDataObserver( - new RecyclerView.AdapterDataObserver() { - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - super.onItemRangeInserted(positionStart, itemCount); - // in the database inserts only occur at the top - LeafAdapter.this.listener.onItemsInserted(positionStart); - } - }); - } - - LeafAdapter setSearchKey(String searchKey) { - this.searchKey = searchKey; - return this; - } - - @NonNull - @Override - public LeafViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View itemView = layoutInflater.inflate(R.layout.wood_list_item_leaf, parent, false); - return new LeafViewHolder(itemView, context, this, listener); - } - - @Override - public void onBindViewHolder(@NonNull LeafViewHolder viewHolder, int position) { - Leaf transaction = getItem(position); - if (transaction != null) { - viewHolder.bind(transaction); - } - } - - @Override - public String get() { - return searchKey; - } - - interface Listener { - void onTransactionClicked(Leaf leaf); - - void onItemsInserted(int firstInsertedItemPosition); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.kt new file mode 100644 index 0000000..1041e6b --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafAdapter.kt @@ -0,0 +1,56 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import javax.inject.Provider + +internal class LeafAdapter( + private val context: Context, + listDiffUtil: ListDiffUtil, + private val listener: Listener +) : PagedListAdapter(listDiffUtil), Provider { + private val layoutInflater: LayoutInflater = LayoutInflater.from(this.context) + + private var searchKey: String? = null + + init { + registerAdapterDataObserver( + object : AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + // in the database inserts only occur at the top + listener.onItemsInserted(positionStart) + } + }) + } + + fun setSearchKey(searchKey: String?): LeafAdapter { + this.searchKey = searchKey + return this + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LeafViewHolder { + val itemView = layoutInflater.inflate(R.layout.wood_list_item_leaf, parent, false) + return LeafViewHolder(itemView, context, this, listener) + } + + override fun onBindViewHolder(viewHolder: LeafViewHolder, position: Int) { + val transaction = getItem(position) + if (transaction != null) { + viewHolder.bind(transaction) + } + } + + override fun get(): String? { + return searchKey + } + + internal interface Listener { + fun onTransactionClicked(leaf: Leaf) + + fun onItemsInserted(firstInsertedItemPosition: Int) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java deleted file mode 100644 index f837232..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.tonytangandroid.wood; - -import android.util.Log; -import androidx.annotation.IntRange; -import androidx.paging.DataSource; -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.RoomWarnings; -import io.reactivex.Flowable; - -@Dao -abstract class LeafDao { - public static final int SEARCH_DEFAULT = Log.VERBOSE; - - @Insert(onConflict = OnConflictStrategy.REPLACE) - public abstract long insertTransaction(Leaf leaf); - - @Delete - public abstract int deleteTransactions(Leaf... leaves); - - @Query("DELETE FROM Leaf WHERE createAt < :beforeDate") - public abstract int deleteTransactionsBefore(long beforeDate); - - @Query("DELETE FROM Leaf") - public abstract int clearAll(); - - @Query("SELECT * FROM Leaf ORDER BY id DESC") - public abstract DataSource.Factory getAllTransactions(); - - @Query("SELECT * FROM Leaf WHERE id = :id") - public abstract Flowable getTransactionsWithId(long id); - - public DataSource.Factory getAllTransactionsWith( - String key, @IntRange(from = 2, to = 7) int priority) { - String endWildCard = key + "%"; - String doubleSideWildCard = "%" + key + "%"; - return getAllTransactionsIncludeRequestResponse(endWildCard, doubleSideWildCard, priority); - } - - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - @Query( - "SELECT id,createAt,length,priority,body FROM Leaf WHERE (tag LIKE :endWildCard OR body LIKE :doubleWildCard) AND priority >= :priority ORDER BY id DESC") - abstract DataSource.Factory getAllTransactionsIncludeRequestResponse( - String endWildCard, String doubleWildCard, int priority); -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt new file mode 100644 index 0000000..41fc667 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt @@ -0,0 +1,49 @@ +package com.tonytangandroid.wood + +import android.util.Log +import androidx.annotation.IntRange +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Flowable + +@Dao +internal abstract class LeafDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertTransaction(leaf: Leaf?): Long + + @Delete + abstract fun deleteTransactions(vararg leaves: Leaf?): Int + + @Query(value = "DELETE FROM Leaf WHERE createAt < :beforeDate") + abstract fun deleteTransactionsBefore(beforeDate: Long): Int + + @Query(value = "DELETE FROM Leaf") + abstract fun clearAll(): Int + + @get:Query(value = "SELECT * FROM Leaf ORDER BY id DESC") + abstract val allTransactions: DataSource.Factory? + + @Query(value = "SELECT * FROM Leaf WHERE id = :id") + abstract fun getTransactionsWithId(id: Long): Flowable? + + fun getAllTransactionsWith( + key: String, @IntRange(from = 2, to = 7) priority: Int + ): DataSource.Factory? { + val endWildCard = "$key%" + val doubleSideWildCard = "%$key%" + return getAllTransactionsIncludeRequestResponse(endWildCard, doubleSideWildCard, priority) + } + + @Query(value = "SELECT id,createAt,length,priority,body FROM Leaf WHERE (tag LIKE :endWildCard OR body LIKE :doubleWildCard) AND priority >= :priority ORDER BY id DESC") + abstract fun getAllTransactionsIncludeRequestResponse( + endWildCard: String?, doubleWildCard: String?, priority: Int + ): DataSource.Factory? + + companion object { + const val SEARCH_DEFAULT: Int = Log.VERBOSE + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java deleted file mode 100644 index 571a6e3..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java +++ /dev/null @@ -1,334 +0,0 @@ -package com.tonytangandroid.wood; - -import static android.content.Context.CLIPBOARD_SERVICE; -import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; -import static com.tonytangandroid.wood.WoodColorUtil.SEARCHED_HIGHLIGHT_BACKGROUND_COLOR; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.os.Bundle; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextWatcher; -import android.text.style.BackgroundColorSpan; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.core.widget.NestedScrollView; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class LeafDetailFragment extends Fragment - implements View.OnClickListener, TextUtil.AsyncTextProvider, TextWatcher { - - private static final String ARG_ID = "arg_id"; - private final BackgroundColorSpan colorSpan = - new BackgroundColorSpan(SEARCHED_HIGHLIGHT_BACKGROUND_COLOR); - private long id; - private String searchKey; - private WoodColorUtil colorUtil; - private int currentSearchIndex; - private Leaf leaf; - private List searchIndexList = new ArrayList<>(0); - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private View search_bar; - private EditText et_key_word; - private TextView tv_search_count; - private AppCompatTextView tv_body; - private final Debouncer searchDebounce = new Debouncer<>(400, this::onSearchKeyEmitted); - private NestedScrollView nested_scroll_view; - private FloatingActionButton floating_action_button; - - public static LeafDetailFragment newInstance(long id) { - LeafDetailFragment fragment = new LeafDetailFragment(); - Bundle b = new Bundle(); - b.putLong(ARG_ID, id); - fragment.setArguments(b); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - assert getArguments() != null; - id = getArguments().getLong(ARG_ID); - colorUtil = WoodColorUtil.getInstance(getContext()); - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.wood_fragment_leaf_detail, container, false); - bindView(rootView); - return rootView; - } - - private void bindView(View rootView) { - tv_body = rootView.findViewById(R.id.wood_details_body); - nested_scroll_view = rootView.findViewById(R.id.wood_details_scroll_parent); - floating_action_button = rootView.findViewById(R.id.wood_details_search_fab); - search_bar = rootView.findViewById(R.id.wood_details_search_bar); - View searchBarPrev = rootView.findViewById(R.id.wood_details_search_prev); - View searchBarNext = rootView.findViewById(R.id.wood_details_search_next); - View searchBarClose = rootView.findViewById(R.id.wood_details_search_close); - et_key_word = rootView.findViewById(R.id.wood_details_search); - tv_search_count = rootView.findViewById(R.id.wood_details_search_count); - floating_action_button.setOnClickListener(this); - searchBarPrev.setOnClickListener(this); - searchBarNext.setOnClickListener(this); - searchBarClose.setOnClickListener(this); - et_key_word.addTextChangedListener(this); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - searchDebounce.consume(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) {} - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - observe(); - } - - private void observe() { - LeafDetailViewModel viewModel = create(); - viewModel - .getTransactionWithId(id) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(getViewLifecycleOwner()))) - .subscribe(this::transactionUpdated); - } - - private LeafDetailViewModel create() { - return new ViewModelProvider(requireActivity()).get(LeafDetailViewModel.class); - } - - private void transactionUpdated(Leaf transaction) { - this.leaf = transaction; - populateUI(); - } - - private void populateUI() { - int color = colorUtil.getTransactionColor(leaf); - floating_action_button.setBackgroundTintList(colorStateList(color)); - search_bar.setBackgroundColor(color); - et_key_word.setHint(R.string.wood_search_hint); - populateBody(); - } - - @NonNull - private ColorStateList colorStateList(int color) { - return new ColorStateList(new int[][] {new int[] {0}}, new int[] {color}); - } - - private void onSearchKeyEmitted(String searchKey) { - this.searchKey = searchKey; - updateUI(); - } - - private void updateUI() { - searchIndexList = FormatUtils.highlightSearchKeyword(tv_body, searchKey); - updateSearch(1, searchKey); - } - - private void populateBody() { - ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar(); - assert actionBar != null; - actionBar.setTitle(leaf.getTag()); - actionBar.setSubtitle(timeDesc(leaf)); - TextUtil.asyncSetText(executor, this); - } - - @NonNull - private String timeDesc(Leaf leaf) { - return FormatUtils.timeDesc(leaf.getCreateAt()); - } - - @Override - public CharSequence getText() { - CharSequence body = leaf.body(); - if (TextUtil.isNullOrWhiteSpace(body) || TextUtil.isNullOrWhiteSpace(searchKey)) { - return body; - } else { - List indexList = FormatUtils.indexOf(body, searchKey); - SpannableString spannableBody = new SpannableString(body); - FormatUtils.applyHighlightSpan(spannableBody, indexList, searchKey.length()); - searchIndexList = indexList; - return spannableBody; - } - } - - @Override - public AppCompatTextView getTextView() { - return tv_body; - } - - private void updateSearch(int targetIndex, String searchKey) { - List list = searchIndexList; - int size = list.size(); - targetIndex = adjustTargetIndex(targetIndex, size); - tv_search_count.setText(String.valueOf(targetIndex).concat("/").concat(String.valueOf(size))); - ((Spannable) tv_body.getText()).removeSpan(colorSpan); - if (targetIndex > 0) { - updateSpan(targetIndex, searchKey, list); - } - currentSearchIndex = targetIndex; - } - - private void updateSpan(int targetIndex, String searchKey, List list) { - int begin = list.get(targetIndex - 1); - int end = begin + searchKey.length(); - int lineNumber = tv_body.getLayout().getLineForOffset(begin); - ((Spannable) tv_body.getText()).setSpan(colorSpan, begin, end, SPAN_EXCLUSIVE_EXCLUSIVE); - int scrollToY = tv_body.getLayout().getLineTop(lineNumber); - nested_scroll_view.scrollTo(0, scrollToY); - } - - private int adjustTargetIndex(int targetIndex, int size) { - if (size == 0) { - targetIndex = 0; - } else { - if (targetIndex > size) { - targetIndex = 1; - } else if (targetIndex <= 0) { - targetIndex = size; - } - } - return targetIndex; - } - - private void showKeyboard() { - et_key_word.requestFocus(); - InputMethodManager imm = inputMethodManager(); - if (imm != null) { - imm.showSoftInput(et_key_word, InputMethodManager.SHOW_IMPLICIT); - } - } - - private InputMethodManager inputMethodManager() { - return (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - } - - private void hideKeyboard() { - InputMethodManager imm = inputMethodManager(); - if (imm != null) { - imm.hideSoftInputFromWindow(et_key_word.getWindowToken(), 0); - } - } - - @SuppressWarnings("deprecation") - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - if (!isVisibleToUser) { - hideKeyboard(); - } - super.setUserVisibleHint(isVisibleToUser); - } - - @Override - public void onClick(View v) { - int id = v.getId(); - if (id == R.id.wood_details_search_fab) { - showSearch(); - } else if (id == R.id.wood_details_search_close) { - clearSearch(); - } else if (id == R.id.wood_details_search_prev) { - updateSearch(currentSearchIndex - 1, searchKey); - } else if (id == R.id.wood_details_search_next) { - updateSearch(currentSearchIndex + 1, searchKey); - } - } - - private void clearSearch() { - if (TextUtil.isNullOrWhiteSpace(searchKey)) { - floating_action_button.show(); - search_bar.setVisibility(View.GONE); - nested_scroll_view.setPadding(0, 0, 0, nested_scroll_view.getBottom()); - hideKeyboard(); - } else { - et_key_word.setText(""); - } - } - - private void showSearch() { - floating_action_button.hide(); - search_bar.setVisibility(View.VISIBLE); - nested_scroll_view.setPadding( - 0, - getResources().getDimensionPixelSize(R.dimen.wood_search_bar_height), - 0, - nested_scroll_view.getBottom()); - showKeyboard(); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater menuInflater) { - menuInflater.inflate(R.menu.wood_details_menu, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - if (itemId == R.id.share_text) { - share(FormatUtils.getShareText(leaf)); - return true; - } else if (itemId == R.id.copy) { - copy(FormatUtils.getShareText(leaf)); - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - private void copy(CharSequence text) { - ClipboardManager clipboard = - (ClipboardManager) requireContext().getSystemService(CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("log", text); - clipboard.setPrimaryClip(clip); - Toast.makeText(requireContext(), "Copied", Toast.LENGTH_SHORT).show(); - } - - private void share(CharSequence content) { - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, content); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, null)); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt new file mode 100644 index 0000000..021849f --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt @@ -0,0 +1,343 @@ +package com.tonytangandroid.wood + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.Editable +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.text.TextWatcher +import android.text.style.BackgroundColorSpan +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.res.ResourcesCompat +import androidx.core.widget.NestedScrollView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.tonytangandroid.wood.FormatUtils.timeDesc +import com.tonytangandroid.wood.TextUtil.AsyncTextProvider +import com.tonytangandroid.wood.TextUtil.asyncSetText +import com.tonytangandroid.wood.TextUtil.isNullOrWhiteSpace +import com.uber.autodispose.AutoDispose +import com.uber.autodispose.FlowableSubscribeProxy +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, TextWatcher { + private val colorSpan = + BackgroundColorSpan(WoodColorUtil.SEARCHED_HIGHLIGHT_BACKGROUND_COLOR) + private var id: Long = 0 + private var searchKey: String? = null + private var colorUtil: WoodColorUtil? = null + private var currentSearchIndex = 0 + private var leaf: Leaf? = null + private var searchIndexList: List = listOf(0) + private val executor: ExecutorService = Executors.newSingleThreadExecutor() + private lateinit var search_bar: View + private lateinit var et_key_word: EditText + private lateinit var tv_search_count: TextView + private lateinit var tv_body: AppCompatTextView + private val searchDebounce = Debouncer(400, object: Callback { + override fun onEmit(event: String) { + onSearchKeyEmitted(event) + } + }) + private lateinit var nested_scroll_view: NestedScrollView + private lateinit var floating_action_button: FloatingActionButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + id = arguments?.getLong(ARG_ID) ?: 0L + } + + override fun onAttach(context: Context) { + super.onAttach(context) + colorUtil = WoodColorUtil.getInstance(context) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val rootView = inflater.inflate(R.layout.wood_fragment_leaf_detail, container, false) + bindView(rootView) + return rootView + } + + private fun bindView(rootView: View) { + tv_body = rootView.findViewById(R.id.wood_details_body) + nested_scroll_view = + rootView.findViewById(R.id.wood_details_scroll_parent) + floating_action_button = + rootView.findViewById(R.id.wood_details_search_fab) + search_bar = rootView.findViewById(R.id.wood_details_search_bar) + val searchBarPrev = rootView.findViewById(R.id.wood_details_search_prev) + val searchBarNext = rootView.findViewById(R.id.wood_details_search_next) + val searchBarClose = rootView.findViewById(R.id.wood_details_search_close) + et_key_word = rootView.findViewById(R.id.wood_details_search) + tv_search_count = rootView.findViewById(R.id.wood_details_search_count) + floating_action_button.setOnClickListener(this) + searchBarPrev.setOnClickListener(this) + searchBarNext.setOnClickListener(this) + searchBarClose.setOnClickListener(this) + et_key_word.addTextChangedListener(this) + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + searchDebounce.consume(s.toString()) + } + + override fun afterTextChanged(s: Editable?) {} + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observe() + } + + private fun observe() { + val viewModel = create() + viewModel + .getTransactionWithId(id)?.run { + subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .`as`?>( + AutoDispose.autoDisposable( + AndroidLifecycleScopeProvider.from(getViewLifecycleOwner()) + ) + ) + .subscribe(Consumer { transaction: Leaf? -> transaction?.let { transactionUpdated(it) }}) + } + } + + private fun create(): LeafDetailViewModel { + return ViewModelProvider(requireActivity()).get(LeafDetailViewModel::class.java) + } + + private fun transactionUpdated(transaction: Leaf) { + this.leaf = transaction + populateUI() + } + + private fun populateUI() { + val l = leaf + val color = if (l != null) { + colorUtil?.getTransactionColor(l) ?: ResourcesCompat.getColor(resources, R.color.wood_status_default, floating_action_button.context.theme) + } else { + resources.getColor(R.color.wood_status_default) + } + floating_action_button.backgroundTintList = colorStateList(color) + search_bar.setBackgroundColor(color) + et_key_word.setHint(R.string.wood_search_hint) + populateBody() + } + + private fun colorStateList(color: Int): ColorStateList { + return ColorStateList(arrayOf(intArrayOf(0)), intArrayOf(color)) + } + + private fun onSearchKeyEmitted(searchKey: String) { + this.searchKey = searchKey + updateUI() + } + + private fun updateUI() { + searchIndexList = FormatUtils.highlightSearchKeyword(tv_body, searchKey) + updateSearch(1, searchKey) + } + + private fun populateBody() { + val actionBar: ActionBar = checkNotNull((requireActivity() as AppCompatActivity).supportActionBar) + actionBar.title = leaf?.tag.orEmpty() + actionBar.subtitle = leaf?.let { timeDesc(it) }.orEmpty() + asyncSetText(executor, this) + } + + private fun timeDesc(leaf: Leaf): String { + return timeDesc(leaf.createAt) + } + + override fun getText(): CharSequence { + val body: CharSequence = leaf?.body().orEmpty() + if (isNullOrWhiteSpace(body) || isNullOrWhiteSpace(searchKey)) { + return body + } else { + val indexList: List = searchKey?.let { FormatUtils.indexOf(body, it) } ?: listOf() + val spannableBody = SpannableString(body) + FormatUtils.applyHighlightSpan(spannableBody, indexList, searchKey?.length ?: 0) + searchIndexList = indexList + return spannableBody + } + } + + override fun getTextView(): AppCompatTextView { + return tv_body + } + + private fun updateSearch(targetIndex: Int, searchKey: String?) { + var targetIndex = targetIndex + val list = searchIndexList + val size = list.size + targetIndex = adjustTargetIndex(targetIndex, size) + tv_search_count.text = "$targetIndex/$size" + (tv_body.getText() as Spannable).removeSpan(colorSpan) + if (targetIndex > 0) { + updateSpan(targetIndex, searchKey.orEmpty(), list) + } + currentSearchIndex = targetIndex + } + + private fun updateSpan(targetIndex: Int, searchKey: String, list: List) { + val begin: Int = list[targetIndex - 1] + val end = begin + searchKey.length + val lineNumber = tv_body.layout.getLineForOffset(begin) + (tv_body.getText() as Spannable).setSpan( + colorSpan, + begin, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + val scrollToY = tv_body.layout.getLineTop(lineNumber) + nested_scroll_view.scrollTo(0, scrollToY) + } + + private fun adjustTargetIndex(targetIndex: Int, size: Int): Int { + var targetIndex = targetIndex + if (size == 0) { + targetIndex = 0 + } else { + if (targetIndex > size) { + targetIndex = 1 + } else if (targetIndex <= 0) { + targetIndex = size + } + } + return targetIndex + } + + private fun showKeyboard() { + et_key_word.requestFocus() + val imm = inputMethodManager() + imm?.showSoftInput(et_key_word, InputMethodManager.SHOW_IMPLICIT) + } + + private fun inputMethodManager(): InputMethodManager? { + return requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + } + + private fun hideKeyboard() { + val imm = inputMethodManager() + imm?.hideSoftInputFromWindow(et_key_word.windowToken, 0) + } + + @Suppress("deprecation") + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + if (!isVisibleToUser) { + hideKeyboard() + } + super.setUserVisibleHint(isVisibleToUser) + } + + override fun onClick(v: View) { + val id = v.id + if (id == R.id.wood_details_search_fab) { + showSearch() + } else if (id == R.id.wood_details_search_close) { + clearSearch() + } else if (id == R.id.wood_details_search_prev) { + updateSearch(currentSearchIndex - 1, searchKey) + } else if (id == R.id.wood_details_search_next) { + updateSearch(currentSearchIndex + 1, searchKey) + } + } + + private fun clearSearch() { + if (isNullOrWhiteSpace(searchKey)) { + floating_action_button.show() + search_bar.visibility = View.GONE + nested_scroll_view.setPadding(0, 0, 0, nested_scroll_view.bottom) + hideKeyboard() + } else { + et_key_word.setText("") + } + } + + private fun showSearch() { + floating_action_button.hide() + search_bar.visibility = View.VISIBLE + nested_scroll_view.setPadding( + 0, + getResources().getDimensionPixelSize(R.dimen.wood_search_bar_height), + 0, + nested_scroll_view.bottom + ) + showKeyboard() + } + + override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.wood_details_menu, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val itemId = item.itemId + if (itemId == R.id.share_text) { + leaf?.let { share(FormatUtils.getShareText(it)) } + return true + } else if (itemId == R.id.copy) { + leaf?.let { copy(FormatUtils.getShareText(it)) } + return true + } else { + return super.onOptionsItemSelected(item) + } + } + + private fun copy(text: CharSequence?) { + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("log", text) + clipboard.setPrimaryClip(clip) + Toast.makeText(requireContext(), "Copied", Toast.LENGTH_SHORT).show() + } + + private fun share(content: CharSequence?) { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, content) + sendIntent.type = "text/plain" + startActivity(Intent.createChooser(sendIntent, null)) + } + + companion object { + private const val ARG_ID = "arg_id" + fun newInstance(id: Long): LeafDetailFragment { + val fragment = LeafDetailFragment() + val b = Bundle() + b.putLong(ARG_ID, id) + fragment.setArguments(b) + return fragment + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.java deleted file mode 100644 index cdefc69..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.Application; -import androidx.lifecycle.AndroidViewModel; -import io.reactivex.Flowable; - -public class LeafDetailViewModel extends AndroidViewModel { - private final LeafDao leafDao; - - public LeafDetailViewModel(Application application) { - super(application); - leafDao = WoodDatabase.getInstance(application).leafDao(); - } - - public Flowable getTransactionWithId(long id) { - return leafDao.getTransactionsWithId(id); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.kt new file mode 100644 index 0000000..1d910a1 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailViewModel.kt @@ -0,0 +1,14 @@ +package com.tonytangandroid.wood + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import com.tonytangandroid.wood.WoodDatabase.Companion.getInstance +import io.reactivex.Flowable + +class LeafDetailViewModel(application: Application) : AndroidViewModel(application) { + private val leafDao: LeafDao = getInstance(application).leafDao() + + fun getTransactionWithId(id: Long): Flowable? { + return leafDao.getTransactionsWithId(id) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.java deleted file mode 100644 index 81a46cf..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import com.google.android.material.appbar.AppBarLayout; - -public class LeafDetailsActivity extends AppCompatActivity { - - private static final String ARG_TRANSACTION_ID = "arg_transaction_id"; - private static final String ARG_PRIORITY = "arg_priority"; - - public static void start(Context context, long id, int priority) { - Intent intent = new Intent(context, LeafDetailsActivity.class); - intent.putExtra(ARG_TRANSACTION_ID, id); - intent.putExtra(ARG_PRIORITY, priority); - context.startActivity(intent); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.wood_activity_leaf_details); - long id = getIntent().getLongExtra(ARG_TRANSACTION_ID, 0); - int priority = getIntent().getIntExtra(ARG_PRIORITY, Log.VERBOSE); - WoodColorUtil colorUtil = WoodColorUtil.getInstance(this); - AppBarLayout appBarLayout = findViewById(R.id.wood_details_appbar); - appBarLayout.setBackgroundColor(colorUtil.getTransactionColor(priority)); - Toolbar toolbar = findViewById(R.id.wood_details_toolbar); - setSupportActionBar(toolbar); - final ActionBar actionBar = getSupportActionBar(); - assert actionBar != null; - actionBar.setDisplayHomeAsUpEnabled(true); - bindFragment(LeafDetailFragment.newInstance(id)); - } - - private void bindFragment(Fragment fragment) { - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.fl_fragment_holder, fragment) - .commit(); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt new file mode 100644 index 0000000..7c95162 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt @@ -0,0 +1,50 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import com.google.android.material.appbar.AppBarLayout +import com.tonytangandroid.wood.WoodColorUtil.Companion.getInstance + +class LeafDetailsActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.wood_activity_leaf_details) + + val id = intent.getLongExtra(ARG_TRANSACTION_ID, 0) + val priority = intent.getIntExtra(ARG_PRIORITY, Log.VERBOSE) + + val colorUtil = getInstance(this) + val appBarLayout = findViewById(R.id.wood_details_appbar) + appBarLayout.setBackgroundColor(colorUtil.getTransactionColor(priority)) + val toolbar = findViewById(R.id.wood_details_toolbar) + setSupportActionBar(toolbar) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + bindFragment(LeafDetailFragment.newInstance(id)) + } + + private fun bindFragment(fragment: Fragment) { + supportFragmentManager + .beginTransaction() + .replace(R.id.fl_fragment_holder, fragment) + .commit() + } + + companion object { + private const val ARG_TRANSACTION_ID = "arg_transaction_id" + private const val ARG_PRIORITY = "arg_priority" + + fun start(context: Context, id: Long, priority: Int) { + val intent = Intent(context, LeafDetailsActivity::class.java) + intent.putExtra(ARG_TRANSACTION_ID, id) + intent.putExtra(ARG_PRIORITY, priority) + context.startActivity(intent) + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.java b/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.java deleted file mode 100644 index 5a2258b..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.pm.ApplicationInfo; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; - -public class LeafListActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.wood_activity_leaf_list); - Toolbar toolbar = findViewById(R.id.wood_toolbar); - setSupportActionBar(toolbar); - toolbar.setSubtitle(getApplicationName()); - } - - private String getApplicationName() { - ApplicationInfo applicationInfo = getApplicationInfo(); - int stringId = applicationInfo.labelRes; - return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : getString(stringId); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.kt new file mode 100644 index 0000000..b7e5f21 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafListActivity.kt @@ -0,0 +1,21 @@ +package com.tonytangandroid.wood + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar + +class LeafListActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.wood_activity_leaf_list) + val toolbar = findViewById(R.id.wood_toolbar) + setSupportActionBar(toolbar) + toolbar.setSubtitle(getApplicationName()) + } + + private fun getApplicationName(): String { + val applicationInfo = getApplicationInfo() + val stringId = applicationInfo.labelRes + return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else getString(stringId) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java deleted file mode 100644 index 37fbbf8..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.tonytangandroid.wood; - -import android.app.Application; -import android.os.AsyncTask; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.paging.DataSource; -import androidx.paging.LivePagedListBuilder; -import androidx.paging.PagedList; - -@SuppressWarnings("deprecation") -public class LeafListViewModel extends AndroidViewModel { - private static final PagedList.Config config = - new PagedList.Config.Builder() - .setPageSize(15) // page size - .setInitialLoadSizeHint(30) // items to fetch on first load - .setPrefetchDistance(10) // trigger when to fetch a page - .setEnablePlaceholders(true) - .build(); - private final LeafDao leafDao; - private final LiveData> transactions; - - public LeafListViewModel(Application application) { - super(application); - leafDao = WoodDatabase.getInstance(application).leafDao(); - transactions = new LivePagedListBuilder<>(leafDao.getAllTransactions(), config).build(); - } - - LiveData> getTransactions(String key) { - if (key == null || key.trim().length() == 0) { - return transactions; - } else { - DataSource.Factory factory = - leafDao.getAllTransactionsWith(key, LeafDao.SEARCH_DEFAULT); - return new LivePagedListBuilder<>(factory, config).build(); - } - } - - public void deleteItem(Leaf transaction) { - new DeleteAsyncTask(leafDao).execute(transaction); - } - - void clearAll() { - new ClearAsyncTask(leafDao).execute(); - } - - private static class DeleteAsyncTask extends AsyncTask { - - private final LeafDao leafDao; - - DeleteAsyncTask(LeafDao leafDao) { - this.leafDao = leafDao; - } - - @Override - protected Integer doInBackground(final Leaf... params) { - return leafDao.deleteTransactions(params); - } - } - - private static class ClearAsyncTask extends AsyncTask { - - private final LeafDao leafDao; - - ClearAsyncTask(LeafDao leafDao) { - this.leafDao = leafDao; - } - - @Override - protected Integer doInBackground(final Leaf... params) { - return leafDao.clearAll(); - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt new file mode 100644 index 0000000..dfd75f5 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt @@ -0,0 +1,62 @@ +package com.tonytangandroid.wood + +import android.app.Application +import android.os.AsyncTask +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import androidx.paging.PagedList.Config +import com.tonytangandroid.wood.LeafListViewModel.ClearAsyncTask +import com.tonytangandroid.wood.LeafListViewModel.DeleteAsyncTask +import com.tonytangandroid.wood.WoodDatabase.Companion.getInstance + +@Suppress("deprecation") +class LeafListViewModel(application: Application) : AndroidViewModel(application) { + private var leafDao: LeafDao = getInstance(application).leafDao() + private var transactions: LiveData>? = leafDao.run { + allTransactions?.let { LivePagedListBuilder(it, config).build() } + } + + fun getTransactions(key: String?): LiveData>? { + return if (key == null || key.trim { it <= ' ' }.isEmpty()) { + transactions + } else { + val test = leafDao.getAllTransactionsWith(key, LeafDao.SEARCH_DEFAULT)?.run { + LivePagedListBuilder(this, config).build() + } + test + } + } + + fun deleteItem(transaction: Leaf) { + DeleteAsyncTask(leafDao).execute(transaction) + } + + fun clearAll() { + ClearAsyncTask(leafDao).execute() + } + + private class DeleteAsyncTask(private val leafDao: LeafDao) : AsyncTask() { + + override fun doInBackground(vararg params: Leaf?): Int { + return leafDao.deleteTransactions(*params) + } + } + + private class ClearAsyncTask(private val leafDao: LeafDao) : AsyncTask() { + + override fun doInBackground(vararg params: Leaf?): Int { + return leafDao.clearAll() + } + } + + companion object { + private val config: Config = Config.Builder() + .setPageSize(15) // page size + .setInitialLoadSizeHint(30) // items to fetch on first load + .setPrefetchDistance(10) // trigger when to fetch a page + .setEnablePlaceholders(true) + .build() + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.java b/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.java deleted file mode 100644 index c9f0ee3..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.view.View; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import javax.inject.Provider; - -class LeafViewHolder extends RecyclerView.ViewHolder { - - private final Context context; - private final Provider searchKey; - private final LeafAdapter.Listener listener; - - private final TextView tv_time; - private final TextView tv_tag; - private final TextView tv_id; - private final TextView tv_body; - - LeafViewHolder( - View itemView, Context context, Provider searchKey, LeafAdapter.Listener listener) { - super(itemView); - this.context = context; - this.listener = listener; - this.searchKey = searchKey; - tv_time = itemView.findViewById(R.id.tv_time); - tv_id = itemView.findViewById(R.id.tv_id); - tv_tag = itemView.findViewById(R.id.tv_tag); - tv_body = itemView.findViewById(R.id.tv_body); - } - - void bind(Leaf transaction) { - tv_tag.setText(transaction.getTag()); - tv_time.setText(FormatUtils.timeDesc(transaction.getCreateAt())); - tv_body.setText(getHighlightedText(String.valueOf(transaction.body()))); - tv_id.setText(String.valueOf(transaction.getId())); - tv_tag.setTextColor(WoodColorUtil.getInstance(context).getTransactionColor(transaction)); - itemView.setOnClickListener(v -> listener.onTransactionClicked(transaction)); - } - - private CharSequence getHighlightedText(String text) { - return FormatUtils.formatTextHighlight(text, searchKey.get()); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt new file mode 100644 index 0000000..95afa6f --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt @@ -0,0 +1,40 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.tonytangandroid.wood.FormatUtils.timeDesc +import com.tonytangandroid.wood.WoodColorUtil.Companion.getInstance +import java.util.Locale +import javax.inject.Provider + +internal class LeafViewHolder( + itemView: View, + private val context: Context, + private val searchKey: Provider, + private val listener: LeafAdapter.Listener +) : RecyclerView.ViewHolder(itemView) { + + private val tv_time: TextView = itemView.findViewById(R.id.tv_time) + private val tv_tag: TextView = itemView.findViewById(R.id.tv_tag) + private val tv_id: TextView = itemView.findViewById(R.id.tv_id) + private val tv_body: TextView = itemView.findViewById(R.id.tv_body) + + fun bind(transaction: Leaf) { + tv_tag.text = transaction.tag + tv_time.text = timeDesc(transaction.createAt) + tv_body.text = getHighlightedText(transaction.body().toString()) + tv_id.text = String.format(Locale.getDefault(), "%d", transaction.id) + tv_tag.setTextColor(getInstance(context).getTransactionColor(transaction)) + itemView.setOnClickListener(View.OnClickListener { v: View? -> + listener.onTransactionClicked( + transaction + ) + }) + } + + private fun getHighlightedText(text: String): CharSequence { + return FormatUtils.formatTextHighlight(text, searchKey.get().orEmpty()) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java deleted file mode 100644 index acca72a..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.tonytangandroid.wood; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModelProvider; -import androidx.paging.PagedList; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class LeavesCollectionFragment extends Fragment - implements LeafAdapter.Listener, SearchView.OnQueryTextListener { - - private LeafAdapter adapter; - private ListDiffUtil listDiffUtil; - private RecyclerView recyclerView; - private LeafListViewModel viewModel; - private LiveData> currentSubscription; - - // 100 mills delay. batch all changes in 100 mills and emit last item at the end of 100 mills - private final Sampler transactionSampler = - new Sampler<>( - 100, - new Callback() { - @Override - public void onEmit(TransactionListWithSearchKeyModel event) { - listDiffUtil.setSearchKey(event.searchKey); - adapter.setSearchKey(event.searchKey).submitList(event.pagedList); - } - }); - - // 300 mills delay min. Max no limit - private final Debouncer searchDebounce = - new Debouncer<>( - 300, - new Callback() { - @Override - public void onEmit(String event) { - loadResults(event, viewModel.getTransactions(event)); - } - }); - - @Nullable - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.wood_fragment_leaves_collection, container, false); - bindView(rootView); - return rootView; - } - - private void bindView(View rootView) { - recyclerView = rootView.findViewById(R.id.wood_transaction_list); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - listDiffUtil = new ListDiffUtil(); - adapter = new LeafAdapter(requireContext(), listDiffUtil, this); - recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); - recyclerView.addItemDecoration( - new DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)); - recyclerView.setAdapter(adapter); - - viewModel = new ViewModelProvider(this).get(LeafListViewModel.class); - loadResults(null, viewModel.getTransactions(null)); - } - - private void loadResults(final String searchKey, LiveData> pagedListLiveData) { - if (currentSubscription != null && currentSubscription.hasObservers()) { - currentSubscription.removeObservers(this); - } - currentSubscription = pagedListLiveData; - currentSubscription.observe(getViewLifecycleOwner(), list -> consume(list, searchKey)); - } - - private void consume(@Nullable PagedList transactionPagedList, String searchKey) { - transactionSampler.consume( - new TransactionListWithSearchKeyModel(searchKey, transactionPagedList)); - } - - @Override - public void onTransactionClicked(Leaf transaction) { - LeafDetailsActivity.start(requireContext(), transaction.getId(), transaction.getPriority()); - } - - @Override - public void onItemsInserted(int firstInsertedItemPosition) { - if (WoodTree.autoScroll(requireContext())) { - recyclerView.smoothScrollToPosition(firstInsertedItemPosition); - } - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater menuInflater) { - menuInflater.inflate(R.menu.wood_list_menu, menu); - MenuItem searchMenuItem = menu.findItem(R.id.search); - SearchView searchView = (SearchView) searchMenuItem.getActionView(); - searchView.setOnQueryTextListener(this); - searchView.setIconifiedByDefault(true); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.clear) { - viewModel.clearAll(); - NotificationHelper.clearBuffer(); - return true; - } else if (item.getItemId() == R.id.browse_sql) { - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - @Override - public boolean onQueryTextSubmit(String query) { - return false; - } - - @Override - public boolean onQueryTextChange(String newText) { - searchDebounce.consume(newText); - return true; - } - - static class TransactionListWithSearchKeyModel { - final String searchKey; - final PagedList pagedList; - - TransactionListWithSearchKeyModel(String searchKey, PagedList pagedList) { - this.searchKey = searchKey; - this.pagedList = pagedList; - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt new file mode 100644 index 0000000..a25f811 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt @@ -0,0 +1,141 @@ +package com.tonytangandroid.wood + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.paging.PagedList +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.tonytangandroid.wood.LeavesCollectionFragment.TransactionListWithSearchKeyModel +import com.tonytangandroid.wood.NotificationHelper.Companion.clearBuffer +import com.tonytangandroid.wood.WoodTree.Companion.autoScroll + +class LeavesCollectionFragment : Fragment(), LeafAdapter.Listener, SearchView.OnQueryTextListener { + private var adapter: LeafAdapter? = null + private var listDiffUtil: ListDiffUtil? = null + private lateinit var recyclerView: RecyclerView + private var viewModel: LeafListViewModel? = null + private var currentSubscription: LiveData>? = null + + // 100 mills delay. batch all changes in 100 mills and emit last item at the end of 100 mills + private val transactionSampler = Sampler( + 100, + object : Callback { + override fun onEmit(event: TransactionListWithSearchKeyModel?) { + listDiffUtil?.setSearchKey(event?.searchKey) + adapter?.setSearchKey(event?.searchKey)?.submitList(event?.pagedList) + } + }) + + // 300 mills delay min. Max no limit + private val searchDebounce = Debouncer( + 300, + object : Callback { + override fun onEmit(event: String?) { + loadResults(event, viewModel?.getTransactions(event)) + + } + }) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.wood_fragment_leaves_collection, container, false) + bindView(rootView) + return rootView + } + + private fun bindView(rootView: View) { + recyclerView = rootView.findViewById(R.id.wood_transaction_list) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + adapter = LeafAdapter(requireContext(), ListDiffUtil().also { listDiffUtil = it }, this) + recyclerView.setLayoutManager(LinearLayoutManager(requireContext())) + recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) + recyclerView.setAdapter(adapter) + + viewModel = ViewModelProvider(this).get(LeafListViewModel::class.java) + loadResults(null, viewModel?.getTransactions(null)) + } + + private fun loadResults(searchKey: String?, pagedListLiveData: LiveData>?) { + currentSubscription?.takeIf { it.hasObservers() == true }?.run { + removeObservers(this@LeavesCollectionFragment) + } + currentSubscription = pagedListLiveData + currentSubscription?.observe( + getViewLifecycleOwner(), + Observer { list: PagedList? -> consume(list, searchKey) }) + } + + private fun consume(transactionPagedList: PagedList?, searchKey: String?) { + transactionSampler.consume( + TransactionListWithSearchKeyModel(searchKey, transactionPagedList) + ) + } + + + override fun onTransactionClicked(transaction: Leaf) { + LeafDetailsActivity.start(requireContext(), transaction.id, transaction.priority) + } + + override fun onItemsInserted(firstInsertedItemPosition: Int) { + if (autoScroll(requireContext())) { + recyclerView.smoothScrollToPosition(firstInsertedItemPosition) + } + } + + override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.wood_list_menu, menu) + val searchMenuItem = menu.findItem(R.id.search) + (searchMenuItem.actionView as? SearchView)?.run { + setOnQueryTextListener(this@LeavesCollectionFragment) + setIconifiedByDefault(true) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.clear) { + viewModel!!.clearAll() + clearBuffer() + return true + } else if (item.itemId == R.id.browse_sql) { + return true + } else { + return super.onOptionsItemSelected(item) + } + } + + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + searchDebounce.consume(newText) + return true + } + + internal class TransactionListWithSearchKeyModel( + val searchKey: String?, + val pagedList: PagedList? + ) +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.java b/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.java deleted file mode 100644 index 31f583f..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.tonytangandroid.wood; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.DiffUtil; - -class ListDiffUtil extends DiffUtil.ItemCallback { - - private String mSearchKey; - - private static boolean areEqual(@Nullable Object oldItem, @Nullable Object newItem) { - if (oldItem == null && newItem == null) { - // both are null - return true; - } else if (oldItem == null || newItem == null) { - // only one is null => return false - return false; - } - return oldItem.equals(newItem); - } - - void setSearchKey(String searchKey) { - this.mSearchKey = searchKey; - } - - @Override - public boolean areItemsTheSame(@NonNull Leaf oldItem, @NonNull Leaf newItem) { - // might not work always due to async nature of Adapter fails in very rare race conditions but - // increases pref. - newItem.searchKey = mSearchKey; - return oldItem.getId() == newItem.getId(); - } - - @Override - public boolean areContentsTheSame(@NonNull Leaf oldItem, @NonNull Leaf newItem) { - // both will non null. because of areItemsTheSame logic only non nulls come here - // comparing only items shown in the list - return areEqual(oldItem.searchKey, newItem.searchKey); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.kt b/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.kt new file mode 100644 index 0000000..fc42924 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/ListDiffUtil.kt @@ -0,0 +1,37 @@ +package com.tonytangandroid.wood + +import androidx.recyclerview.widget.DiffUtil + +internal class ListDiffUtil : DiffUtil.ItemCallback() { + private var mSearchKey: String? = null + + fun setSearchKey(searchKey: String?) { + this.mSearchKey = searchKey + } + + override fun areItemsTheSame(oldItem: Leaf, newItem: Leaf): Boolean { + // might not work always due to async nature of Adapter fails in very rare race conditions but + // increases pref. + newItem.searchKey = mSearchKey + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Leaf, newItem: Leaf): Boolean { + // both will non null. because of areItemsTheSame logic only non nulls come here + // comparing only items shown in the list + return areEqual(oldItem.searchKey, newItem.searchKey) + } + + companion object { + private fun areEqual(oldItem: Any?, newItem: Any?): Boolean { + if (oldItem == null && newItem == null) { + // both are null + return true + } else if (oldItem == null || newItem == null) { + // only one is null => return false + return false + } + return oldItem == newItem + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Logger.java b/wood/src/main/java/com/tonytangandroid/wood/Logger.java deleted file mode 100644 index 08cd7d6..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Logger.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.tonytangandroid.wood; - -import android.annotation.SuppressLint; -import android.util.Log; - -@SuppressLint("LogNotTimber") -class Logger { - private static final String LOG_TAG = "WoodTree"; - - public static void i(String message) { - Log.i(LOG_TAG, message); - } - - public static void w(String message) { - Log.w(LOG_TAG, message); - } - - public static void e(String message, Exception e) { - Log.e(LOG_TAG, message, e); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Logger.kt b/wood/src/main/java/com/tonytangandroid/wood/Logger.kt new file mode 100644 index 0000000..1b1ddfc --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Logger.kt @@ -0,0 +1,22 @@ +package com.tonytangandroid.wood + +import android.annotation.SuppressLint +import android.util.Log +import java.lang.Exception + +@SuppressLint("LogNotTimber") +internal object Logger { + private const val LOG_TAG = "WoodTree" + + fun i(message: String) { + Log.i(LOG_TAG, message) + } + + fun w(message: String) { + Log.w(LOG_TAG, message) + } + + fun e(message: String?, e: Exception?) { + Log.e(LOG_TAG, message, e) + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.java b/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.java deleted file mode 100644 index b48b67c..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.tonytangandroid.wood; - -import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE; - -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.util.LongSparseArray; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; -import com.tony.tang.safe.pending.intent.sdk.SafePendingIntent; - -class NotificationHelper { - - private static final String CHANNEL_ID = "wood_notification_log_channel"; - private static final int BUFFER_SIZE = 10; - - private static final LongSparseArray TRANSACTION_BUFFER = new LongSparseArray<>(); - private static int TRANSACTION_COUNT; - - private final Context mContext; - private final NotificationManager mNotificationManager; - private final WoodColorUtil mColorUtil; - - public NotificationHelper(Context context) { - this.mContext = context; - mNotificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - mColorUtil = WoodColorUtil.getInstance(context); - setUpChannelIfNecessary(); - } - - public static synchronized void clearBuffer() { - TRANSACTION_BUFFER.clear(); - TRANSACTION_COUNT = 0; - } - - private static synchronized void addToBuffer(Leaf transaction) { - TRANSACTION_COUNT++; - TRANSACTION_BUFFER.put(transaction.getId(), transaction); - if (TRANSACTION_BUFFER.size() > BUFFER_SIZE) { - TRANSACTION_BUFFER.removeAt(0); - } - } - - private void setUpChannelIfNecessary() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = - new NotificationChannel( - CHANNEL_ID, - mContext.getString(R.string.wood_notification_category), - NotificationManager.IMPORTANCE_LOW); - channel.setShowBadge(false); - - mNotificationManager.createNotificationChannel(channel); - } - } - - public synchronized void show(Leaf transaction, boolean stickyNotification) { - addToBuffer(transaction); - NotificationCompat.Builder builder = - new NotificationCompat.Builder(mContext, CHANNEL_ID) - .setContentIntent( - SafePendingIntent.getActivity(mContext, 0, Wood.getLaunchIntent(mContext), 0)) - .setLocalOnly(true) - .setSmallIcon(R.drawable.wood_icon) - .setColor(ContextCompat.getColor(mContext, R.color.wood_colorPrimary)) - .setOngoing(stickyNotification) - .setContentTitle(mContext.getString(R.string.wood_notification_title)); - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - - int count = 0; - for (int i = TRANSACTION_BUFFER.size() - 1; i >= 0; i--) { - if (count < BUFFER_SIZE) { - if (count == 0) { - builder.setContentText(getNotificationText(TRANSACTION_BUFFER.valueAt(i))); - } - inboxStyle.addLine(getNotificationText(TRANSACTION_BUFFER.valueAt(i))); - } - count++; - } - builder.setAutoCancel(true); - builder.setStyle(inboxStyle); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.setSubText(String.valueOf(TRANSACTION_COUNT)); - } else { - builder.setNumber(TRANSACTION_COUNT); - } - builder.addAction(getDismissAction()); - builder.addAction(getClearAction()); - mNotificationManager.notify(CHANNEL_ID.hashCode(), builder.build()); - } - - private CharSequence getNotificationText(Leaf transaction) { - int color = mColorUtil.getTransactionColor(transaction); - String text = transaction.body(); - // Simple span no Truss required - SpannableString spannableString = new SpannableString(text); - spannableString.setSpan( - new ForegroundColorSpan(color), 0, text.length(), SPAN_INCLUSIVE_EXCLUSIVE); - return spannableString; - } - - @NonNull - private NotificationCompat.Action getClearAction() { - CharSequence clearTitle = mContext.getString(R.string.wood_clear); - Intent deleteIntent = new Intent(mContext, ClearTransactionsService.class); - PendingIntent intent = - SafePendingIntent.getService(mContext, 11, deleteIntent, PendingIntent.FLAG_ONE_SHOT); - return new NotificationCompat.Action(R.drawable.wood_ic_delete_white_24dp, clearTitle, intent); - } - - @NonNull - private NotificationCompat.Action getDismissAction() { - CharSequence dismissTitle = mContext.getString(R.string.wood_dismiss); - Intent dismissIntent = new Intent(mContext, DismissNotificationService.class); - PendingIntent intent = - SafePendingIntent.getService(mContext, 12, dismissIntent, PendingIntent.FLAG_ONE_SHOT); - return new NotificationCompat.Action(0, dismissTitle, intent); - } - - public void dismiss() { - mNotificationManager.cancel(CHANNEL_ID.hashCode()); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt b/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt new file mode 100644 index 0000000..b3f7eda --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt @@ -0,0 +1,133 @@ +package com.tonytangandroid.wood + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.util.LongSparseArray +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.tony.tang.safe.pending.intent.sdk.SafePendingIntent +import com.tonytangandroid.wood.Wood.getLaunchIntent +import com.tonytangandroid.wood.WoodColorUtil.Companion.getInstance + +internal class NotificationHelper(context: Context) { + private val mContext: Context = context + private val mNotificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val mColorUtil: WoodColorUtil = getInstance(context) + + init { + setUpChannelIfNecessary() + } + + private fun setUpChannelIfNecessary() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + CHANNEL_ID, + mContext.getString(R.string.wood_notification_category), + NotificationManager.IMPORTANCE_LOW + ) + channel.setShowBadge(false) + + mNotificationManager.createNotificationChannel(channel) + } + } + + @Synchronized + fun show(transaction: Leaf, stickyNotification: Boolean) { + addToBuffer(transaction) + val builder = + NotificationCompat.Builder(mContext, CHANNEL_ID) + .setContentIntent( + SafePendingIntent.getActivity(mContext, 0, getLaunchIntent(mContext), 0) + ) + .setLocalOnly(true) + .setSmallIcon(R.drawable.wood_icon) + .setColor(ContextCompat.getColor(mContext, R.color.wood_colorPrimary)) + .setOngoing(stickyNotification) + .setContentTitle(mContext.getString(R.string.wood_notification_title)) + val inboxStyle = NotificationCompat.InboxStyle() + + var count = 0 + for (i in TRANSACTION_BUFFER.size() - 1 downTo 0) { + if (count < BUFFER_SIZE) { + if (count == 0) { + builder.setContentText(getNotificationText(TRANSACTION_BUFFER.valueAt(i)!!)) + } + inboxStyle.addLine(getNotificationText(TRANSACTION_BUFFER.valueAt(i)!!)) + } + count++ + } + builder.setAutoCancel(true) + builder.setStyle(inboxStyle) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setSubText(TRANSACTION_COUNT.toString()) + } else { + builder.setNumber(TRANSACTION_COUNT) + } + builder.addAction(getDismissAction()) + builder.addAction(getClearAction()) + mNotificationManager.notify(CHANNEL_ID.hashCode(), builder.build()) + } + + private fun getNotificationText(transaction: Leaf): CharSequence { + val color = mColorUtil.getTransactionColor(transaction) + val text = transaction.body() + // Simple span no Truss required + val spannableString = SpannableString(text) + spannableString.setSpan( + ForegroundColorSpan(color), 0, text!!.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + return spannableString + } + + private fun getClearAction(): NotificationCompat.Action { + val clearTitle: CharSequence = mContext.getString(R.string.wood_clear) + val deleteIntent = Intent(mContext, ClearTransactionsService::class.java) + val intent = + SafePendingIntent.getService(mContext, 11, deleteIntent, PendingIntent.FLAG_ONE_SHOT) + return NotificationCompat.Action(R.drawable.wood_ic_delete_white_24dp, clearTitle, intent) + } + + private fun getDismissAction(): NotificationCompat.Action { + val dismissTitle: CharSequence = mContext.getString(R.string.wood_dismiss) + val dismissIntent = Intent(mContext, DismissNotificationService::class.java) + val intent = + SafePendingIntent.getService(mContext, 12, dismissIntent, PendingIntent.FLAG_ONE_SHOT) + return NotificationCompat.Action(0, dismissTitle, intent) + } + + fun dismiss() { + mNotificationManager.cancel(CHANNEL_ID.hashCode()) + } + + companion object { + private const val CHANNEL_ID = "wood_notification_log_channel" + private const val BUFFER_SIZE = 10 + + private val TRANSACTION_BUFFER = LongSparseArray() + private var TRANSACTION_COUNT = 0 + + @JvmStatic + @Synchronized + fun clearBuffer() { + TRANSACTION_BUFFER.clear() + TRANSACTION_COUNT = 0 + } + + @Synchronized + private fun addToBuffer(transaction: Leaf) { + TRANSACTION_COUNT++ + TRANSACTION_BUFFER.put(transaction.id, transaction) + if (TRANSACTION_BUFFER.size() > BUFFER_SIZE) { + TRANSACTION_BUFFER.removeAt(0) + } + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.java b/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.java deleted file mode 100644 index d2804ab..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.content.SharedPreferences; -import java.util.Date; -import java.util.concurrent.TimeUnit; - -class RetentionManager { - private static final String PREFS_NAME = "wood_preferences"; - private static final String KEY_LAST_CLEANUP = "last_cleanup"; - - private static long LAST_CLEAN_UP; - - private final WoodDatabase woodDatabase; - private final long period; - private final long cleanupFrequency; - private final SharedPreferences prefs; - - public RetentionManager(Context context, WoodTree.Period retentionPeriod) { - this.woodDatabase = WoodDatabase.getInstance(context); - period = toMillis(retentionPeriod); - prefs = context.getSharedPreferences(PREFS_NAME, 0); - cleanupFrequency = - (retentionPeriod == WoodTree.Period.ONE_HOUR) - ? TimeUnit.MINUTES.toMillis(30) - : TimeUnit.HOURS.toMillis(2); - } - - public synchronized void doMaintenance() { - if (period > 0) { - long now = new Date().getTime(); - if (isCleanupDue(now)) { - Logger.i("Performing data retention maintenance..."); - deleteSince(getThreshold(now)); - updateLastCleanup(now); - } - } - } - - private long getLastCleanup(long fallback) { - if (LAST_CLEAN_UP == 0) { - LAST_CLEAN_UP = prefs.getLong(KEY_LAST_CLEANUP, fallback); - } - return LAST_CLEAN_UP; - } - - private void updateLastCleanup(long time) { - LAST_CLEAN_UP = time; - prefs.edit().putLong(KEY_LAST_CLEANUP, time).apply(); - } - - private void deleteSince(long threshold) { - long rows = woodDatabase.leafDao().deleteTransactionsBefore(threshold); - Logger.i(rows + " transactions deleted"); - } - - private boolean isCleanupDue(long now) { - return (now - getLastCleanup(now)) > cleanupFrequency; - } - - private long getThreshold(long now) { - return (period == 0) ? now : now - period; - } - - private long toMillis(WoodTree.Period period) { - switch (period) { - case ONE_HOUR: - return TimeUnit.HOURS.toMillis(1); - case ONE_DAY: - return TimeUnit.DAYS.toMillis(1); - case ONE_WEEK: - return TimeUnit.DAYS.toMillis(7); - default: - return 0; - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt b/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt new file mode 100644 index 0000000..8242d82 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt @@ -0,0 +1,77 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.content.SharedPreferences +import com.tonytangandroid.wood.WoodDatabase.Companion.getInstance +import java.util.Date +import java.util.concurrent.TimeUnit + +internal class RetentionManager(context: Context, retentionPeriod: WoodTree.Period) { + private val woodDatabase: WoodDatabase = getInstance(context) + private val period: Long + private val cleanupFrequency: Long + private val prefs: SharedPreferences + + init { + period = toMillis(retentionPeriod) + prefs = context.getSharedPreferences(PREFS_NAME, 0) + cleanupFrequency = + if ((retentionPeriod == WoodTree.Period.ONE_HOUR)) + TimeUnit.MINUTES.toMillis(30) + else + TimeUnit.HOURS.toMillis(2) + } + + @Synchronized + fun doMaintenance() { + if (period > 0) { + val now = Date().getTime() + if (isCleanupDue(now)) { + Logger.i("Performing data retention maintenance...") + deleteSince(getThreshold(now)) + updateLastCleanup(now) + } + } + } + + private fun getLastCleanup(fallback: Long): Long { + if (LAST_CLEAN_UP == 0L) { + LAST_CLEAN_UP = prefs.getLong(KEY_LAST_CLEANUP, fallback) + } + return LAST_CLEAN_UP + } + + private fun updateLastCleanup(time: Long) { + LAST_CLEAN_UP = time + prefs.edit().putLong(KEY_LAST_CLEANUP, time).apply() + } + + private fun deleteSince(threshold: Long) { + val rows = woodDatabase.leafDao()!!.deleteTransactionsBefore(threshold).toLong() + Logger.i("$rows transactions deleted") + } + + private fun isCleanupDue(now: Long): Boolean { + return (now - getLastCleanup(now)) > cleanupFrequency + } + + private fun getThreshold(now: Long): Long { + return if ((period == 0L)) now else now - period + } + + private fun toMillis(period: WoodTree.Period): Long { + when (period) { + WoodTree.Period.ONE_HOUR -> return TimeUnit.HOURS.toMillis(1) + WoodTree.Period.ONE_DAY -> return TimeUnit.DAYS.toMillis(1) + WoodTree.Period.ONE_WEEK -> return TimeUnit.DAYS.toMillis(7) + else -> return 0 + } + } + + companion object { + private const val PREFS_NAME = "wood_preferences" + private const val KEY_LAST_CLEANUP = "last_cleanup" + + private var LAST_CLEAN_UP: Long = 0 + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Sampler.java b/wood/src/main/java/com/tonytangandroid/wood/Sampler.java deleted file mode 100644 index 277aa16..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Sampler.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.tonytangandroid.wood; - -import android.os.Handler; -import androidx.annotation.NonNull; - -class Sampler { - - private final int interval; - private final Callback callback; - private final Handler handler; - - private Counter currentRunnable; - - public Sampler(int intervalInMills, @NonNull Callback callback) { - interval = intervalInMills; - this.callback = callback; - handler = new Handler(); - } - - public void consume(V event) { - if (currentRunnable == null) { - // first runnable - currentRunnable = new Counter<>(event, callback); - handler.postDelayed(currentRunnable, interval); - } else { - if (currentRunnable.state == Counter.STATE_CREATED - || currentRunnable.state == Counter.STATE_QUEUED) { - // yet to emit (with in an interval) - currentRunnable.updateEvent(event); - } else if (currentRunnable.state == Counter.STATE_RUNNING - || currentRunnable.state == Counter.STATE_FINISHED) { - // interval finished. open new batch - currentRunnable = new Counter<>(event, callback); - handler.postDelayed(currentRunnable, interval); - } - } - } - - public static class Counter implements Runnable { - static final int STATE_CREATED = 1; - static final int STATE_QUEUED = 2; - static final int STATE_RUNNING = 3; - static final int STATE_FINISHED = 4; - private final Callback callback; - int state; - private T event; - - Counter(T event, Callback callback) { - this.event = event; - this.callback = callback; - state = STATE_CREATED; - } - - void updateEvent(T deliverable) { - this.event = deliverable; - } - - @Override - public void run() { - state = STATE_RUNNING; - callback.onEmit(event); - state = STATE_FINISHED; - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt b/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt new file mode 100644 index 0000000..e9d4f1a --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt @@ -0,0 +1,72 @@ +package com.tonytangandroid.wood + +import android.os.Handler +import android.os.Looper + +internal class Sampler(intervalInMills: Int, callback: Callback) { + private val interval: Int = intervalInMills + private val callback: Callback + private val handler: Handler + + private var currentRunnable: Counter? = null + + init { + this.callback = callback + handler = Handler(Looper.getMainLooper()) + } + + fun consume(event: T?) { + val runnable = currentRunnable + if (runnable == null) { + // first runnable + Counter(event, callback).also { + currentRunnable = it + handler.postDelayed(it, interval.toLong()) + } + } else { + if (runnable.state == Counter.Companion.STATE_CREATED + || runnable.state == Counter.Companion.STATE_QUEUED + ) { + // yet to emit (with in an interval) + runnable.updateEvent(event) + } else if (runnable.state == Counter.Companion.STATE_RUNNING + || runnable.state == Counter.Companion.STATE_FINISHED + ) { + // interval finished. open new batch + Counter(event, callback).also { + currentRunnable = it + handler.postDelayed(it, interval.toLong()) + } + } + } + } + + class Counter internal constructor(event: T?, callback: Callback) : Runnable { + private val callback: Callback + var state: Int + private var event: T? + + init { + this.event = event + this.callback = callback + state = STATE_CREATED + } + + fun updateEvent(deliverable: T?) { + this.event = deliverable + } + + override fun run() { + state = STATE_RUNNING + callback.onEmit(event) + state = STATE_FINISHED + } + + companion object { + const val STATE_CREATED: Int = 1 + const val STATE_QUEUED: Int = 2 + const val STATE_RUNNING: Int = 3 + const val STATE_FINISHED: Int = 4 + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java b/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java deleted file mode 100644 index 49c0fbc..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.tonytangandroid.wood; - -import android.os.Build; -import android.text.PrecomputedText; -import android.widget.TextView; -import androidx.appcompat.widget.AppCompatTextView; -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.util.concurrent.Executor; - -class TextUtil { - /** - * Pref Matters - * - *

PrecomputedText is not yet in support library, But still this is left because the callable - * which is formatting Json, Xml will now be done in background thread - */ - public static void asyncSetText(Executor bgExecutor, final AsyncTextProvider asyncTextProvider) { - final Reference asyncTextProviderReference = - new WeakReference<>(asyncTextProvider); - - bgExecutor.execute( - new Runnable() { - @Override - public void run() { - try { - AsyncTextProvider asyncTextProvider = asyncTextProviderReference.get(); - if (asyncTextProvider == null) return; - // get text from background - CharSequence longString = asyncTextProvider.getText(); - // pre-compute Text before setting on text view. so UI thread can be free from - // calculating text paint - CharSequence updateText; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - PrecomputedText.Params params = - asyncTextProvider.getTextView().getTextMetricsParams(); - updateText = PrecomputedText.create(longString, params); - } else { - updateText = longString; - } - final CharSequence updateTextFinal = updateText; - - asyncTextProvider - .getTextView() - .post( - new Runnable() { - @Override - public void run() { - AsyncTextProvider asyncTextProviderInternal = - asyncTextProviderReference.get(); - if (asyncTextProviderInternal == null) return; - // set pre computed text - TextView textView = asyncTextProviderInternal.getTextView(); - textView.setText(updateTextFinal, TextView.BufferType.SPANNABLE); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - public static boolean isNullOrWhiteSpace(CharSequence text) { - return text == null || text.length() == 0 || text.toString().trim().length() == 0; - } - - public interface AsyncTextProvider { - CharSequence getText(); - - AppCompatTextView getTextView(); - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/TextUtil.kt b/wood/src/main/java/com/tonytangandroid/wood/TextUtil.kt new file mode 100644 index 0000000..7780233 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/TextUtil.kt @@ -0,0 +1,63 @@ +package com.tonytangandroid.wood + +import android.os.Build +import android.text.PrecomputedText +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import com.tonytangandroid.wood.TextUtil.AsyncTextProvider +import java.lang.Exception +import java.lang.ref.Reference +import java.lang.ref.WeakReference +import java.util.concurrent.Executor + +internal object TextUtil { + /** + * Pref Matters + * + * + * PrecomputedText is not yet in support library, But still this is left because the callable + * which is formatting Json, Xml will now be done in background thread + */ + fun asyncSetText(bgExecutor: Executor, asyncTextProvider: AsyncTextProvider?) { + val asyncTextProviderReference: Reference = + WeakReference(asyncTextProvider) + + bgExecutor.execute(Runnable { + try { + asyncTextProviderReference.get()?.run { + // get text from background + val longString = getText() + // pre-compute Text before setting on text view. so UI thread can be free from + // calculating text paint + var updateText: CharSequence? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val params = getTextView().textMetricsParams + updateText = PrecomputedText.create(longString, params) + } else { + updateText = longString + } + val updateTextFinal = updateText + + getTextView().post(Runnable { + asyncTextProviderReference.get()?.run { + val textView: TextView = getTextView() + textView.setText(updateTextFinal, TextView.BufferType.SPANNABLE) + } + }) + } + } catch (e: Exception) { + e.printStackTrace() + } + }) + } + + fun isNullOrWhiteSpace(text: CharSequence?): Boolean { + return text == null || text.isEmpty() || text.toString().trim { it <= ' ' }.isEmpty() + } + + interface AsyncTextProvider { + fun getText(): CharSequence + + fun getTextView(): AppCompatTextView + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Wood.java b/wood/src/main/java/com/tonytangandroid/wood/Wood.java deleted file mode 100644 index 5efd1ac..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/Wood.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.tonytangandroid.wood; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.drawable.Icon; -import android.os.Build; -import androidx.annotation.Nullable; -import java.util.Collections; - -/** - * Class description - * - * @author tonytangandroid - * @version 1.0 - * @since 03/06/18 - */ -public class Wood { - - /** - * Get an Intent to launch the Wood UI directly. - * - * @param context A Context. - * @return An Intent for the main Wood Activity that can be started with {@link - * Context#startActivity(Intent)}. - */ - public static Intent getLaunchIntent(Context context) { - return new Intent(context, LeafListActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - - /** - * Register an app shortcut to launch the Wood UI directly from the launcher on Android 7.0 and - * above. - * - * @param context A valid {@link Context} - * @return The id of the added shortcut (null if this feature is not supported on the - * device). It can be used if you want to remove this shortcut later on. - */ - @TargetApi(Build.VERSION_CODES.N_MR1) - @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) - @Nullable - public static String addAppShortcut(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - final String id = context.getPackageName() + ".wood_ui"; - final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class); - final ShortcutInfo shortcut = - new ShortcutInfo.Builder(context, id) - .setShortLabel("Wood") - .setLongLabel("Open Wood") - .setIcon(Icon.createWithResource(context, R.drawable.wood_icon)) - .setIntent(getLaunchIntent(context).setAction(Intent.ACTION_VIEW)) - .build(); - shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut)); - return id; - } else { - return null; - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/Wood.kt b/wood/src/main/java/com/tonytangandroid/wood/Wood.kt new file mode 100644 index 0000000..09a29f5 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/Wood.kt @@ -0,0 +1,56 @@ +package com.tonytangandroid.wood + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Build + +/** + * Class description + * + * @author tonytangandroid + * @version 1.0 + * @since 03/06/18 + */ +object Wood { + /** + * Get an Intent to launch the Wood UI directly. + * + * @param context A Context. + * @return An Intent for the main Wood Activity that can be started with [ ][Context.startActivity]. + */ + fun getLaunchIntent(context: Context?): Intent { + return Intent(context, LeafListActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + /** + * Register an app shortcut to launch the Wood UI directly from the launcher on Android 7.0 and + * above. + * + * @param context A valid [Context] + * @return The id of the added shortcut (`null` if this feature is not supported on the + * device). It can be used if you want to remove this shortcut later on. + */ + @TargetApi(Build.VERSION_CODES.N_MR1) + fun addAppShortcut(context: Context): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val id = context.packageName + ".wood_ui" + val shortcutManager = + context.getSystemService(ShortcutManager::class.java) + val shortcut = + ShortcutInfo.Builder(context, id) + .setShortLabel("Wood") + .setLongLabel("Open Wood") + .setIcon(Icon.createWithResource(context, R.drawable.wood_icon)) + .setIntent(getLaunchIntent(context).setAction(Intent.ACTION_VIEW)) + .build() + shortcutManager.addDynamicShortcuts(mutableListOf(shortcut)) + return id + } else { + return null + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.java b/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.java deleted file mode 100644 index 9c8d045..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.graphics.Color; -import android.util.Log; -import androidx.core.content.ContextCompat; - -class WoodColorUtil { - - public static final int SEARCHED_HIGHLIGHT_BACKGROUND_COLOR = Color.parseColor("#FD953F"); - public static final int HIGHLIGHT_BACKGROUND_COLOR = Color.parseColor("#FFFD38"); - public static final int HIGHLIGHT_TEXT_COLOR = 0; - public static final boolean HIGHLIGHT_UNDERLINE = false; - private static WoodColorUtil TRANSACTION_COLOR_UTIL_INSTANCE; - private final int mColorDefault; - private final int mColorVerbose; - private final int mColorError; - private final int mColorAssert; - private final int mColorInfo; - private final int mColorWarning; - private final int mColorDebug; - - private WoodColorUtil(Context context) { - mColorDefault = ContextCompat.getColor(context, R.color.wood_status_default); - mColorVerbose = ContextCompat.getColor(context, R.color.wood_log_verbose); - mColorDebug = ContextCompat.getColor(context, R.color.wood_log_debug); - mColorInfo = ContextCompat.getColor(context, R.color.wood_log_info); - mColorWarning = ContextCompat.getColor(context, R.color.wood_log_warning); - mColorError = ContextCompat.getColor(context, R.color.wood_log_error); - mColorAssert = ContextCompat.getColor(context, R.color.wood_log_assert); - } - - public static WoodColorUtil getInstance(Context context) { - if (TRANSACTION_COLOR_UTIL_INSTANCE == null) { - TRANSACTION_COLOR_UTIL_INSTANCE = new WoodColorUtil(context); - } - return TRANSACTION_COLOR_UTIL_INSTANCE; - } - - public int getTransactionColor(Leaf transaction) { - return getTransactionColor(transaction.getPriority()); - } - - public int getTransactionColor(int priority) { - if (priority == Log.VERBOSE) { - return mColorVerbose; - } else if (priority == Log.DEBUG) { - return mColorDebug; - } else if (priority == Log.INFO) { - return mColorInfo; - } else if (priority == Log.WARN) { - return mColorWarning; - } else if (priority == Log.ERROR) { - return mColorError; - } else if (priority == Log.ASSERT) { - return mColorAssert; - } else { - return mColorDefault; - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt new file mode 100644 index 0000000..4cdda6f --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt @@ -0,0 +1,42 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.graphics.Color +import android.util.Log +import androidx.core.content.ContextCompat + +internal class WoodColorUtil private constructor(context: Context) { + private val mColorDefault: Int = ContextCompat.getColor(context, R.color.wood_status_default) + private val mColorVerbose: Int = ContextCompat.getColor(context, R.color.wood_log_verbose) + private val mColorError: Int = ContextCompat.getColor(context, R.color.wood_log_error) + private val mColorAssert: Int = ContextCompat.getColor(context, R.color.wood_log_assert) + private val mColorInfo: Int = ContextCompat.getColor(context, R.color.wood_log_info) + private val mColorWarning: Int = ContextCompat.getColor(context, R.color.wood_log_warning) + private val mColorDebug: Int = ContextCompat.getColor(context, R.color.wood_log_debug) + + fun getTransactionColor(transaction: Leaf): Int { + return getTransactionColor(transaction.priority) + } + + fun getTransactionColor(priority: Int): Int = when(priority) { + Log.VERBOSE -> mColorVerbose + Log.DEBUG -> mColorDebug + Log.INFO -> mColorInfo + Log.WARN -> mColorWarning + Log.ERROR -> mColorError + Log.ASSERT -> mColorAssert + else -> mColorDefault + } + + companion object { + val SEARCHED_HIGHLIGHT_BACKGROUND_COLOR: Int = Color.parseColor("#FD953F") + val HIGHLIGHT_BACKGROUND_COLOR: Int = Color.parseColor("#FFFD38") + const val HIGHLIGHT_TEXT_COLOR: Int = 0 + const val HIGHLIGHT_UNDERLINE: Boolean = false + private var TRANSACTION_COLOR_UTIL_INSTANCE: WoodColorUtil? = null + + fun getInstance(context: Context): WoodColorUtil = TRANSACTION_COLOR_UTIL_INSTANCE ?: WoodColorUtil(context).also { + TRANSACTION_COLOR_UTIL_INSTANCE = it + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.java b/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.java deleted file mode 100644 index fd62e7c..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import androidx.room.Database; -import androidx.room.Room; -import androidx.room.RoomDatabase; - -@Database( - entities = {Leaf.class}, - version = 1, - exportSchema = false) -public abstract class WoodDatabase extends RoomDatabase { - private static WoodDatabase WOOD_DATABASE_INSTANCE; - - public static WoodDatabase getInstance(Context context) { - if (WOOD_DATABASE_INSTANCE == null) { - WOOD_DATABASE_INSTANCE = - Room.databaseBuilder(context, WoodDatabase.class, "WoodDatabase").build(); - } - return WOOD_DATABASE_INSTANCE; - } - - public abstract LeafDao leafDao(); -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt new file mode 100644 index 0000000..ae8a892 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt @@ -0,0 +1,23 @@ +package com.tonytangandroid.wood + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Leaf::class], version = 1, exportSchema = false) +internal abstract class WoodDatabase : RoomDatabase() { + abstract fun leafDao(): LeafDao + + companion object { + private var WOOD_DATABASE_INSTANCE: WoodDatabase? = null + + fun getInstance(context: Context): WoodDatabase = WOOD_DATABASE_INSTANCE ?: Room.databaseBuilder( + context, + WoodDatabase::class.java, + "WoodDatabase" + ).build().also { + WOOD_DATABASE_INSTANCE = it + } + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodTree.java b/wood/src/main/java/com/tonytangandroid/wood/WoodTree.java deleted file mode 100644 index b2203fa..0000000 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodTree.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.tonytangandroid.wood; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.Executor; -import timber.log.Timber; - -public class WoodTree extends Timber.DebugTree { - - @NonNull private static final Period DEFAULT_RETENTION = Period.ONE_WEEK; - private static final String PREF_WOOD_CONFIG = "pref_wood_config"; - private static final String PREF_KEY_AUTO_SCROLL = "pref_key_auto_scroll"; - @NonNull private final Context context; - @NonNull private final WoodDatabase woodDatabase; - private final Executor executor; - private final String threadTagPrefix; - @Nullable private NotificationHelper notificationHelper; - @NonNull private RetentionManager retentionManager; - private int maxContentLength = 250000; - private boolean stickyNotification = false; - private List supportedTaggerList = new ArrayList<>(); - private final SharedPreferences sharedPreferences; - private int logLevel = Log.DEBUG; - - /** @param context The current Context. */ - public WoodTree(@NonNull Context context) { - this(context, ""); - } - - /** - * @param context context - * @param threadTagPrefix the extra prefix added on the logged message. - */ - public WoodTree(@NonNull Context context, String threadTagPrefix) { - this.threadTagPrefix = threadTagPrefix; - this.executor = new JobExecutor(); - this.context = context.getApplicationContext(); - this.woodDatabase = WoodDatabase.getInstance(context); - this.retentionManager = new RetentionManager(this.context, DEFAULT_RETENTION); - this.sharedPreferences = context.getSharedPreferences(PREF_WOOD_CONFIG, Context.MODE_PRIVATE); - } - - public static boolean autoScroll(Context context) { - SharedPreferences sharedPreferences = - context.getSharedPreferences(PREF_WOOD_CONFIG, Context.MODE_PRIVATE); - return sharedPreferences.getBoolean(PREF_KEY_AUTO_SCROLL, true); - } - - /** - * Control whether a notification is shown while Timber log is recorded. - * - * @param sticky true to show a sticky notification. - * @return The {@link WoodTree} instance. - */ - @NonNull - public WoodTree showNotification(boolean sticky) { - this.stickyNotification = sticky; - notificationHelper = new NotificationHelper(this.context); - return this; - } - - @NonNull - public WoodTree limitToTheseTaggerList(@NonNull List supportedTaggerList) { - this.supportedTaggerList = supportedTaggerList; - return this; - } - - /** - * If you want to only log warning or above, pass {@link android.util.Log#WARN}. By default, it - * will log all debug log {@link android.util.Log#DEBUG} or above - * - * @param logLevel the log level value from {@link android.util.Log} - * @return The {@link WoodTree} instance. - */ - @NonNull - public WoodTree logLevel(int logLevel) { - this.logLevel = logLevel; - return this; - } - - /** - * Set the retention period for Timber log data captured by this interceptor. The default is one - * week. - * - * @param period the period for which to retain Timber log data. - * @return The {@link WoodTree} instance. - */ - @NonNull - public WoodTree retainDataFor(Period period) { - retentionManager = new RetentionManager(context, period); - return this; - } - - /** - * Set the log should auto scroll like Android Logcat console. By default it is false. - * - * @param autoScroll true if you want to make the log auto scroll. - * @return The {@link WoodTree} instance. - */ - @NonNull - public WoodTree autoScroll(boolean autoScroll) { - sharedPreferences.edit().putBoolean(PREF_KEY_AUTO_SCROLL, autoScroll).apply(); - return this; - } - - /** - * Set the maximum length for request and response content before it is truncated. Warning: - * setting this value too high may cause unexpected results. - * - * @param max the maximum length (in bytes) for request/response content. - * @return The {@link WoodTree} instance. - */ - @NonNull - public WoodTree maxLength(int max) { - this.maxContentLength = Math.min(max, 999999); // close to => 1 MB Max in a BLOB SQLite. - return this; - } - - @Override - protected void log( - final int priority, final String tag, final @NonNull String message, final Throwable t) { - if (shouldBeLogged(priority, tag)) { - String assembledMessage = formatThreadTag(message, this.threadTagPrefix); - executor.execute(() -> doLog(priority, tag, assembledMessage, t)); - } - } - - private static String formatThreadTag(String message, String threadTagPrefix) { - return String.format( - Locale.US, "[%s#%s]:%s", threadTagPrefix, Thread.currentThread().getName(), message); - } - - private boolean shouldBeLogged(int priority, String tag) { - if (priority < logLevel) { - return false; - } - - if (hasNoTagFilter()) { - return true; - } - - return tagIsListedCaseInsensitive(tag); - } - - private boolean tagIsListedCaseInsensitive(String tag) { - String toLowerCase = tag.toLowerCase(); - for (String supportedTagger : supportedTaggerList) { - if (supportedTagger.toLowerCase().contains(toLowerCase)) { - return true; - } - } - return false; - } - - private boolean hasNoTagFilter() { - return supportedTaggerList.size() == 0; - } - - private void doLog(int priority, String tag, @NonNull String message, Throwable t) { - Leaf transaction = new Leaf(); - transaction.setPriority(priority); - transaction.setCreateAt(System.currentTimeMillis()); - transaction.setTag(tag); - transaction.setLength(message.length()); - if (t != null) { - message = message + "\n" + t.getMessage() + "\n" + ErrorUtil.asString(t); - } - transaction.setBody(message.substring(0, Math.min(message.length(), maxContentLength)) + ""); - create(transaction); - } - - private void create(@NonNull Leaf transaction) { - long transactionId = woodDatabase.leafDao().insertTransaction(transaction); - transaction.setId(transactionId); - if (notificationHelper != null) { - notificationHelper.show(transaction, stickyNotification); - } - retentionManager.doMaintenance(); - } - - public enum Period { - /** Retain data for the last hour. */ - ONE_HOUR, - /** Retain data for the last day. */ - ONE_DAY, - /** Retain data for the last week. */ - ONE_WEEK, - /** Retain data forever. */ - FOREVER - } - - /** From https://stackoverflow.com/a/1149712/4068957 */ - static class ErrorUtil { - - public static String asString(Throwable throwable) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - throwable.printStackTrace(pw); - return sw.toString(); // stack trace as a string - } - } -} diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt new file mode 100644 index 0000000..a9f3600 --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt @@ -0,0 +1,205 @@ +package com.tonytangandroid.wood + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import com.tonytangandroid.wood.WoodTree.ErrorUtil +import timber.log.Timber.DebugTree +import java.io.PrintWriter +import java.io.StringWriter +import java.util.ArrayList +import java.util.Locale +import java.util.concurrent.Executor +import kotlin.math.min + +class WoodTree @JvmOverloads constructor(context: Context, private val threadTagPrefix: String = "") : DebugTree() { + private var contextApp: Context = context.applicationContext + private val woodDatabase: WoodDatabase = WoodDatabase.getInstance(context) + private val executor: Executor = JobExecutor() + private var notificationHelper: NotificationHelper? = null + private var retentionManager: RetentionManager + private var maxContentLength: Int = 250000 + private var stickyNotification = false + private var supportedTaggerList: MutableList = ArrayList() + private val sharedPreferences: SharedPreferences + private var logLevel = Log.DEBUG + + /** + * @param context context + * @param threadTagPrefix the extra prefix added on the logged message. + */ + /** @param context The current Context. + */ + init { + this.retentionManager = RetentionManager(contextApp, DEFAULT_RETENTION) + this.sharedPreferences = + context.getSharedPreferences(PREF_WOOD_CONFIG, Context.MODE_PRIVATE) + } + + /** + * Control whether a notification is shown while Timber log is recorded. + * + * @param sticky true to show a sticky notification. + * @return The [WoodTree] instance. + */ + fun showNotification(sticky: Boolean): WoodTree { + this.stickyNotification = sticky + notificationHelper = NotificationHelper(contextApp) + return this + } + + fun limitToTheseTaggerList(supportedTaggerList: MutableList): WoodTree { + this.supportedTaggerList = supportedTaggerList + return this + } + + /** + * If you want to only log warning or above, pass [Log.WARN]. By default, it + * will log all debug log [Log.DEBUG] or above + * + * @param logLevel the log level value from [Log] + * @return The [WoodTree] instance. + */ + fun logLevel(logLevel: Int): WoodTree { + this.logLevel = logLevel + return this + } + + /** + * Set the retention period for Timber log data captured by this interceptor. The default is one + * week. + * + * @param period the period for which to retain Timber log data. + * @return The [WoodTree] instance. + */ + fun retainDataFor(period: Period): WoodTree { + retentionManager = RetentionManager(contextApp, period) + return this + } + + /** + * Set the log should auto scroll like Android Logcat console. By default it is false. + * + * @param autoScroll true if you want to make the log auto scroll. + * @return The [WoodTree] instance. + */ + fun autoScroll(autoScroll: Boolean): WoodTree { + sharedPreferences.edit().putBoolean(PREF_KEY_AUTO_SCROLL, autoScroll).apply() + return this + } + + /** + * Set the maximum length for request and response content before it is truncated. Warning: + * setting this value too high may cause unexpected results. + * + * @param max the maximum length (in bytes) for request/response content. + * @return The [WoodTree] instance. + */ + fun maxLength(max: Int): WoodTree { + maxContentLength = min(max, 999999) // close to => 1 MB Max in a BLOB SQLite. + return this + } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (shouldBeLogged(priority, tag.orEmpty())) { + val assembledMessage: String = formatThreadTag(message, this.threadTagPrefix) + executor.execute(Runnable { doLog(priority, tag, assembledMessage, t) }) + } + } + + private fun shouldBeLogged(priority: Int, tag: String): Boolean { + if (priority < logLevel) { + return false + } + + if (hasNoTagFilter()) { + return true + } + + return tagIsListedCaseInsensitive(tag) + } + + private fun tagIsListedCaseInsensitive(tag: String): Boolean { + val toLowerCase = tag.lowercase(Locale.getDefault()) + for (supportedTagger in supportedTaggerList) { + if (supportedTagger.lowercase(Locale.getDefault()).contains(toLowerCase)) { + return true + } + } + return false + } + + private fun hasNoTagFilter(): Boolean { + return supportedTaggerList.isEmpty() + } + + private fun doLog(priority: Int, tag: String?, message: String, t: Throwable?) { + var message = message + val transaction = Leaf() + transaction.priority = priority + transaction.createAt = System.currentTimeMillis() + transaction.tag = tag + transaction.setLength(message.length) + if (t != null) { + message = message + "\n" + t.message + "\n" + ErrorUtil.asString(t) + } + transaction.setBody( + message.substring( + 0, + min(message.length, maxContentLength) + ) + "" + ) + create(transaction) + } + + private fun create(transaction: Leaf) { + val transactionId = woodDatabase.leafDao().insertTransaction(transaction) + transaction.id = transactionId + if (notificationHelper != null) { + notificationHelper!!.show(transaction, stickyNotification) + } + retentionManager.doMaintenance() + } + + enum class Period { + /** Retain data for the last hour. */ + ONE_HOUR, + + /** Retain data for the last day. */ + ONE_DAY, + + /** Retain data for the last week. */ + ONE_WEEK, + + /** Retain data forever. */ + FOREVER + } + + /** From https://stackoverflow.com/a/1149712/4068957 */ + internal object ErrorUtil { + fun asString(throwable: Throwable): String { + val sw = StringWriter() + val pw = PrintWriter(sw) + throwable.printStackTrace(pw) + return sw.toString() // stack trace as a string + } + } + + companion object { + private val DEFAULT_RETENTION = Period.ONE_WEEK + private const val PREF_WOOD_CONFIG = "pref_wood_config" + private const val PREF_KEY_AUTO_SCROLL = "pref_key_auto_scroll" + @JvmStatic + fun autoScroll(context: Context): Boolean { + val sharedPreferences = + context.getSharedPreferences(PREF_WOOD_CONFIG, Context.MODE_PRIVATE) + return sharedPreferences.getBoolean(PREF_KEY_AUTO_SCROLL, true) + } + + private fun formatThreadTag(message: String?, threadTagPrefix: String): String { + return String.format( + Locale.US, "[%s#%s]:%s", threadTagPrefix, Thread.currentThread().getName(), message + ) + } + } +} diff --git a/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.java b/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.java deleted file mode 100644 index 8486c3c..0000000 --- a/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.tonytangandroid.wood; - -import static com.google.common.truth.Truth.assertThat; - -import java.util.Collections; -import org.junit.Test; - -public class FormatUtilsTest { - - @Test - public void onHandleIntent() { - assertThat(FormatUtils.indexOf("abcd", "c")).isEqualTo(Collections.singletonList(2)); - } -} diff --git a/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.kt b/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.kt new file mode 100644 index 0000000..50dca39 --- /dev/null +++ b/wood/src/test/java/com/tonytangandroid/wood/FormatUtilsTest.kt @@ -0,0 +1,12 @@ +package com.tonytangandroid.wood + +import com.google.common.truth.Truth +import com.tonytangandroid.wood.FormatUtils.indexOf +import org.junit.Test + +class FormatUtilsTest { + @Test + fun onHandleIntent() { + Truth.assertThat(indexOf("abcd", "c")).isEqualTo(mutableListOf(2)) + } +} From 06527b2712be4a0a163ac7d10e8e289aa2d8a93f Mon Sep 17 00:00:00 2001 From: Florian Fournier Date: Thu, 21 Nov 2024 09:35:48 +0100 Subject: [PATCH 2/2] clean the code --- .../wood/sample/GeneratingLogActivity.kt | 5 +- .../wood/sample/HomeActivity.kt | 9 ++- .../wood/sample/WoodIntegrationUtil.kt | 3 +- .../layout/activity_keep_generating_log.xml | 2 +- build.gradle | 10 +-- settings.gradle | 3 + wood-no-op/build.gradle | 4 +- .../woodnoop/LeavesCollectionFragment.kt | 5 +- .../com/tonytangandroid/woodnoop/WoodTree.kt | 2 +- .../res/layout/wood_activity_leaf_list.xml | 7 +- wood/build.gradle | 22 +++-- wood/proguard-rules.pro | 2 +- .../com/tonytangandroid/wood/Debouncer.kt | 15 ++-- .../com/tonytangandroid/wood/FormatUtils.kt | 2 +- .../com/tonytangandroid/wood/HighlightSpan.kt | 17 ++-- .../com/tonytangandroid/wood/JobExecutor.kt | 2 +- .../java/com/tonytangandroid/wood/Leaf.kt | 20 +---- .../java/com/tonytangandroid/wood/LeafDao.kt | 4 +- .../wood/LeafDetailFragment.kt | 80 +++++++++---------- .../wood/LeafDetailsActivity.kt | 3 +- .../tonytangandroid/wood/LeafListViewModel.kt | 8 +- .../tonytangandroid/wood/LeafViewHolder.kt | 18 ++--- .../wood/LeavesCollectionFragment.kt | 2 +- .../wood/NotificationHelper.kt | 41 +++++----- .../tonytangandroid/wood/RetentionManager.kt | 14 ++-- .../java/com/tonytangandroid/wood/Sampler.kt | 23 ++---- .../java/com/tonytangandroid/wood/Wood.kt | 2 +- .../com/tonytangandroid/wood/WoodColorUtil.kt | 32 ++++---- .../com/tonytangandroid/wood/WoodDatabase.kt | 12 +-- .../java/com/tonytangandroid/wood/WoodTree.kt | 13 +-- .../res/layout/wood_fragment_leaf_detail.xml | 6 +- 31 files changed, 180 insertions(+), 208 deletions(-) diff --git a/app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.kt b/app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.kt index beb58e8..9f5e31a 100644 --- a/app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.kt +++ b/app/src/main/java/com/tonytangandroid/wood/sample/GeneratingLogActivity.kt @@ -2,11 +2,12 @@ package com.tonytangandroid.wood.sample import android.os.Bundle import android.os.Handler +import android.os.Looper import androidx.appcompat.app.AppCompatActivity import timber.log.Timber class GeneratingLogActivity : AppCompatActivity() { - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private var count = 0 override fun onCreate(savedInstanceState: Bundle?) { @@ -24,7 +25,7 @@ class GeneratingLogActivity : AppCompatActivity() { } private fun log() { - for (i in 0..9) { + (0..9).forEach { i -> count++ Timber.v("generate log :%s", count) } diff --git a/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt b/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt index 4ebb4b5..0c4001d 100644 --- a/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt +++ b/app/src/main/java/com/tonytangandroid/wood/sample/HomeActivity.kt @@ -13,12 +13,13 @@ import timber.log.Timber import java.lang.Exception class HomeActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) - findViewById(R.id.btn_generate_log).setOnClickListener(View.OnClickListener { view: View? -> generateTimberLog() }) - findViewById(R.id.btn_test_extreme_log).setOnClickListener(View.OnClickListener { view: View? -> keepGenerateLog() }) - findViewById(R.id.launch_wood_directly).setOnClickListener(View.OnClickListener { view: View? -> launchWoodDirectly() }) + findViewById(R.id.btn_generate_log).setOnClickListener(View.OnClickListener { view: View -> generateTimberLog() }) + findViewById(R.id.btn_test_extreme_log).setOnClickListener(View.OnClickListener { view: View -> keepGenerateLog() }) + findViewById(R.id.launch_wood_directly).setOnClickListener(View.OnClickListener { view: View -> launchWoodDirectly() }) addAppShortcut(this) } @@ -66,7 +67,7 @@ class HomeActivity : AppCompatActivity() { } companion object { - @JvmStatic + fun logInBackground() { Thread(Runnable { Timber.i("This is an INFO message triggered in background thread.") }).start() } diff --git a/app/src/main/java/com/tonytangandroid/wood/sample/WoodIntegrationUtil.kt b/app/src/main/java/com/tonytangandroid/wood/sample/WoodIntegrationUtil.kt index 5cbddc8..9c37746 100644 --- a/app/src/main/java/com/tonytangandroid/wood/sample/WoodIntegrationUtil.kt +++ b/app/src/main/java/com/tonytangandroid/wood/sample/WoodIntegrationUtil.kt @@ -7,10 +7,9 @@ import timber.log.Timber object WoodIntegrationUtil { - @JvmStatic fun initWood(application: Application) { Timber.plant( - WoodTree(application,"tony") + WoodTree(application, "tony") .retainDataFor(WoodTree.Period.FOREVER) .logLevel(Log.VERBOSE) .autoScroll(false) diff --git a/app/src/main/res/layout/activity_keep_generating_log.xml b/app/src/main/res/layout/activity_keep_generating_log.xml index 64d3954..0f23091 100644 --- a/app/src/main/res/layout/activity_keep_generating_log.xml +++ b/app/src/main/res/layout/activity_keep_generating_log.xml @@ -8,7 +8,7 @@ android:padding="16dp" tools:context=".HomeActivity"> - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/wood/build.gradle b/wood/build.gradle index c39aa91..758e52b 100644 --- a/wood/build.gradle +++ b/wood/build.gradle @@ -63,13 +63,13 @@ dependencies { api "androidx.appcompat:appcompat:$appcompatVersion" api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-rxjava2:$roomVersion" - api "androidx.fragment:fragment-ktx:1.8.3" + api "androidx.fragment:fragment-ktx:1.8.5" api "com.uber.autodispose:autodispose:1.4.0" api "com.uber.autodispose:autodispose-android:1.4.0" api "com.uber.autodispose:autodispose-android-archcomponents:1.4.0" - implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'androidx.core:core-ktx:1.15.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' api "androidx.paging:paging-rxjava2:$pagingVersion" api "androidx.paging:paging-runtime:$pagingVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" @@ -86,12 +86,18 @@ dependencies { annotationProcessor "com.google.dagger:dagger-android-processor:${daggerVersion}" androidTestAnnotationProcessor "com.google.dagger:dagger-android-processor:${daggerVersion}" - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation "com.google.truth:truth:$truthVersion" - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation "androidx.fragment:fragment-testing:1.5.2" + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation "androidx.fragment:fragment-testing:1.8.5" testImplementation "junit:junit:$junit" testImplementation "com.google.truth:truth:$truthVersion" } + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} diff --git a/wood/proguard-rules.pro b/wood/proguard-rules.pro index 3a251d0..51c5c34 100644 --- a/wood/proguard-rules.pro +++ b/wood/proguard-rules.pro @@ -19,4 +19,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class android.support.v7.widget.SearchView { *; } \ No newline at end of file +-keep class androidx.appcompat.widget.SearchView { *; } \ No newline at end of file diff --git a/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt b/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt index 97f6ab4..fb700f0 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/Debouncer.kt @@ -1,21 +1,22 @@ package com.tonytangandroid.wood import android.os.Handler +import android.os.Looper -internal class Debouncer(private val mInterval: Int, private val mCallback: Callback) { - private val mHandler = Handler() +internal class Debouncer(private val interval: Int, private val callback: Callback) { + private val handler = Handler(Looper.getMainLooper()) fun consume(event: T) { - mHandler.removeCallbacksAndMessages(null) - mHandler.postDelayed(Counter(event, mCallback), mInterval.toLong()) + handler.removeCallbacksAndMessages(null) + handler.postDelayed(Counter(event, callback), interval.toLong()) } class Counter internal constructor( - private val mEvent: T, - private val mCallback: Callback + private val event: T, + private val callback: Callback ) : Runnable { override fun run() { - mCallback.onEmit(mEvent) + callback.onEmit(event) } } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt index 624c3fe..c6c2531 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.kt @@ -52,7 +52,7 @@ internal object FormatUtils { } fun getShareText(transaction: Leaf): CharSequence { - return transaction.body().orEmpty() + return transaction.body.orEmpty() } fun highlightSearchKeyword(textView: TextView, searchKey: String?): List { diff --git a/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt b/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt index 3cdc62e..e4e4748 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/HighlightSpan.kt @@ -6,16 +6,17 @@ import android.text.style.UpdateAppearance import androidx.annotation.ColorInt internal class HighlightSpan( - private val mBackgroundColor: Int, - @param:ColorInt private val mTextColor: Int, - private val mUnderLineText: Boolean + private val backgroundColor: Int, + @param:ColorInt private val textColor: Int, + private val underLineText: Boolean ) : CharacterStyle(), UpdateAppearance { - private val mApplyBackgroundColor = mBackgroundColor != 0 - private val mApplyTextColor = mTextColor != 0 + + private val applyBackgroundColor = backgroundColor != 0 + private val applyTextColor = textColor != 0 override fun updateDrawState(ds: TextPaint) { - if (mApplyTextColor) ds.color = mTextColor - if (mApplyBackgroundColor) ds.bgColor = mBackgroundColor - ds.isUnderlineText = mUnderLineText + if (applyTextColor) ds.color = textColor + if (applyBackgroundColor) ds.bgColor = backgroundColor + ds.isUnderlineText = underLineText } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt b/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt index fc5e4cb..42da993 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/JobExecutor.kt @@ -12,7 +12,7 @@ internal class JobExecutor : Executor { init { val workQueue: BlockingQueue = LinkedBlockingQueue() - this.threadPoolExecutor = + threadPoolExecutor = ThreadPoolExecutor( INITIAL_POOL_SIZE, MAX_POOL_SIZE, diff --git a/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt b/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt index e32e3eb..0b497b3 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/Leaf.kt @@ -28,24 +28,8 @@ class Leaf { var priority: Int = 0 @ColumnInfo(name = "length") - private var length = 0 + var length: Int = 0 @ColumnInfo(name = "body", typeAffinity = ColumnInfo.TEXT) - private var body: String? = null - - fun length(): Int { - return length - } - - fun setLength(length: Int) { - this.length = length - } - - fun body(): String? { - return body - } - - fun setBody(body: String?) { - this.body = body - } + var body: String? = null } diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt index 41fc667..b293ca7 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.kt @@ -13,10 +13,10 @@ import io.reactivex.Flowable @Dao internal abstract class LeafDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertTransaction(leaf: Leaf?): Long + abstract fun insertTransaction(leaf: Leaf): Long @Delete - abstract fun deleteTransactions(vararg leaves: Leaf?): Int + abstract fun deleteTransactions(vararg leaves: Leaf): Int @Query(value = "DELETE FROM Leaf WHERE createAt < :beforeDate") abstract fun deleteTransactionsBefore(beforeDate: Long): Int diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt index 021849f..3aeee3c 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.kt @@ -53,17 +53,17 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, private var leaf: Leaf? = null private var searchIndexList: List = listOf(0) private val executor: ExecutorService = Executors.newSingleThreadExecutor() - private lateinit var search_bar: View - private lateinit var et_key_word: EditText - private lateinit var tv_search_count: TextView - private lateinit var tv_body: AppCompatTextView + private lateinit var searchBar: View + private lateinit var etKeyWord: EditText + private lateinit var tvSearchCount: TextView + private lateinit var tvBody: AppCompatTextView private val searchDebounce = Debouncer(400, object: Callback { override fun onEmit(event: String) { onSearchKeyEmitted(event) } }) - private lateinit var nested_scroll_view: NestedScrollView - private lateinit var floating_action_button: FloatingActionButton + private lateinit var nestedScrollView: NestedScrollView + private lateinit var floatingActionButton: FloatingActionButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,22 +87,22 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, } private fun bindView(rootView: View) { - tv_body = rootView.findViewById(R.id.wood_details_body) - nested_scroll_view = + tvBody = rootView.findViewById(R.id.wood_details_body) + nestedScrollView = rootView.findViewById(R.id.wood_details_scroll_parent) - floating_action_button = + floatingActionButton = rootView.findViewById(R.id.wood_details_search_fab) - search_bar = rootView.findViewById(R.id.wood_details_search_bar) + searchBar = rootView.findViewById(R.id.wood_details_search_bar) val searchBarPrev = rootView.findViewById(R.id.wood_details_search_prev) val searchBarNext = rootView.findViewById(R.id.wood_details_search_next) val searchBarClose = rootView.findViewById(R.id.wood_details_search_close) - et_key_word = rootView.findViewById(R.id.wood_details_search) - tv_search_count = rootView.findViewById(R.id.wood_details_search_count) - floating_action_button.setOnClickListener(this) + etKeyWord = rootView.findViewById(R.id.wood_details_search) + tvSearchCount = rootView.findViewById(R.id.wood_details_search_count) + floatingActionButton.setOnClickListener(this) searchBarPrev.setOnClickListener(this) searchBarNext.setOnClickListener(this) searchBarClose.setOnClickListener(this) - et_key_word.addTextChangedListener(this) + etKeyWord.addTextChangedListener(this) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} @@ -145,13 +145,13 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, private fun populateUI() { val l = leaf val color = if (l != null) { - colorUtil?.getTransactionColor(l) ?: ResourcesCompat.getColor(resources, R.color.wood_status_default, floating_action_button.context.theme) + colorUtil?.getTransactionColor(l) ?: ResourcesCompat.getColor(resources, R.color.wood_status_default, floatingActionButton.context.theme) } else { - resources.getColor(R.color.wood_status_default) + ResourcesCompat.getColor(resources, R.color.wood_status_default, context?.theme) } - floating_action_button.backgroundTintList = colorStateList(color) - search_bar.setBackgroundColor(color) - et_key_word.setHint(R.string.wood_search_hint) + floatingActionButton.backgroundTintList = colorStateList(color) + searchBar.setBackgroundColor(color) + etKeyWord.setHint(R.string.wood_search_hint) populateBody() } @@ -165,7 +165,7 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, } private fun updateUI() { - searchIndexList = FormatUtils.highlightSearchKeyword(tv_body, searchKey) + searchIndexList = FormatUtils.highlightSearchKeyword(tvBody, searchKey) updateSearch(1, searchKey) } @@ -181,7 +181,7 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, } override fun getText(): CharSequence { - val body: CharSequence = leaf?.body().orEmpty() + val body: CharSequence = leaf?.body.orEmpty() if (isNullOrWhiteSpace(body) || isNullOrWhiteSpace(searchKey)) { return body } else { @@ -194,7 +194,7 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, } override fun getTextView(): AppCompatTextView { - return tv_body + return tvBody } private fun updateSearch(targetIndex: Int, searchKey: String?) { @@ -202,8 +202,8 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, val list = searchIndexList val size = list.size targetIndex = adjustTargetIndex(targetIndex, size) - tv_search_count.text = "$targetIndex/$size" - (tv_body.getText() as Spannable).removeSpan(colorSpan) + tvSearchCount.text = "$targetIndex/$size" + (tvBody.getText() as Spannable).removeSpan(colorSpan) if (targetIndex > 0) { updateSpan(targetIndex, searchKey.orEmpty(), list) } @@ -213,15 +213,15 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, private fun updateSpan(targetIndex: Int, searchKey: String, list: List) { val begin: Int = list[targetIndex - 1] val end = begin + searchKey.length - val lineNumber = tv_body.layout.getLineForOffset(begin) - (tv_body.getText() as Spannable).setSpan( + val lineNumber = tvBody.layout.getLineForOffset(begin) + (tvBody.getText() as Spannable).setSpan( colorSpan, begin, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) - val scrollToY = tv_body.layout.getLineTop(lineNumber) - nested_scroll_view.scrollTo(0, scrollToY) + val scrollToY = tvBody.layout.getLineTop(lineNumber) + nestedScrollView.scrollTo(0, scrollToY) } private fun adjustTargetIndex(targetIndex: Int, size: Int): Int { @@ -239,9 +239,9 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, } private fun showKeyboard() { - et_key_word.requestFocus() + etKeyWord.requestFocus() val imm = inputMethodManager() - imm?.showSoftInput(et_key_word, InputMethodManager.SHOW_IMPLICIT) + imm?.showSoftInput(etKeyWord, InputMethodManager.SHOW_IMPLICIT) } private fun inputMethodManager(): InputMethodManager? { @@ -250,7 +250,7 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, private fun hideKeyboard() { val imm = inputMethodManager() - imm?.hideSoftInputFromWindow(et_key_word.windowToken, 0) + imm?.hideSoftInputFromWindow(etKeyWord.windowToken, 0) } @Suppress("deprecation") @@ -276,23 +276,23 @@ class LeafDetailFragment : Fragment(), View.OnClickListener, AsyncTextProvider, private fun clearSearch() { if (isNullOrWhiteSpace(searchKey)) { - floating_action_button.show() - search_bar.visibility = View.GONE - nested_scroll_view.setPadding(0, 0, 0, nested_scroll_view.bottom) + floatingActionButton.show() + searchBar.visibility = View.GONE + nestedScrollView.setPadding(0, 0, 0, nestedScrollView.bottom) hideKeyboard() } else { - et_key_word.setText("") + etKeyWord.setText("") } } private fun showSearch() { - floating_action_button.hide() - search_bar.visibility = View.VISIBLE - nested_scroll_view.setPadding( + floatingActionButton.hide() + searchBar.visibility = View.VISIBLE + nestedScrollView.setPadding( 0, - getResources().getDimensionPixelSize(R.dimen.wood_search_bar_height), + resources.getDimensionPixelSize(R.dimen.wood_search_bar_height), 0, - nested_scroll_view.bottom + nestedScrollView.bottom ) showKeyboard() } diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt index 7c95162..17bfc55 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailsActivity.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log -import androidx.appcompat.app.ActionBar import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment @@ -22,7 +21,7 @@ class LeafDetailsActivity : AppCompatActivity() { val colorUtil = getInstance(this) val appBarLayout = findViewById(R.id.wood_details_appbar) appBarLayout.setBackgroundColor(colorUtil.getTransactionColor(priority)) - val toolbar = findViewById(R.id.wood_details_toolbar) + val toolbar = findViewById(R.id.wood_details_toolbar) setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt index dfd75f5..71af393 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.kt @@ -37,16 +37,16 @@ class LeafListViewModel(application: Application) : AndroidViewModel(application ClearAsyncTask(leafDao).execute() } - private class DeleteAsyncTask(private val leafDao: LeafDao) : AsyncTask() { + private class DeleteAsyncTask(private val leafDao: LeafDao) : AsyncTask() { - override fun doInBackground(vararg params: Leaf?): Int { + override fun doInBackground(vararg params: Leaf): Int { return leafDao.deleteTransactions(*params) } } - private class ClearAsyncTask(private val leafDao: LeafDao) : AsyncTask() { + private class ClearAsyncTask(private val leafDao: LeafDao) : AsyncTask() { - override fun doInBackground(vararg params: Leaf?): Int { + override fun doInBackground(vararg params: Leaf): Int { return leafDao.clearAll() } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt b/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt index 95afa6f..765ae0f 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafViewHolder.kt @@ -16,17 +16,17 @@ internal class LeafViewHolder( private val listener: LeafAdapter.Listener ) : RecyclerView.ViewHolder(itemView) { - private val tv_time: TextView = itemView.findViewById(R.id.tv_time) - private val tv_tag: TextView = itemView.findViewById(R.id.tv_tag) - private val tv_id: TextView = itemView.findViewById(R.id.tv_id) - private val tv_body: TextView = itemView.findViewById(R.id.tv_body) + private val tvTime: TextView = itemView.findViewById(R.id.tv_time) + private val tvTag: TextView = itemView.findViewById(R.id.tv_tag) + private val tvId: TextView = itemView.findViewById(R.id.tv_id) + private val tvBody: TextView = itemView.findViewById(R.id.tv_body) fun bind(transaction: Leaf) { - tv_tag.text = transaction.tag - tv_time.text = timeDesc(transaction.createAt) - tv_body.text = getHighlightedText(transaction.body().toString()) - tv_id.text = String.format(Locale.getDefault(), "%d", transaction.id) - tv_tag.setTextColor(getInstance(context).getTransactionColor(transaction)) + tvTag.text = transaction.tag + tvTime.text = timeDesc(transaction.createAt) + tvBody.text = getHighlightedText(transaction.body.toString()) + tvId.text = String.format(Locale.getDefault(), "%d", transaction.id) + tvTag.setTextColor(getInstance(context).getTransactionColor(transaction)) itemView.setOnClickListener(View.OnClickListener { v: View? -> listener.onTransactionClicked( transaction diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt index a25f811..5e92dca 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.kt @@ -115,7 +115,7 @@ class LeavesCollectionFragment : Fragment(), LeafAdapter.Listener, SearchView.On override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.clear) { - viewModel!!.clearAll() + viewModel?.clearAll() clearBuffer() return true } else if (item.itemId == R.id.browse_sql) { diff --git a/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt b/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt index b3f7eda..caa011f 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/NotificationHelper.kt @@ -17,9 +17,9 @@ import com.tonytangandroid.wood.Wood.getLaunchIntent import com.tonytangandroid.wood.WoodColorUtil.Companion.getInstance internal class NotificationHelper(context: Context) { - private val mContext: Context = context - private val mNotificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val mColorUtil: WoodColorUtil = getInstance(context) + private val context: Context = context + private val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val colorUtil: WoodColorUtil = getInstance(context) init { setUpChannelIfNecessary() @@ -30,12 +30,12 @@ internal class NotificationHelper(context: Context) { val channel = NotificationChannel( CHANNEL_ID, - mContext.getString(R.string.wood_notification_category), + context.getString(R.string.wood_notification_category), NotificationManager.IMPORTANCE_LOW ) channel.setShowBadge(false) - mNotificationManager.createNotificationChannel(channel) + notificationManager.createNotificationChannel(channel) } } @@ -43,15 +43,15 @@ internal class NotificationHelper(context: Context) { fun show(transaction: Leaf, stickyNotification: Boolean) { addToBuffer(transaction) val builder = - NotificationCompat.Builder(mContext, CHANNEL_ID) + NotificationCompat.Builder(context, CHANNEL_ID) .setContentIntent( - SafePendingIntent.getActivity(mContext, 0, getLaunchIntent(mContext), 0) + SafePendingIntent.getActivity(context, 0, getLaunchIntent(context), 0) ) .setLocalOnly(true) .setSmallIcon(R.drawable.wood_icon) - .setColor(ContextCompat.getColor(mContext, R.color.wood_colorPrimary)) + .setColor(ContextCompat.getColor(context, R.color.wood_colorPrimary)) .setOngoing(stickyNotification) - .setContentTitle(mContext.getString(R.string.wood_notification_title)) + .setContentTitle(context.getString(R.string.wood_notification_title)) val inboxStyle = NotificationCompat.InboxStyle() var count = 0 @@ -73,38 +73,38 @@ internal class NotificationHelper(context: Context) { } builder.addAction(getDismissAction()) builder.addAction(getClearAction()) - mNotificationManager.notify(CHANNEL_ID.hashCode(), builder.build()) + notificationManager.notify(CHANNEL_ID.hashCode(), builder.build()) } private fun getNotificationText(transaction: Leaf): CharSequence { - val color = mColorUtil.getTransactionColor(transaction) - val text = transaction.body() + val color = colorUtil.getTransactionColor(transaction) + val text = transaction.body // Simple span no Truss required val spannableString = SpannableString(text) spannableString.setSpan( - ForegroundColorSpan(color), 0, text!!.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ForegroundColorSpan(color), 0, text?.length ?: 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE ) return spannableString } private fun getClearAction(): NotificationCompat.Action { - val clearTitle: CharSequence = mContext.getString(R.string.wood_clear) - val deleteIntent = Intent(mContext, ClearTransactionsService::class.java) + val clearTitle: CharSequence = context.getString(R.string.wood_clear) + val deleteIntent = Intent(context, ClearTransactionsService::class.java) val intent = - SafePendingIntent.getService(mContext, 11, deleteIntent, PendingIntent.FLAG_ONE_SHOT) + SafePendingIntent.getService(context, 11, deleteIntent, PendingIntent.FLAG_ONE_SHOT) return NotificationCompat.Action(R.drawable.wood_ic_delete_white_24dp, clearTitle, intent) } private fun getDismissAction(): NotificationCompat.Action { - val dismissTitle: CharSequence = mContext.getString(R.string.wood_dismiss) - val dismissIntent = Intent(mContext, DismissNotificationService::class.java) + val dismissTitle: CharSequence = context.getString(R.string.wood_dismiss) + val dismissIntent = Intent(context, DismissNotificationService::class.java) val intent = - SafePendingIntent.getService(mContext, 12, dismissIntent, PendingIntent.FLAG_ONE_SHOT) + SafePendingIntent.getService(context, 12, dismissIntent, PendingIntent.FLAG_ONE_SHOT) return NotificationCompat.Action(0, dismissTitle, intent) } fun dismiss() { - mNotificationManager.cancel(CHANNEL_ID.hashCode()) + notificationManager.cancel(CHANNEL_ID.hashCode()) } companion object { @@ -114,7 +114,6 @@ internal class NotificationHelper(context: Context) { private val TRANSACTION_BUFFER = LongSparseArray() private var TRANSACTION_COUNT = 0 - @JvmStatic @Synchronized fun clearBuffer() { TRANSACTION_BUFFER.clear() diff --git a/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt b/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt index 8242d82..3353041 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/RetentionManager.kt @@ -25,7 +25,7 @@ internal class RetentionManager(context: Context, retentionPeriod: WoodTree.Peri @Synchronized fun doMaintenance() { if (period > 0) { - val now = Date().getTime() + val now = Date().time if (isCleanupDue(now)) { Logger.i("Performing data retention maintenance...") deleteSince(getThreshold(now)) @@ -59,13 +59,11 @@ internal class RetentionManager(context: Context, retentionPeriod: WoodTree.Peri return if ((period == 0L)) now else now - period } - private fun toMillis(period: WoodTree.Period): Long { - when (period) { - WoodTree.Period.ONE_HOUR -> return TimeUnit.HOURS.toMillis(1) - WoodTree.Period.ONE_DAY -> return TimeUnit.DAYS.toMillis(1) - WoodTree.Period.ONE_WEEK -> return TimeUnit.DAYS.toMillis(7) - else -> return 0 - } + private fun toMillis(period: WoodTree.Period): Long = when (period) { + WoodTree.Period.ONE_HOUR -> TimeUnit.HOURS.toMillis(1) + WoodTree.Period.ONE_DAY -> TimeUnit.DAYS.toMillis(1) + WoodTree.Period.ONE_WEEK -> TimeUnit.DAYS.toMillis(7) + else -> 0 } companion object { diff --git a/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt b/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt index e9d4f1a..ce32e7d 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/Sampler.kt @@ -3,18 +3,11 @@ package com.tonytangandroid.wood import android.os.Handler import android.os.Looper -internal class Sampler(intervalInMills: Int, callback: Callback) { +internal class Sampler(intervalInMills: Int, val callback: Callback) { private val interval: Int = intervalInMills - private val callback: Callback - private val handler: Handler - + private val handler: Handler = Handler(Looper.getMainLooper()) private var currentRunnable: Counter? = null - init { - this.callback = callback - handler = Handler(Looper.getMainLooper()) - } - fun consume(event: T?) { val runnable = currentRunnable if (runnable == null) { @@ -24,13 +17,13 @@ internal class Sampler(intervalInMills: Int, callback: Callback) { handler.postDelayed(it, interval.toLong()) } } else { - if (runnable.state == Counter.Companion.STATE_CREATED - || runnable.state == Counter.Companion.STATE_QUEUED + if (runnable.state == Counter.STATE_CREATED + || runnable.state == Counter.STATE_QUEUED ) { // yet to emit (with in an interval) runnable.updateEvent(event) - } else if (runnable.state == Counter.Companion.STATE_RUNNING - || runnable.state == Counter.Companion.STATE_FINISHED + } else if (runnable.state == Counter.STATE_RUNNING + || runnable.state == Counter.STATE_FINISHED ) { // interval finished. open new batch Counter(event, callback).also { @@ -41,14 +34,12 @@ internal class Sampler(intervalInMills: Int, callback: Callback) { } } - class Counter internal constructor(event: T?, callback: Callback) : Runnable { - private val callback: Callback + class Counter internal constructor(event: T?, val callback: Callback) : Runnable { var state: Int private var event: T? init { this.event = event - this.callback = callback state = STATE_CREATED } diff --git a/wood/src/main/java/com/tonytangandroid/wood/Wood.kt b/wood/src/main/java/com/tonytangandroid/wood/Wood.kt index 09a29f5..525d91f 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Wood.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/Wood.kt @@ -47,7 +47,7 @@ object Wood { .setIcon(Icon.createWithResource(context, R.drawable.wood_icon)) .setIntent(getLaunchIntent(context).setAction(Intent.ACTION_VIEW)) .build() - shortcutManager.addDynamicShortcuts(mutableListOf(shortcut)) + shortcutManager.addDynamicShortcuts(listOf(shortcut)) return id } else { return null diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt index 4cdda6f..e8e64e2 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodColorUtil.kt @@ -6,26 +6,24 @@ import android.util.Log import androidx.core.content.ContextCompat internal class WoodColorUtil private constructor(context: Context) { - private val mColorDefault: Int = ContextCompat.getColor(context, R.color.wood_status_default) - private val mColorVerbose: Int = ContextCompat.getColor(context, R.color.wood_log_verbose) - private val mColorError: Int = ContextCompat.getColor(context, R.color.wood_log_error) - private val mColorAssert: Int = ContextCompat.getColor(context, R.color.wood_log_assert) - private val mColorInfo: Int = ContextCompat.getColor(context, R.color.wood_log_info) - private val mColorWarning: Int = ContextCompat.getColor(context, R.color.wood_log_warning) - private val mColorDebug: Int = ContextCompat.getColor(context, R.color.wood_log_debug) + private val colorDefault: Int = ContextCompat.getColor(context, R.color.wood_status_default) + private val colorVerbose: Int = ContextCompat.getColor(context, R.color.wood_log_verbose) + private val colorError: Int = ContextCompat.getColor(context, R.color.wood_log_error) + private val colorAssert: Int = ContextCompat.getColor(context, R.color.wood_log_assert) + private val colorInfo: Int = ContextCompat.getColor(context, R.color.wood_log_info) + private val colorWarning: Int = ContextCompat.getColor(context, R.color.wood_log_warning) + private val colorDebug: Int = ContextCompat.getColor(context, R.color.wood_log_debug) - fun getTransactionColor(transaction: Leaf): Int { - return getTransactionColor(transaction.priority) - } + fun getTransactionColor(transaction: Leaf): Int = getTransactionColor(transaction.priority) fun getTransactionColor(priority: Int): Int = when(priority) { - Log.VERBOSE -> mColorVerbose - Log.DEBUG -> mColorDebug - Log.INFO -> mColorInfo - Log.WARN -> mColorWarning - Log.ERROR -> mColorError - Log.ASSERT -> mColorAssert - else -> mColorDefault + Log.VERBOSE -> colorVerbose + Log.DEBUG -> colorDebug + Log.INFO -> colorInfo + Log.WARN -> colorWarning + Log.ERROR -> colorError + Log.ASSERT -> colorAssert + else -> colorDefault } companion object { diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt index ae8a892..8a5ee82 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodDatabase.kt @@ -12,12 +12,12 @@ internal abstract class WoodDatabase : RoomDatabase() { companion object { private var WOOD_DATABASE_INSTANCE: WoodDatabase? = null - fun getInstance(context: Context): WoodDatabase = WOOD_DATABASE_INSTANCE ?: Room.databaseBuilder( + fun getInstance(context: Context): WoodDatabase = WOOD_DATABASE_INSTANCE ?: Room.databaseBuilder( context, - WoodDatabase::class.java, - "WoodDatabase" - ).build().also { - WOOD_DATABASE_INSTANCE = it - } + WoodDatabase::class.java, + "WoodDatabase" + ).build().also { + WOOD_DATABASE_INSTANCE = it + } } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt b/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt index a9f3600..aad57f5 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt +++ b/wood/src/main/java/com/tonytangandroid/wood/WoodTree.kt @@ -139,16 +139,11 @@ class WoodTree @JvmOverloads constructor(context: Context, private val threadTag transaction.priority = priority transaction.createAt = System.currentTimeMillis() transaction.tag = tag - transaction.setLength(message.length) + transaction.length = message.length if (t != null) { message = message + "\n" + t.message + "\n" + ErrorUtil.asString(t) } - transaction.setBody( - message.substring( - 0, - min(message.length, maxContentLength) - ) + "" - ) + transaction.body = message.substring(0, min(message.length, maxContentLength)) + "" create(transaction) } @@ -189,7 +184,7 @@ class WoodTree @JvmOverloads constructor(context: Context, private val threadTag private val DEFAULT_RETENTION = Period.ONE_WEEK private const val PREF_WOOD_CONFIG = "pref_wood_config" private const val PREF_KEY_AUTO_SCROLL = "pref_key_auto_scroll" - @JvmStatic + fun autoScroll(context: Context): Boolean { val sharedPreferences = context.getSharedPreferences(PREF_WOOD_CONFIG, Context.MODE_PRIVATE) @@ -198,7 +193,7 @@ class WoodTree @JvmOverloads constructor(context: Context, private val threadTag private fun formatThreadTag(message: String?, threadTagPrefix: String): String { return String.format( - Locale.US, "[%s#%s]:%s", threadTagPrefix, Thread.currentThread().getName(), message + Locale.US, "[%s#%s]:%s", threadTagPrefix, Thread.currentThread().name, message ) } } diff --git a/wood/src/main/res/layout/wood_fragment_leaf_detail.xml b/wood/src/main/res/layout/wood_fragment_leaf_detail.xml index 4526410..741140a 100644 --- a/wood/src/main/res/layout/wood_fragment_leaf_detail.xml +++ b/wood/src/main/res/layout/wood_fragment_leaf_detail.xml @@ -29,8 +29,9 @@ android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/wood_ic_search_white_24dp" + android:contentDescription="search" app:fabSize="mini" - tools:ignore="HardcodedText" /> + tools:ignore="HardcodedText" /> - - -