...
 
Commits (3)
......@@ -27,13 +27,12 @@ class ColorViewModel(state: Bundle?) : ViewModel() {
var numbers = state?.getIntegerArrayList(STATE_NUMBERS) ?: buildItems()
fun onSaveInstanceState(state: Bundle) {
state.putIntegerArrayList(STATE_NUMBERS, numbers)
state.putIntegerArrayList(STATE_NUMBERS, ArrayList(numbers))
}
fun refresh() {
numbers = buildItems()
}
private fun buildItems() =
ArrayList<Int>(generateSequence { random.nextInt() }.take(25).toList())
private fun buildItems() = List(25) { random.nextInt() }
}
\ No newline at end of file
......@@ -63,13 +63,13 @@ class MainActivity : AppCompatActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when {
item.itemId == R.id.refresh -> {
return when(item.itemId) {
R.id.refresh -> {
vm.refresh()
colorAdapter.submitList(vm.numbers)
true
}
item.itemId == R.id.about -> {
R.id.about -> {
Toast.makeText(
this@MainActivity,
R.string.msg_toast,
......
......@@ -8,7 +8,7 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:theme="?attr/actionBarPopupTheme"
......
......@@ -6,10 +6,10 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="actionBarPopupTheme">@style/AppTheme.PopupOverlay</item>
<item name="actionBarPopupTheme">@style/PopupOverlay</item>
</style>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<style name="PopupOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="iconTint">@android:color/white</item>
</style>
......
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.commonsware.jetpack.bookmarker"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
dataBinding {
enabled = true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01"
implementation "androidx.room:room-runtime:2.1.0-alpha04"
implementation "androidx.room:room-coroutines:2.1.0-alpha04"
kapt "androidx.room:room-compiler:2.1.0-alpha04"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'com.github.bumptech.glide:glide:4.9.0'
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.jetpack.bookmarker"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
@BindingAdapter("imageUrl")
fun ImageView.loadImage(url: String?) {
url?.let {
Glide.with(context)
.load(it)
.into(this)
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.view.LayoutInflater
import android.view.ViewGroup
import com.commonsware.jetpack.bookmarker.databinding.RowBinding
import androidx.recyclerview.widget.ListAdapter
class BookmarkAdapter(private val inflater: LayoutInflater) :
ListAdapter<RowState, RowHolder>(RowState.DIFFER) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowHolder {
val binding = RowBinding.inflate(inflater, parent, false)
return RowHolder(binding)
}
override fun onBindViewHolder(holder: RowHolder, position: Int) {
holder.bind(getItem(position))
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
private const val DB_NAME = "bookmarks.db"
@Database(entities = [BookmarkEntity::class], version = 1)
abstract class BookmarkDatabase : RoomDatabase() {
abstract fun bookmarkStore(): BookmarkStore
companion object {
@Volatile
private var INSTANCE: BookmarkDatabase? = null
@Synchronized
operator fun get(context: Context): BookmarkDatabase {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context, BookmarkDatabase::class.java, DB_NAME)
.build()
}
return INSTANCE!!
}
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
class BookmarkEntity {
@PrimaryKey
var pageUrl: String = ""
var title: String? = null
var iconUrl: String? = null
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
class BookmarkModel(entity: BookmarkEntity) {
val pageUrl = entity.pageUrl
val title = entity.title
val iconUrl = entity.iconUrl
}
package com.commonsware.jetpack.bookmarker
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import java.net.URI
object BookmarkRepository {
suspend fun save(context: Context, pageUrl: String) =
withContext(Dispatchers.IO) {
val db: BookmarkDatabase = BookmarkDatabase[context]
val entity = BookmarkEntity()
val doc = Jsoup.connect(pageUrl).get()
entity.pageUrl = pageUrl
entity.title = doc.title()
// based on https://www.mkyong.com/java/jsoup-get-favicon-from-html-page/
val iconUrl: String? =
doc.head().select("link[href~=.*\\.(ico|png)]").first()?.attr("href")
?: doc.head().select("meta[itemprop=image]").first().attr("content")
if (iconUrl != null) {
val uri = URI(pageUrl)
entity.iconUrl = uri.resolve(iconUrl).toString()
}
db.bookmarkStore().save(entity)
BookmarkModel(entity)
}
fun load(context: Context): LiveData<List<BookmarkModel>> {
val db: BookmarkDatabase = BookmarkDatabase[context]
return Transformations.map(db.bookmarkStore().all()) { entities ->
entities.map { BookmarkModel(it) }
}
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
data class BookmarkResult(val content: BookmarkModel?, val error: Throwable?)
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface BookmarkStore {
@Query("SELECT * FROM BookmarkEntity ORDER BY title")
fun all(): LiveData<List<BookmarkEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(entity: BookmarkEntity)
}
package com.commonsware.jetpack.bookmarker
import androidx.lifecycle.Observer
class Event<out T>(private val content: T) {
private var hasBeenHandled = false
fun handle(handler: (T) -> Unit) {
if (!hasBeenHandled) {
hasBeenHandled = true
handler(content)
}
}
}
class EventObserver<T>(private val handler: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(value: Event<T>?) {
value?.handle(handler)
}
}
\ No newline at end of file
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var motor: MainMotor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
motor = ViewModelProviders.of(this)[MainMotor::class.java]
saveBookmark(intent)
val adapter = BookmarkAdapter(layoutInflater)
bookmarks.layoutManager = LinearLayoutManager(this)
bookmarks.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
bookmarks.adapter = adapter
motor.states.observe(
this,
Observer { state ->
when (state) {
is MainViewState.Content -> adapter.submitList(state.rows)
is MainViewState.Error -> {
Toast.makeText(
this@MainActivity, state.throwable.localizedMessage,
Toast.LENGTH_LONG
).show()
Log.e("Bookmarker", "Exception loading data", state.throwable)
}
}
})
motor.saveEvents.observe(this, Observer { event ->
event.handle { result ->
val message = if (result.error == null) {
result.content?.title + " was saved!"
} else {
result.error.localizedMessage
}
Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
}
})
}
private fun saveBookmark(intent: Intent) {
if (Intent.ACTION_SEND == intent.action) {
val pageUrl = getIntent().getStringExtra(Intent.EXTRA_STREAM)
?: getIntent().getStringExtra(Intent.EXTRA_TEXT)
if (pageUrl != null && Uri.parse(pageUrl).scheme!!.startsWith("http")) {
motor.save(pageUrl)
} else {
Toast.makeText(this, R.string.msg_invalid_url, Toast.LENGTH_LONG).show()
finish()
}
}
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
class MainMotor(application: Application) : AndroidViewModel(application) {
private val _saveEvents = MutableLiveData<Event<BookmarkResult>>()
val saveEvents: LiveData<Event<BookmarkResult>> = _saveEvents
val states: LiveData<MainViewState>
init {
states =
Transformations.map(BookmarkRepository.load(getApplication())) { models ->
val content = ArrayList<RowState>()
for (model in models) {
content.add(RowState(model))
}
MainViewState.Content(content)
}
}
fun save(pageUrl: String) {
viewModelScope.launch(Dispatchers.Main) {
_saveEvents.value = try {
val model = BookmarkRepository.save(getApplication(), pageUrl)
Event(BookmarkResult(model, null))
} catch (t: Throwable) {
Event(BookmarkResult(null, t))
}
}
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
sealed class MainViewState {
data class Content(val rows: List<RowState>) : MainViewState()
data class Error(val throwable: Throwable) : MainViewState()
}
\ No newline at end of file
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.content.Intent
import android.net.Uri
import androidx.recyclerview.widget.RecyclerView
import com.commonsware.jetpack.bookmarker.databinding.RowBinding
class RowHolder(private val binding: RowBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(state: RowState) {
binding.state = state
binding.executePendingBindings()
val pageUrl = state.pageUrl
binding.root.setOnClickListener {
val viewPage = Intent(Intent.ACTION_VIEW, Uri.parse(pageUrl))
binding.root.context.startActivity(viewPage)
}
}
}
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.bookmarker
import android.text.Spanned
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.DiffUtil
class RowState(model: BookmarkModel) {
val title: Spanned =
HtmlCompat.fromHtml(model.title ?: "", HtmlCompat.FROM_HTML_MODE_COMPACT)
val iconUrl = model.iconUrl
val pageUrl = model.pageUrl
companion object {
val DIFFER: DiffUtil.ItemCallback<RowState> =
object : DiffUtil.ItemCallback<RowState>() {
override fun areItemsTheSame(
oldItem: RowState,
newItem: RowState
): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(
oldItem: RowState,
newItem: RowState
): Boolean {
return oldItem.title.toString() == newItem.title.toString()
}
}
}
}
<vector xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmarks"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.recyclerview.widget.RecyclerView>
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="state"
type="com.commonsware.jetpack.bookmarker.RowState" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@{state.title}"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="A Really Awesome Web page" />
<ImageView
android:id="@+id/icon"
android:layout_width="0dp"
android:layout_height="64dp"
android:contentDescription="@string/icon"
app:imageUrl="@{state.iconUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#D2DBDA</color>
</resources>
\ No newline at end of file
<resources>
<string name="app_name">Bookmarker</string>
<string name="icon">Icon</string>
<string name="msg_invalid_url">Did not receive a valid URL!</string>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
......@@ -21,23 +21,20 @@ import android.content.Intent
import android.os.Bundle
import android.provider.ContactsContract
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
import kotlinx.android.synthetic.main.activity_main.*
private const val REQUEST_PICK = 1337
class MainActivity : AppCompatActivity() {
private lateinit var vm: ContactViewModel
private lateinit var viewButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewButton = findViewById(R.id.view)
vm = ViewModelProviders
.of(this, ContactViewModelFactory(savedInstanceState))
.get(ContactViewModel::class.java)
......@@ -56,7 +53,7 @@ class MainActivity : AppCompatActivity() {
}
}
viewButton.setOnClickListener { _ ->
view.setOnClickListener {
try {
startActivity(Intent(Intent.ACTION_VIEW, vm.contact))
} catch (e: Exception) {
......@@ -86,7 +83,7 @@ class MainActivity : AppCompatActivity() {
private fun updateViewButton() {
if (vm.contact != null) {
viewButton.isEnabled = true
view.isEnabled = true
}
}
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.commonsware.jetpack.contenteditor"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.jetpack.contenteditor"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
/*
Copyright (c) 2019 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
Covered in detail in the book _Elements of Android Jetpack_
https://commonsware.com/Jetpack
*/
package com.commonsware.jetpack.contenteditor
import android.Manifest
import android.app.Activity