Skip to content

Commit ac4bfef

Browse files
committed
introduces logging capabilities and added support for syncing with openScale beta build.
1 parent efd0ec4 commit ac4bfef

File tree

7 files changed

+549
-129
lines changed

7 files changed

+549
-129
lines changed

src/app/src/main/AndroidManifest.xml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
66
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
77
<uses-permission android:name="com.health.openscale.READ_WRITE_DATA" />
8+
<uses-permission android:name="com.health.openscale.oss.READ_WRITE_DATA" />
9+
<uses-permission android:name="com.health.openscale.beta.READ_WRITE_DATA" />
810
<uses-permission android:name="com.health.openscale.light.READ_WRITE_DATA" />
911
<uses-permission android:name="com.health.openscale.pro.READ_WRITE_DATA" />
10-
<uses-permission android:name="com.health.openscale.oss.READ_WRITE_DATA" />
1112
<uses-permission android:name="android.permission.health.WRITE_BODY_FAT" />
1213
<uses-permission android:name="android.permission.health.WRITE_BODY_WATER_MASS" />
1314
<uses-permission android:name="android.permission.health.WRITE_WEIGHT" />
1415

1516
<queries>
1617
<package android:name="com.health.openscale" />
18+
<package android:name="com.health.openscale.oss" />
19+
<package android:name="com.health.openscale.beta" />
1720
<package android:name="com.health.openscale.light" />
1821
<package android:name="com.health.openscale.pro" />
19-
<package android:name="com.health.openscale.oss" />
2022
<package android:name="com.google.android.apps.healthdata" />
2123
</queries>
2224

@@ -67,6 +69,17 @@
6769
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
6870
</intent-filter>
6971
</activity-alias>
72+
73+
<!-- Log file provider -->
74+
<provider
75+
android:name="androidx.core.content.FileProvider"
76+
android:authorities="${applicationId}.fileprovider"
77+
android:exported="false"
78+
android:grantUriPermissions="true">
79+
<meta-data
80+
android:name="android.support.FILE_PROVIDER_PATHS"
81+
android:resource="@xml/file_paths" />
82+
</provider>
7083
</application>
7184

7285
</manifest>

src/app/src/main/java/com/health/openscale/sync/core/service/SyncService.kt

Lines changed: 198 additions & 127 deletions
Large diffs are not rendered by default.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package com.health.openscale.sync.core.utils
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import android.os.Build
6+
import android.util.Log
7+
import timber.log.Timber
8+
import java.io.File
9+
import java.io.FileOutputStream
10+
import java.io.FileWriter
11+
import java.text.SimpleDateFormat
12+
import java.util.Date
13+
import java.util.Locale
14+
15+
class FileLoggingTree(
16+
private val context: Context,
17+
private val maxSizeBytes: Long = 10L * 1024L * 1024L // 10 MB
18+
) : Timber.Tree() {
19+
20+
companion object {
21+
const val BASE_NAME = "openscale_sync_log.txt"
22+
private val TS_HUMAN = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
23+
}
24+
25+
private val lock = Any()
26+
private val logDir: File by lazy { File(context.filesDir, "logs").apply { mkdirs() } }
27+
private val logFile: File by lazy {
28+
File(logDir, BASE_NAME).apply {
29+
if (!exists()) writeHeader(this)
30+
}
31+
}
32+
33+
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
34+
val ts = TS_HUMAN.format(Date())
35+
val lvl = when (priority) {
36+
Log.VERBOSE -> "VERBOSE"
37+
Log.DEBUG -> "DEBUG"
38+
Log.INFO -> "INFO"
39+
Log.WARN -> "WARN"
40+
Log.ERROR -> "ERROR"
41+
else -> "UNKNOWN"
42+
}
43+
44+
val sb = StringBuilder()
45+
.append(ts).append(' ')
46+
.append('[').append(lvl).append(']').append(' ')
47+
.append(tag ?: "openScale-sync").append(": ")
48+
.append(message).append('\n')
49+
50+
if (t != null) sb.append(Log.getStackTraceString(t)).append('\n')
51+
52+
val payload = sb.toString()
53+
val payloadBytes = payload.toByteArray(Charsets.UTF_8)
54+
55+
synchronized(lock) {
56+
rotateIfNeeded(payloadBytes.size)
57+
runCatching { FileWriter(logFile, true).use { it.write(payload) } }
58+
}
59+
}
60+
61+
fun file(): File = logFile
62+
63+
private fun rotateIfNeeded(incomingBytes: Int) {
64+
val currentBytes = if (logFile.exists()) logFile.length() else 0L
65+
if (currentBytes + incomingBytes <= maxSizeBytes) return
66+
67+
val now = TS_HUMAN.format(Date())
68+
val note = "NOTE: Previous log exceeded ${formatBytes(maxSizeBytes)} at $now; started new log.\n\n"
69+
70+
if (logFile.exists()) runCatching { logFile.delete() }
71+
writeHeader(logFile)
72+
runCatching { FileWriter(logFile, true).use { it.write(note) } }
73+
}
74+
75+
fun writeHeader(target: File) {
76+
val pm = context.packageManager
77+
val pkg = context.packageName
78+
val info = if (Build.VERSION.SDK_INT >= 33) {
79+
pm.getPackageInfo(pkg, android.content.pm.PackageManager.PackageInfoFlags.of(0))
80+
} else {
81+
@Suppress("DEPRECATION")
82+
pm.getPackageInfo(pkg, 0)
83+
}
84+
val appName = context.applicationInfo.loadLabel(pm).toString()
85+
val versionName = info.versionName ?: "?"
86+
val versionCode = info.longVersionCode
87+
val started = TS_HUMAN.format(Date())
88+
val device = "${Build.MANUFACTURER} ${Build.MODEL}"
89+
val androidVersion = "Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})"
90+
91+
val header = buildString {
92+
appendLine("============================================================")
93+
appendLine("openScale Sync Log")
94+
appendLine("Application : $appName")
95+
appendLine("Package : $pkg")
96+
appendLine("Version : $versionName ($versionCode)")
97+
appendLine("Device : $device")
98+
appendLine("OS : $androidVersion")
99+
appendLine("Log started : $started")
100+
appendLine("============================================================")
101+
appendLine()
102+
}
103+
FileWriter(target, true).use { it.write(header) }
104+
}
105+
106+
private fun formatBytes(bytes: Long): String {
107+
val mb = bytes / (1024.0 * 1024.0)
108+
return String.format(Locale.US, "%.1f MB", mb)
109+
}
110+
}
111+
112+
object LogManager {
113+
private const val PREF_KEY = "loggingEnabled"
114+
private var fileTree: FileLoggingTree? = null
115+
private const val MAX_SIZE_BYTES: Long = 10L * 1024L * 1024L // 10 MB
116+
private val lock = Any()
117+
118+
fun init(context: Context, prefs: SharedPreferences) {
119+
if (isEnabled(prefs)) enableInternal(context) else disableInternal()
120+
}
121+
122+
fun isEnabled(prefs: SharedPreferences): Boolean =
123+
prefs.getBoolean(PREF_KEY, false)
124+
125+
fun setEnabled(context: Context, prefs: SharedPreferences, enabled: Boolean) {
126+
synchronized(lock) {
127+
val wasEnabled = isEnabled(prefs)
128+
prefs.edit().putBoolean(PREF_KEY, enabled).apply()
129+
130+
if (enabled && !wasEnabled) {
131+
// Transition off -> on: fresh file
132+
disableInternal()
133+
freshLogFile(context)
134+
enableInternal(context)
135+
Timber.i("Logging enabled (fresh start)")
136+
} else if (!enabled && wasEnabled) {
137+
// Transition on -> off
138+
disableInternal()
139+
Timber.i("Logging disabled")
140+
} else {
141+
}
142+
}
143+
}
144+
145+
private fun enableInternal(context: Context) {
146+
if (fileTree == null) {
147+
fileTree = FileLoggingTree(context.applicationContext, MAX_SIZE_BYTES)
148+
Timber.plant(fileTree!!)
149+
}
150+
}
151+
152+
private fun disableInternal() {
153+
fileTree?.let {
154+
Timber.uproot(it)
155+
fileTree = null
156+
}
157+
}
158+
159+
fun logFile(context: Context): File =
160+
(fileTree ?: FileLoggingTree(context, MAX_SIZE_BYTES)).file()
161+
162+
fun hasLogFile(context: Context): Boolean {
163+
val f = logFile(context)
164+
return f.exists() && f.length() > 0
165+
}
166+
167+
private fun freshLogFile(context: Context) {
168+
val dir = File(context.filesDir, "logs").apply { mkdirs() }
169+
val f = File(dir, FileLoggingTree.BASE_NAME)
170+
if (f.exists()) runCatching { f.delete() }
171+
FileLoggingTree(context).writeHeader(target = f)
172+
}
173+
174+
fun clearLog(context: Context) {
175+
synchronized(lock) {
176+
val dir = File(context.filesDir, "logs").apply { mkdirs() }
177+
val f = File(dir, FileLoggingTree.BASE_NAME)
178+
if (f.exists()) runCatching { f.delete() }
179+
FileLoggingTree(context).writeHeader(target = f)
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)