diff --git a/README.md b/README.md index 688ce83..761ccca 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,35 @@ Wood is a simple in-app Timber log recorder. Wood records and persists all Timbe 1. Apps using Wood will display a notification showing a summary of ongoing Timber log activity. Tapping on the notification launches the full Wood UI. Apps can optionally suppress the notification, and launch the Wood UI directly from within their own interface. Log can be copied into clipboard or exported via a share intent. 2. Search Log by any keyword. 3. The main Wood activity is launched in its own task, allowing it to be displayed alongside the host app UI using Android 7.x multi-window support. +4. Allows to grab whole log as a string (recent records limited by file size). Example of usage: + +```kotlin + GlobalScope.launch { + val content = Wood.getAllTransactions(this@MainActivity, 1000000) + sendLogs(content.toByteArray()) + } + + private fun sendLogs(content: ByteArray) { + val storage = Firebase.storage + + val fileName = "${FORMAT_LOG_FILE_NAME.format(System.currentTimeMillis())}.log" + Timber.tag(TAG).w("Sending log file $fileName to cloud") + + val fileRef = storage.getReference("logs/$fileName") + fileRef.putBytes(content) + .addOnFailureListener { ex -> + Timber.tag(TAG).e(ex, "Failed to upload logs") + Snackbar.make(rootView, R.string.logs_upload_failed, Snackbar.LENGTH_LONG) + .show() + } + .addOnSuccessListener { + Timber.tag(TAG).w("Logs uploaded.") + Snackbar.make(rootView, R.string.logs_uploaded, Snackbar.LENGTH_LONG) + .show() + } + + } +``` Wood requires Android 4.1+ and Timber. diff --git a/build.gradle b/build.gradle index 74ad0c5..88d0591 100644 --- a/build.gradle +++ b/build.gradle @@ -33,8 +33,8 @@ ext { targetSdkVersion = 29 compileSdkVersion = 29 - versionCode = 19 - versionName = "2.0.0" + versionCode = 20 + versionName = "0.8.0" appcompatVersioon = '1.2.0' materialVersioon = '1.2.1' lifecycleVersion = '2.2.0' diff --git a/wood/src/main/java/com/tonytangandroid/wood/Callback.java b/wood/src/main/java/com/tonytangandroid/wood/Callback.java index 75a1e13..fc175c4 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Callback.java +++ b/wood/src/main/java/com/tonytangandroid/wood/Callback.java @@ -1,5 +1,5 @@ package com.tonytangandroid.wood; -interface Callback { +public interface Callback { void onEmit(T event); } \ No newline at end of file diff --git a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java index db0b31a..d587b34 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java +++ b/wood/src/main/java/com/tonytangandroid/wood/FormatUtils.java @@ -4,8 +4,10 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; +import android.util.Log; import android.widget.TextView; +import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -15,6 +17,8 @@ class FormatUtils { + private final static SimpleDateFormat FORMAT_TIME = new SimpleDateFormat("yy-MM-dd HH:mm:ss.SSS", Locale.US); + public static CharSequence formatTextHighlight(String text, String searchKey) { if (TextUtil.isNullOrWhiteSpace(text) || TextUtil.isNullOrWhiteSpace(searchKey)) { return text; @@ -47,11 +51,18 @@ public static void applyHighlightSpan(Spannable spannableString, List i } } - public static CharSequence getShareText(Leaf transaction) { return transaction.body(); } + public static CharSequence getShareTextFull(Leaf transaction) { + + return MessageFormat.format("{0} {1}/{2}: {3}", + timeDesc(transaction.getCreateAt()), + transactionPriority(transaction.getPriority()), + transaction.getTag(), + transaction.body()); + } private static CharSequence v(CharSequence charSequence) { return (charSequence != null) ? charSequence : ""; @@ -82,7 +93,24 @@ public static List highlightSearchKeyword(TextView textView, String sea 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); + return FORMAT_TIME.format(date); + } + + public static String transactionPriority(int priority) { + if (priority == Log.VERBOSE) { + return "V"; + } else if (priority == Log.DEBUG) { + return "D"; + } else if (priority == Log.INFO) { + return "I"; + } else if (priority == Log.WARN) { + return "W"; + } else if (priority == Log.ERROR) { + return "E"; + } else if (priority == Log.ASSERT) { + return "A"; + } else { + return String.valueOf(priority); + } } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java index 50f8a01..b50eb8b 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDao.java @@ -10,8 +10,12 @@ import androidx.room.RoomWarnings; import androidx.room.Update; import androidx.annotation.IntRange; + +import android.database.Cursor; import android.util.Log; +import java.util.List; + @Dao abstract class LeafDao { public static final int SEARCH_DEFAULT = Log.VERBOSE; @@ -32,7 +36,10 @@ abstract class LeafDao { public abstract int clearAll(); @Query("SELECT * FROM Leaf ORDER BY id DESC") - public abstract DataSource.Factory getAllTransactions(); + public abstract DataSource.Factory getPagedTransactions(); + + @Query("SELECT * FROM Leaf ORDER BY id DESC") + public abstract Cursor getAllTransactions(); @Query("SELECT * FROM Leaf WHERE id = :id") public abstract LiveData getTransactionsWithId(long id); diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java index 02dcc0e..a86d0bb 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafDetailFragment.java @@ -1,20 +1,10 @@ package com.tonytangandroid.wood; -import androidx.lifecycle.ViewModelProviders; 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import androidx.fragment.app.Fragment; -import androidx.core.widget.NestedScrollView; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; import android.text.Editable; import android.text.Spannable; import android.text.SpannableString; @@ -31,6 +21,17 @@ 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.ViewModelProviders; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; @@ -312,12 +313,6 @@ private void copy(CharSequence text) { } 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)); + TextUtil.share(requireContext(), content); } - - } \ No newline at end of file diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java index f23ed36..18ce12b 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java +++ b/wood/src/main/java/com/tonytangandroid/wood/LeafListViewModel.java @@ -1,12 +1,13 @@ 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; -import android.os.AsyncTask; public class LeafListViewModel extends AndroidViewModel { @@ -18,12 +19,12 @@ public class LeafListViewModel extends AndroidViewModel { .setEnablePlaceholders(true) .build(); private final LeafDao mLeafDao; - private LiveData> mTransactions; + private final LiveData> mTransactions; public LeafListViewModel(Application application) { super(application); mLeafDao = WoodDatabase.getInstance(application).leafDao(); - DataSource.Factory factory = mLeafDao.getAllTransactions(); + DataSource.Factory factory = mLeafDao.getPagedTransactions(); mTransactions = new LivePagedListBuilder<>(factory, config).build(); } diff --git a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java index cca4a24..39ac210 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java +++ b/wood/src/main/java/com/tonytangandroid/wood/LeavesCollectionFragment.java @@ -1,16 +1,6 @@ package com.tonytangandroid.wood; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.ViewModelProviders; -import androidx.paging.PagedList; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.SearchView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -18,6 +8,17 @@ 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.ViewModelProviders; +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; diff --git a/wood/src/main/java/com/tonytangandroid/wood/ReadAllTransactions.java b/wood/src/main/java/com/tonytangandroid/wood/ReadAllTransactions.java new file mode 100644 index 0000000..d3dff7e --- /dev/null +++ b/wood/src/main/java/com/tonytangandroid/wood/ReadAllTransactions.java @@ -0,0 +1,40 @@ +package com.tonytangandroid.wood; + +import android.database.Cursor; + +public class ReadAllTransactions { + private final LeafDao leafDao; + public int maxSize = 100000; + + public ReadAllTransactions(LeafDao leafDao) { + this.leafDao = leafDao; + } + + public String load(){ + final StringBuilder sb = new StringBuilder(); + + try (Cursor cursor = leafDao.getAllTransactions()) { + Leaf leaf = new Leaf(); + if (cursor.isAfterLast()) return "No data"; + int cCreateAt = cursor.getColumnIndex("createAt"); + int cTag = cursor.getColumnIndex("tag"); + int cPriority = cursor.getColumnIndex("priority"); + int cBody = cursor.getColumnIndex("body"); + while (cursor.moveToNext()) { + leaf.setCreateAt(cursor.getLong(cCreateAt)); + leaf.setTag(cursor.getString(cTag)); + leaf.setPriority(cursor.getInt(cPriority)); + leaf.setBody(cursor.getString(cBody)); + + sb.insert(0, "\n"); + sb.insert(0, FormatUtils.getShareTextFull(leaf)); + + if (sb.length() > maxSize){ + break; + } + } + } + + return sb.toString(); + } +} diff --git a/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java b/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java index e7cf39c..7dc1ec4 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java +++ b/wood/src/main/java/com/tonytangandroid/wood/TextUtil.java @@ -1,6 +1,8 @@ package com.tonytangandroid.wood; +import android.content.Context; +import android.content.Intent; import android.os.Build; import androidx.appcompat.widget.AppCompatTextView; import android.text.PrecomputedText; @@ -65,4 +67,12 @@ public interface AsyncTextProvider { AppCompatTextView getTextView(); } + + public static void share(Context context, CharSequence content) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, content); + sendIntent.setType("text/plain"); + context.startActivity(Intent.createChooser(sendIntent, null)); + } } diff --git a/wood/src/main/java/com/tonytangandroid/wood/Wood.java b/wood/src/main/java/com/tonytangandroid/wood/Wood.java index 83c745e..c329860 100644 --- a/wood/src/main/java/com/tonytangandroid/wood/Wood.java +++ b/wood/src/main/java/com/tonytangandroid/wood/Wood.java @@ -56,4 +56,16 @@ public static String addAppShortcut(Context context) { } } + + /** + * Loads transactions to a string + * This method is blocking so it should be used only in a backgound thread or in a coroutine + * @param maxSize Approximate maximum content size to return. In fact the size can be slightly bigger + */ + public static String getAllTransactions(Context context, int maxSize){ + final LeafDao leafDao = WoodDatabase.getInstance(context).leafDao(); + final ReadAllTransactions task = new ReadAllTransactions(leafDao); + task.maxSize = maxSize; + return task.load(); + } } \ No newline at end of file diff --git a/wood/src/main/res/values/strings.xml b/wood/src/main/res/values/strings.xml index fa31466..1fc1b1a 100644 --- a/wood/src/main/res/values/strings.xml +++ b/wood/src/main/res/values/strings.xml @@ -12,4 +12,5 @@ Wood Timber log notifications Search Request and Headers Copy + The content for sharing is being prepared