リビジョン 0406a46c
| learn/android/TempRecorderClassicKt/.idea/vcs.xml | ||
|---|---|---|
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<project version="4">
|
||
|
<component name="VcsDirectoryMappings">
|
||
|
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||
|
</component>
|
||
|
</project>
|
||
| learn/android/TempRecorderClassicKt/app/src/main/AndroidManifest.xml | ||
|---|---|---|
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||
|
android:supportsRtl="true"
|
||
|
android:theme="@style/AppTheme">
|
||
|
<provider
|
||
|
android:name=".TempProvider"
|
||
|
android:authorities="com.torutk.temprecorder.kt.provider"
|
||
|
android:enabled="true"
|
||
|
android:exported="true"></provider>
|
||
|
|
||
|
<activity android:name=".MainActivity">
|
||
|
<intent-filter>
|
||
|
<action android:name="android.intent.action.MAIN" />
|
||
| learn/android/TempRecorderClassicKt/app/src/main/java/com/torutk/temprecorder/kt/MainActivity.kt | ||
|---|---|---|
|
package com.torutk.temprecorder.kt
|
||
|
|
||
|
import android.content.ContentValues
|
||
|
import android.database.ContentObserver
|
||
|
import androidx.appcompat.app.AppCompatActivity
|
||
|
import android.os.Bundle
|
||
|
import android.os.Handler
|
||
|
import kotlinx.android.synthetic.main.activity_main.*
|
||
|
import java.time.LocalDateTime
|
||
|
import java.time.format.DateTimeFormatter
|
||
|
|
||
|
val DATE_TIME_VIEW_FORMATTER = DateTimeFormatter.ofPattern("MM.dd HH:mm")
|
||
|
|
||
|
class MainActivity : AppCompatActivity() {
|
||
|
lateinit var tempAdapter: TempAdapter
|
||
|
lateinit var tempProviderObserver: ContentObserver
|
||
|
|
||
|
private var measuredAt: LocalDateTime = LocalDateTime.now() // 検温日時
|
||
|
set(dateTime: LocalDateTime) {
|
||
|
field = dateTime
|
||
|
measuredAtTextView.text = field.format(DATE_TIME_VIEW_FORMATTER)
|
||
|
}
|
||
|
|
||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||
|
super.onCreate(savedInstanceState)
|
||
|
setContentView(R.layout.activity_main)
|
||
|
dec10MinButton.setOnClickListener { measuredAt = measuredAt.minusMinutes(10) }
|
||
|
inc10MinButton.setOnClickListener { measuredAt = measuredAt.plusMinutes(10) }
|
||
|
with(integralNumberPicker) {
|
||
|
minValue = 35
|
||
|
maxValue = 40
|
||
|
value = 36
|
||
|
wrapSelectorWheel = false
|
||
|
}
|
||
|
with(fractionNumberPicker) {
|
||
|
minValue = 0
|
||
|
maxValue = 9
|
||
|
value = 5
|
||
|
}
|
||
|
submitButton.setOnClickListener { submitTemp() }
|
||
|
tempRecyclerView.setHasFixedSize(true)
|
||
|
tempAdapter = TempAdapter(null)
|
||
|
tempRecyclerView.adapter = tempAdapter
|
||
|
tempProviderObserver = object : ContentObserver(Handler()) {
|
||
|
override fun onChange(selfChange: Boolean) {
|
||
|
super.onChange(selfChange)
|
||
|
queryTemp()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
override fun onStart() {
|
||
|
super.onStart()
|
||
|
queryTemp()
|
||
|
}
|
||
|
|
||
|
override fun onResume() {
|
||
|
super.onResume()
|
||
|
measuredAt = LocalDateTime.now()
|
||
|
contentResolver.registerContentObserver(TempContract.CONTENT_URI, true, tempProviderObserver)
|
||
|
}
|
||
|
|
||
|
override fun onPause() {
|
||
|
super.onPause()
|
||
|
contentResolver.unregisterContentObserver(tempProviderObserver)
|
||
|
}
|
||
|
|
||
|
// 検温履歴の取得
|
||
|
private fun queryTemp() {
|
||
|
val cursor = contentResolver.query(
|
||
|
TempContract.CONTENT_URI, null, null, null, "_id DESC"
|
||
|
)
|
||
|
cursor?.let { tempAdapter.swapCursor(cursor) }
|
||
|
}
|
||
|
|
||
|
// 検温結果の登録
|
||
|
private fun submitTemp() {
|
||
|
val values = ContentValues().apply {
|
||
|
put(TempContract.TempEntry.MEASURED_AT, measuredAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
||
|
put(TempContract.TempEntry.MEASUREMENT, integralNumberPicker.value + fractionNumberPicker.value / 10.0)
|
||
|
}
|
||
|
contentResolver.insert(TempContract.CONTENT_URI, values)
|
||
|
}
|
||
|
}
|
||
| learn/android/TempRecorderClassicKt/app/src/main/java/com/torutk/temprecorder/kt/TempAdapter.kt | ||
|---|---|---|
|
package com.torutk.temprecorder.kt
|
||
|
|
||
|
import android.database.Cursor
|
||
|
import android.view.LayoutInflater
|
||
|
import android.view.View
|
||
|
import android.view.ViewGroup
|
||
|
import android.widget.TextView
|
||
|
import androidx.recyclerview.widget.RecyclerView
|
||
|
import kotlinx.android.synthetic.main.list_item.view.*
|
||
|
import java.time.LocalDateTime
|
||
|
import java.time.format.DateTimeFormatter
|
||
|
|
||
|
class TempAdapter(private var cursor: Cursor?) : RecyclerView.Adapter<TempAdapter.TempViewHolder>() {
|
||
|
|
||
|
class TempViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||
|
val dateTimeView: TextView = itemView.dateTimeView
|
||
|
val temperatureView: TextView = itemView.temperatureView
|
||
|
}
|
||
|
|
||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TempViewHolder {
|
||
|
return TempViewHolder(
|
||
|
LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
|
||
|
)
|
||
|
}
|
||
|
|
||
|
override fun onBindViewHolder(holder: TempViewHolder, position: Int) {
|
||
|
cursor?.let {
|
||
|
if (it.isClosed) return
|
||
|
it.moveToPosition(position)
|
||
|
holder.dateTimeView.text = getDateTime(it)
|
||
|
holder.temperatureView.text = "%.1f".format(getTemperature(it))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private fun getDateTime(cursor: Cursor): String {
|
||
|
val isoDateTimetext = cursor.getString(cursor.getColumnIndex(TempContract.TempEntry.MEASURED_AT))
|
||
|
val measuredAt = LocalDateTime.parse(isoDateTimetext, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||
|
return measuredAt.format(DATE_TIME_VIEW_FORMATTER)
|
||
|
}
|
||
|
|
||
|
private fun getTemperature(cursor: Cursor): Double {
|
||
|
return cursor.getDouble(cursor.getColumnIndex(TempContract.TempEntry.MEASUREMENT))
|
||
|
}
|
||
|
|
||
|
override fun getItemCount() = cursor?.let { if (it.isClosed) 0 else it.count } ?: 0
|
||
|
|
||
|
fun swapCursor(newCursor: Cursor) {
|
||
|
if (cursor == newCursor) return
|
||
|
cursor = newCursor
|
||
|
if (cursor != null) {
|
||
|
notifyDataSetChanged()
|
||
|
} else {
|
||
|
notifyItemRangeChanged(0, itemCount)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
| learn/android/TempRecorderClassicKt/app/src/main/java/com/torutk/temprecorder/kt/TempContract.kt | ||
|---|---|---|
|
package com.torutk.temprecorder.kt
|
||
|
|
||
|
import android.net.Uri
|
||
|
import android.provider.BaseColumns
|
||
|
|
||
|
class TempContract private constructor() {
|
||
|
companion object {
|
||
|
const val AUTHORITY = "com.tourtk.temprecorder.kt.provider"
|
||
|
val CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/temperatures")
|
||
|
}
|
||
|
|
||
|
class TempEntry private constructor() {
|
||
|
companion object {
|
||
|
const val _ID = BaseColumns._ID
|
||
|
const val _COUNT = BaseColumns._COUNT
|
||
|
const val TABLE_NAME = "Temperatures"
|
||
|
const val MEASURED_AT = "measured_at"
|
||
|
const val MEASUREMENT = "measurement"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
| learn/android/TempRecorderClassicKt/app/src/main/java/com/torutk/temprecorder/kt/TempDbHelper.kt | ||
|---|---|---|
|
package com.torutk.temprecorder.kt
|
||
|
|
||
|
import android.content.Context
|
||
|
import android.database.sqlite.SQLiteDatabase
|
||
|
import android.database.sqlite.SQLiteOpenHelper
|
||
|
|
||
|
internal const val DATABASE_NAME = "body_temperature.db"
|
||
|
internal const val DATABASE_VERSION = 1
|
||
|
internal val CREATE_TABLE = """
|
||
|
|CREATE TABLE ${TempContract.TempEntry.TABLE_NAME} (
|
||
|
| ${TempContract.TempEntry._ID} INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
| ${TempContract.TempEntry.MEASURED_AT} TEXT NOT NULL,
|
||
|
| ${TempContract.TempEntry.MEASUREMENT} REAL NOT NULL);
|
||
|
""".trimMargin()
|
||
|
|
||
|
class TempDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||
|
override fun onCreate(db: SQLiteDatabase?) {
|
||
|
db?.execSQL(CREATE_TABLE)
|
||
|
}
|
||
|
|
||
|
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||
|
db?.execSQL("DROP TABLE IF EXISTS ${TempContract.TempEntry.TABLE_NAME}")
|
||
|
onCreate(db)
|
||
|
}
|
||
|
}
|
||
| learn/android/TempRecorderClassicKt/app/src/main/java/com/torutk/temprecorder/kt/TempProvider.kt | ||
|---|---|---|
|
package com.torutk.temprecorder.kt
|
||
|
|
||
|
import android.content.ContentProvider
|
||
|
import android.content.ContentValues
|
||
|
import android.content.UriMatcher
|
||
|
import android.database.Cursor
|
||
|
import android.database.sqlite.SQLiteDatabase
|
||
|
import android.net.Uri
|
||
|
import java.lang.IllegalArgumentException
|
||
|
|
||
|
private const val TEMPERATURES = 1
|
||
|
private const val TEMPERATURES_ID = 2
|
||
|
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
|
||
|
addURI(TempContract.AUTHORITY, "temperatures", TEMPERATURES)
|
||
|
addURI(TempContract.AUTHORITY, "temperatures/#", TEMPERATURES_ID)
|
||
|
}
|
||
|
|
||
|
class TempProvider : ContentProvider() {
|
||
|
|
||
|
private lateinit var dbHelper: TempDbHelper
|
||
|
|
||
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
||
|
TODO("Implement this to handle requests to delete one or more rows")
|
||
|
}
|
||
|
|
||
|
override fun getType(uri: Uri): String? {
|
||
|
TODO(
|
||
|
"Implement this to handle requests for the MIME type of the data" +
|
||
|
"at the given URI"
|
||
|
)
|
||
|
}
|
||
|
|
||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||
|
val database = dbHelper.writableDatabase
|
||
|
val id = database.insert(TempContract.TempEntry.TABLE_NAME, null, values)
|
||
|
val insertedUri = Uri.withAppendedPath(uri, id.toString())
|
||
|
context?.contentResolver?.notifyChange(uri, null)
|
||
|
return insertedUri
|
||
|
}
|
||
|
|
||
|
override fun onCreate(): Boolean {
|
||
|
dbHelper = TempDbHelper(context!!)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
override fun query(
|
||
|
uri: Uri, projection: Array<String>?, selection: String?,
|
||
|
selectionArgs: Array<String>?, sortOrder: String?
|
||
|
): Cursor? {
|
||
|
val db = dbHelper.readableDatabase
|
||
|
val cursor: Cursor
|
||
|
when (uriMatcher.match(uri)) {
|
||
|
TEMPERATURES -> {
|
||
|
cursor = db.query(
|
||
|
TempContract.TempEntry.TABLE_NAME,
|
||
|
projection,
|
||
|
selection,
|
||
|
selectionArgs,
|
||
|
null,
|
||
|
null,
|
||
|
sortOrder
|
||
|
)
|
||
|
}
|
||
|
TEMPERATURES_ID -> {
|
||
|
cursor = db.query(
|
||
|
TempContract.TempEntry.TABLE_NAME,
|
||
|
projection,
|
||
|
"_id = ?",
|
||
|
arrayOf(uri.lastPathSegment),
|
||
|
null,
|
||
|
null,
|
||
|
sortOrder
|
||
|
)
|
||
|
}
|
||
|
else -> throw IllegalArgumentException("Unknown URI ${uri}")
|
||
|
}
|
||
|
cursor.setNotificationUri(context!!.contentResolver, uri)
|
||
|
return cursor
|
||
|
}
|
||
|
|
||
|
override fun update(
|
||
|
uri: Uri, values: ContentValues?, selection: String?,
|
||
|
selectionArgs: Array<String>?
|
||
|
): Int {
|
||
|
TODO("Implement this to handle requests to update one or more rows.")
|
||
|
}
|
||
|
}
|
||
| learn/android/TempRecorderClassicKt/app/src/main/res/layout/activity_main.xml | ||
|---|---|---|
|
app:layout_constraintTop_toTopOf="parent" />
|
||
|
|
||
|
<TextView
|
||
|
android:id="@+id/timeTextView"
|
||
|
android:id="@+id/measuredAtTextView"
|
||
|
android:layout_width="0dp"
|
||
|
android:layout_height="wrap_content"
|
||
|
android:layout_marginStart="8dp"
|
||
|
android:layout_marginTop="16dp"
|
||
|
android:fontFamily="monospace"
|
||
|
android:text="09.13 11:33"
|
||
|
android:text="09.15 18:50"
|
||
|
android:textAlignment="center"
|
||
|
android:textSize="36sp"
|
||
|
app:layout_constraintEnd_toEndOf="parent"
|
||
|
app:layout_constraintHorizontal_bias="1.0"
|
||
|
app:layout_constraintStart_toStartOf="parent"
|
||
|
app:layout_constraintTop_toBottomOf="@+id/textView" />
|
||
|
|
||
| ... | ... | |
|
app:layout_constraintEnd_toStartOf="@+id/inc10MinButton"
|
||
|
app:layout_constraintHorizontal_bias="0.5"
|
||
|
app:layout_constraintStart_toStartOf="parent"
|
||
|
app:layout_constraintTop_toBottomOf="@+id/timeTextView" />
|
||
|
app:layout_constraintTop_toBottomOf="@+id/measuredAtTextView" />
|
||
|
|
||
|
<Button
|
||
|
android:id="@+id/inc10MinButton"
|
||
| ... | ... | |
|
android:layout_width="0dp"
|
||
|
android:layout_height="0dp"
|
||
|
android:layout_marginTop="16dp"
|
||
|
app:layoutManager="LinearLayoutManager"
|
||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||
|
app:layout_constraintEnd_toEndOf="parent"
|
||
|
app:layout_constraintStart_toStartOf="parent"
|
||
| learn/android/TempRecorderClassicKt/app/src/main/res/layout/list_item.xml | ||
|---|---|---|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||
|
android:layout_width="match_parent"
|
||
|
android:layout_height="wrap_content">
|
||
|
|
||
|
<TextView
|
||
|
android:id="@+id/dateTimeView"
|
||
|
android:layout_width="0dp"
|
||
|
android:layout_height="wrap_content"
|
||
|
android:layout_weight="1"
|
||
|
android:fontFamily="monospace"
|
||
|
android:text="TextView"
|
||
|
android:textAlignment="center"
|
||
|
android:textSize="18sp" />
|
||
|
|
||
|
<TextView
|
||
|
android:id="@+id/temperatureView"
|
||
|
android:layout_width="0dp"
|
||
|
android:layout_height="wrap_content"
|
||
|
android:layout_weight="1"
|
||
|
android:fontFamily="monospace"
|
||
|
android:text="TextView"
|
||
|
android:textAlignment="center"
|
||
|
android:textSize="18sp" />
|
||
|
</LinearLayout>
|
||
refs #166 Add Content Provider