Sample app, update check

This commit is contained in:
Dimitry Ivanov 2020-07-11 15:52:01 +03:00
parent 086494bd97
commit c96ea690f6
26 changed files with 452 additions and 22 deletions

View File

@ -3,6 +3,15 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
def gitSha = { ->
def output = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = output
}
return output.toString().trim()
}.memoize()
android { android {
compileSdkVersion config['compile-sdk'] compileSdkVersion config['compile-sdk']
@ -17,7 +26,10 @@ android {
resConfig 'en' resConfig 'en'
setProperty("archivesBaseName", "markwon-$versionName") setProperty("archivesBaseName", "markwon")
buildConfigField 'String', 'GIT_SHA', "\"${gitSha()}\""
buildConfigField 'String', 'GIT_REPOSITORY', '"https://github.com/noties/Markwon"'
final def scheme = 'markwon' final def scheme = 'markwon'
buildConfigField 'String', 'DEEPLINK_SCHEME', "\"$scheme\"" buildConfigField 'String', 'DEEPLINK_SCHEME', "\"$scheme\""
@ -41,6 +53,37 @@ android {
java.srcDirs += '../sample-utils/annotations' java.srcDirs += '../sample-utils/annotations'
} }
} }
signingConfigs {
config {
final def keystoreFile = project.file('keystore.jks')
final def keystoreFilePassword = 'MARKWON_KEYSTORE_FILE_PASSWORD'
final def keystoreAlias = 'MARKWON_KEY_ALIAS'
final def keystoreAliasPassword = 'MARKWON_KEY_ALIAS_PASSWORD'
final def properties = [
keystoreFilePassword,
keystoreAlias,
keystoreAliasPassword
]
if (!keystoreFile.exists()) {
throw new IllegalStateException("No '${keystoreFile.name}' file is found.")
}
final def missingProperties = properties.findAll { !project.hasProperty(it) }
if (!missingProperties.isEmpty()) {
throw new IllegalStateException("Missing required signing properties: $missingProperties")
}
storeFile keystoreFile
storePassword project[keystoreFilePassword]
keyAlias project[keystoreAlias]
keyPassword project[keystoreAliasPassword]
}
}
} }
kapt { kapt {

24
app-sample/deploy.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env sh
# abort on errors
set -e
# build
../gradlew :app-sample:clean
../gradlew :app-sample:assembleDebug
# navigate into the build output directory
cd ./build/outputs/apk/debug/
revision=$(git rev-parse --short HEAD)
echo "output.json" > ./.gitignore
echo "$revision" > ./version
git init
git add -A
git commit -m "sample $revision"
git push -f git@github.com:noties/Markwon.git master:sample-store
cd -

BIN
app-sample/keystore.jks Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"version": 1,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "io.noties.markwon.app",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 1,
"versionName": "1",
"enabled": true,
"outputFile": "markwon-4.4.1-SNAPSHOT-release.apk"
}
]
}

View File

@ -1,8 +1,12 @@
package io.noties.markwon.app.sample.ui package io.noties.markwon.app.sample.ui
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -15,20 +19,26 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.noties.adapt.Adapt import io.noties.adapt.Adapt
import io.noties.adapt.DiffUtilDataSetChanged import io.noties.adapt.DiffUtilDataSetChanged
import io.noties.adapt.Item
import io.noties.debug.Debug import io.noties.debug.Debug
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.App import io.noties.markwon.app.App
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.app.readme.ReadMeActivity import io.noties.markwon.app.readme.ReadMeActivity
import io.noties.markwon.app.sample.Sample import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.sample.SampleItem
import io.noties.markwon.app.sample.SampleManager import io.noties.markwon.app.sample.SampleManager
import io.noties.markwon.app.sample.SampleSearch import io.noties.markwon.app.sample.SampleSearch
import io.noties.markwon.app.sample.ui.adapt.CheckForUpdateItem
import io.noties.markwon.app.sample.ui.adapt.SampleItem
import io.noties.markwon.app.sample.ui.adapt.VersionItem
import io.noties.markwon.app.utils.Cancellable import io.noties.markwon.app.utils.Cancellable
import io.noties.markwon.app.utils.UpdateUtils
import io.noties.markwon.app.utils.displayName import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.hidden import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.onPreDraw import io.noties.markwon.app.utils.onPreDraw
import io.noties.markwon.app.utils.recyclerView import io.noties.markwon.app.utils.recyclerView
import io.noties.markwon.app.utils.stackTraceString
import io.noties.markwon.app.utils.tagDisplayName import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.app.widget.SearchBar import io.noties.markwon.app.widget.SearchBar
import io.noties.markwon.movement.MovementMethodPlugin import io.noties.markwon.movement.MovementMethodPlugin
@ -50,6 +60,13 @@ class SampleListFragment : Fragment() {
private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null
private var cancellable: Cancellable? = null private var cancellable: Cancellable? = null
private var checkForUpdateCancellable: Cancellable? = null
private lateinit var progressBar: View
private val versionItem: VersionItem by lazy(LazyThreadSafetyMode.NONE) {
VersionItem()
}
private val sampleManager: SampleManager private val sampleManager: SampleManager
get() = App.sampleManager get() = App.sampleManager
@ -73,6 +90,8 @@ class SampleListFragment : Fragment() {
val context = requireContext() val context = requireContext()
progressBar = view.findViewById(R.id.progress_bar)
val searchBar: SearchBar = view.findViewById(R.id.search_bar) val searchBar: SearchBar = view.findViewById(R.id.search_bar)
searchBar.onSearchListener = { searchBar.onSearchListener = {
search = it search = it
@ -187,16 +206,30 @@ class SampleListFragment : Fragment() {
} }
} }
private fun bindSamples(samples: List<Sample>) { private fun bindSamples(samples: List<Sample>, addVersion: Boolean) {
val items = samples.map {
SampleItem( val items: List<Item<*>> = samples
markwon, .map {
it, SampleItem(
{ artifact -> openArtifact(artifact) }, markwon,
{ tag -> openTag(tag) }, it,
{ sample -> openSample(sample) } { artifact -> openArtifact(artifact) },
) { tag -> openTag(tag) },
} { sample -> openSample(sample) }
)
}
.let {
if (addVersion) {
val list: List<Item<*>> = it
list.toMutableList().apply {
add(0, CheckForUpdateItem(this@SampleListFragment::checkForUpdate))
add(0, versionItem)
}
} else {
it
}
}
adapt.setItems(items) adapt.setItems(items)
val recyclerView = adapt.recyclerView ?: return val recyclerView = adapt.recyclerView ?: return
@ -218,6 +251,66 @@ class SampleListFragment : Fragment() {
} }
} }
private fun checkForUpdate() {
val current = checkForUpdateCancellable
if (current != null && !current.isCancelled) {
return
}
progressBar.hidden = false
checkForUpdateCancellable = UpdateUtils.checkForUpdate { result ->
progressBar.post {
processUpdateResult(result)
}
}
}
private fun processUpdateResult(result: UpdateUtils.Result) {
val context = context ?: return
progressBar.hidden = true
val builder = AlertDialog.Builder(context)
when (result) {
is UpdateUtils.Result.UpdateAvailable -> {
val md = """
## Update available
Would you like to download it?
""".trimIndent()
builder.setMessage(markwon.toMarkdown(md))
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton("Download") { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(result.url))
startActivity(Intent.createChooser(intent, null))
}
}
is UpdateUtils.Result.NoUpdate -> {
val md = """
## No update
You are using latest version (${BuildConfig.GIT_SHA})
""".trimIndent()
builder.setMessage(markwon.toMarkdown(md))
builder.setPositiveButton(android.R.string.ok, null)
}
is UpdateUtils.Result.Error -> {
// trimIndent is confused by tabs in stack trace
val md = """
## Error
```
${result.throwable.stackTraceString()}
```
"""
builder.setMessage(markwon.toMarkdown(md))
builder.setPositiveButton(android.R.string.ok, null)
}
}
builder.show()
}
private fun openArtifact(artifact: MarkwonArtifact) { private fun openArtifact(artifact: MarkwonArtifact) {
Debug.i(artifact) Debug.i(artifact)
openResultFragment(init(artifact)) openResultFragment(init(artifact))
@ -262,7 +355,8 @@ class SampleListFragment : Fragment() {
} }
cancellable = sampleManager.samples(sampleSearch) { cancellable = sampleManager.samples(sampleSearch) {
bindSamples(it) val addVersion = sampleSearch is SampleSearch.All && TextUtils.isEmpty(sampleSearch.text)
bindSamples(it, addVersion)
} }
} }

View File

@ -0,0 +1,22 @@
package io.noties.markwon.app.sample.ui.adapt
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.noties.adapt.Item
import io.noties.markwon.app.R
class CheckForUpdateItem(private val action: () -> Unit) : Item<CheckForUpdateItem.Holder>(42L) {
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.adapt_check_for_update, parent, false))
}
override fun render(holder: Holder) {
holder.button.setOnClickListener { action() }
}
class Holder(view: View) : Item.Holder(view) {
val button: View = requireView(R.id.button)
}
}

View File

@ -1,4 +1,4 @@
package io.noties.markwon.app.sample package io.noties.markwon.app.sample.ui.adapt
import android.text.Spanned import android.text.Spanned
import android.view.LayoutInflater import android.view.LayoutInflater
@ -8,6 +8,7 @@ import android.widget.TextView
import io.noties.adapt.Item import io.noties.adapt.Item
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.R import io.noties.markwon.app.R
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.utils.displayName import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.hidden import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.tagDisplayName import io.noties.markwon.app.utils.tagDisplayName

View File

@ -0,0 +1,84 @@
package io.noties.markwon.app.sample.ui.adapt
import android.content.Context
import android.text.Spanned
import android.text.TextPaint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.noties.adapt.Item
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.LinkResolver
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.R
import io.noties.markwon.core.CoreProps
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.core.spans.LinkSpan
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.movement.MovementMethodPlugin
import org.commonmark.node.Link
class VersionItem : Item<VersionItem.Holder>(42L) {
private lateinit var context: Context
private val markwon: Markwon by lazy(LazyThreadSafetyMode.NONE) {
Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(MovementMethodPlugin.link())
.usePlugin(HtmlPlugin.create())
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.setFactory(Link::class.java) { configuration, props ->
LinkSpanNoUnderline(
configuration.theme(),
CoreProps.LINK_DESTINATION.require(props),
configuration.linkResolver()
)
}
}
})
.build()
}
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
val md = """
<a href="${BuildConfig.GIT_REPOSITORY}/blob/master/CHANGELOG.md">
![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot)
![changelog](https://fonts.gstatic.com/s/i/materialicons/open_in_browser/v6/24px.svg?download=true)
</a>
""".trimIndent()
markwon.toMarkdown(md)
}
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
context = parent.context
return Holder(inflater.inflate(R.layout.adapt_version, parent, false))
}
override fun render(holder: Holder) {
markwon.setParsedMarkdown(holder.textView, text)
}
class Holder(view: View) : Item.Holder(view) {
val textView: TextView = requireView(R.id.text_view)
}
class LinkSpanNoUnderline(
theme: MarkwonTheme,
destination: String,
resolver: LinkResolver
) : LinkSpan(theme, destination, resolver) {
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}
}

View File

@ -11,6 +11,7 @@ import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.app.BuildConfig;
import io.noties.markwon.app.sample.Tags; import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.InlineProcessor; import io.noties.markwon.inlineparser.InlineProcessor;
@ -78,7 +79,7 @@ class IssueInlineProcessor extends InlineProcessor {
@NonNull @NonNull
private static String createIssueOrPullRequestLinkDestination(@NonNull String id) { private static String createIssueOrPullRequestLinkDestination(@NonNull String id) {
return "https://github.com/noties/Markwon/issues/" + id; return BuildConfig.GIT_REPOSITORY + "/issues/" + id;
} }
} }

View File

@ -13,6 +13,7 @@ import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps; import io.noties.markwon.RenderProps;
import io.noties.markwon.SpannableBuilder; import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.BuildConfig;
import io.noties.markwon.app.sample.Tags; import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CorePlugin; import io.noties.markwon.core.CorePlugin;
@ -87,7 +88,7 @@ class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListe
// issues and pull-requests on github follow the same pattern and we // issues and pull-requests on github follow the same pattern and we
// cannot know for sure which one it is, but if we use issues for all types, // cannot know for sure which one it is, but if we use issues for all types,
// github will automatically redirect to pull-request if it's the one which is opened // github will automatically redirect to pull-request if it's the one which is opened
return "https://github.com/noties/Markwon/issues/" + number; return BuildConfig.GIT_REPOSITORY + "/issues/" + number;
} }
@NonNull @NonNull

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.sample.Tags import io.noties.markwon.app.sample.Tags
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.image.ImagesPlugin import io.noties.markwon.image.ImagesPlugin
@ -23,7 +24,7 @@ class ToastDynamicContentSample : MarkwonTextViewSample() {
val md = """ val md = """
# Head! # Head!
![alt](https://github.com/noties/Markwon/raw/master/art/markwon_logo.png) ![alt](${BuildConfig.GIT_REPOSITORY}/raw/master/art/markwon_logo.png)
Do you see an image? Do you see an image?
""".trimIndent() """.trimIndent()

View File

@ -28,6 +28,7 @@ import java.util.List;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder; import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.BuildConfig;
import io.noties.markwon.app.R; import io.noties.markwon.app.R;
import io.noties.markwon.app.sample.Tags; import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonSample; import io.noties.markwon.app.sample.ui.MarkwonSample;
@ -56,7 +57,7 @@ public class HtmlDetailsSample extends MarkwonSample {
@Override @Override
protected int getLayoutResId() { protected int getLayoutResId() {
return R.layout.activity_html_details; return R.layout.sample_html_details;
} }
@Override @Override
@ -83,7 +84,8 @@ public class HtmlDetailsSample extends MarkwonSample {
"* list\n" + "* list\n" +
"* with\n" + "* with\n" +
"\n\n" + "\n\n" +
"![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" + "![img](" + BuildConfig.GIT_REPOSITORY + "/raw/master/art/markwon_logo.png)\n\n" +
"" +
" 1. nested\n" + " 1. nested\n" +
" 1. items\n" + " 1. items\n" +
"\n" + "\n" +

View File

@ -2,6 +2,7 @@ package io.noties.markwon.app.samples.movementmethod
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.sample.Tags import io.noties.markwon.app.sample.Tags
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact import io.noties.markwon.sample.annotations.MarkwonArtifact
@ -22,7 +23,7 @@ class ExplicitMovementMethodSample : MarkwonTextViewSample() {
If `TextView` already has a movement method specified, then `Markwon` If `TextView` already has a movement method specified, then `Markwon`
won't be applying a default one. You can specify movement won't be applying a default one. You can specify movement
method via call to `setMovementMethod`. If your movement method can method via call to `setMovementMethod`. If your movement method can
handle [links](https://github.com/noties/Markwon) then link would be handle [links](${BuildConfig.GIT_REPOSITORY}) then link would be
_clickable_ _clickable_
""".trimIndent() """.trimIndent()

View File

@ -1,6 +1,7 @@
package io.noties.markwon.app.samples.movementmethod package io.noties.markwon.app.samples.movementmethod
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.sample.Tags import io.noties.markwon.app.sample.Tags
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact import io.noties.markwon.sample.annotations.MarkwonArtifact
@ -18,7 +19,7 @@ class ImplicitMovementMethodSample : MarkwonTextViewSample() {
val md = """ val md = """
# Implicit movement method # Implicit movement method
By default `Markwon` applies `LinkMovementMethod` if it is missing, By default `Markwon` applies `LinkMovementMethod` if it is missing,
so in order for [links](https://github.com/noties/Markwon) to be clickable so in order for [links](${BuildConfig.GIT_REPOSITORY}) to be clickable
nothing special should be done nothing special should be done
""".trimIndent() """.trimIndent()

View File

@ -0,0 +1,13 @@
package io.noties.markwon.app.utils
import java.io.PrintWriter
import java.io.StringWriter
object ThrowableUtils
fun Throwable.stackTraceString(): String {
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
this.printStackTrace(printWriter)
return stringWriter.toString()
}

View File

@ -0,0 +1,10 @@
package io.noties.markwon.app.utils
@Suppress("unused")
class UncaughtExceptionHandler(private val origin: Thread.UncaughtExceptionHandler?)
: Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread?, e: Throwable?) {
}
}

View File

@ -0,0 +1,63 @@
package io.noties.markwon.app.utils
import io.noties.markwon.app.App
import io.noties.markwon.app.BuildConfig
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
object UpdateUtils {
sealed class Result {
class UpdateAvailable(val url: String) : Result()
object NoUpdate : Result()
class Error(val throwable: Throwable) : Result()
}
fun checkForUpdate(updateAction: (Result) -> Unit): Cancellable {
var action: ((Result) -> Unit)? = updateAction
val future = App.executorService
.submit {
val url = "${BuildConfig.GIT_REPOSITORY}/raw/sample-store/version"
val request = Request.Builder()
.get()
.url(url)
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
action?.invoke(Result.Error(e))
}
override fun onResponse(call: Call, response: Response) {
try {
val revision = response.body()?.string()
val hasUpdate = revision != null && BuildConfig.GIT_SHA != revision
if (hasUpdate) {
action?.invoke(Result.UpdateAvailable(apkUrl))
} else {
action?.invoke(Result.NoUpdate)
}
} catch (e: IOException) {
action?.invoke(Result.Error(e))
}
}
})
}
return object : Cancellable {
override val isCancelled: Boolean
get() = future.isDone
override fun cancel() {
action = null
future.cancel(true)
}
}
}
private const val apkUrl = "${BuildConfig.GIT_REPOSITORY}/raw/sample-store/markwon-debug.apk"
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z" />
</vector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/content_padding"
android:paddingRight="@dimen/content_padding">
<Button
android:id="@+id/button"
style="@android:style/Widget.Material.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/check_for_update" />
</FrameLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/content_padding_double"
android:paddingTop="@dimen/content_padding"
android:paddingRight="@dimen/content_padding_double"
android:paddingBottom="@dimen/content_padding"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Badges here" />

View File

@ -48,6 +48,17 @@
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="16dip"
android:layout_marginTop="-8dip"
android:layout_marginBottom="-8dip"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -5,6 +5,8 @@
<string name="tab_bar_preview">Preview</string> <string name="tab_bar_preview">Preview</string>
<string name="tab_bar_code">Code</string> <string name="tab_bar_code">Code</string>
<string name="check_for_update">Check for update</string>
<string name="lorem"><![CDATA[ <string name="lorem"><![CDATA[
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.

Binary file not shown.

View File

@ -29,7 +29,6 @@ dependencies {
testImplementation deps['commons-io'] testImplementation deps['commons-io']
deps['test'].with { deps['test'].with {
testImplementation it['junit'] testImplementation it['junit']
testImplementation it['robolectric'] testImplementation it['robolectric']
testImplementation it['mockito'] testImplementation it['mockito']