Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f9edd5e63 | |||
| 838b10f2fd | |||
| f7444a06eb |
@@ -1,3 +1,7 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="icon.png" width="108" alt="Claude Usage Widget">
|
||||||
|
</p>
|
||||||
|
|
||||||
# Claude Usage Widget
|
# Claude Usage Widget
|
||||||
|
|
||||||
Android home screen widget that shows your Claude Pro usage at a glance.
|
Android home screen widget that shows your Claude Pro usage at a glance.
|
||||||
@@ -6,6 +10,11 @@ Android home screen widget that shows your Claude Pro usage at a glance.
|
|||||||
|
|
||||||
- **SESSION** bar — current 5-hour window utilization with reset time
|
- **SESSION** bar — current 5-hour window utilization with reset time
|
||||||
- **WEEKLY** bar — 7-day rolling usage with reset time
|
- **WEEKLY** bar — 7-day rolling usage with reset time
|
||||||
|
- **Pace marker** — a colored tick on each bar shows where you *should be* right now to finish at
|
||||||
|
exactly 100% by reset. Tick color grades your projection: green (way under budget) → teal →
|
||||||
|
yellow → orange → red → purple (burning way too fast), with an "X% over/under pace" label.
|
||||||
|
- **Peak-hours indicator** — a Claude burst icon that lights up 🔥 during Anthropic's peak window
|
||||||
|
(5–11 AM Pacific, Mon–Fri), when tokens burn faster, with a countdown to the window close.
|
||||||
- Tap the widget to open the app; tap ⟳ to force-refresh
|
- Tap the widget to open the app; tap ⟳ to force-refresh
|
||||||
- Responsive: works as 4×1 (compact) or 4×2 (full)
|
- Responsive: works as 4×1 (compact) or 4×2 (full)
|
||||||
- Auto-refreshes every 5 minutes in the background
|
- Auto-refreshes every 5 minutes in the background
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "me.khodak.claudeusage"
|
applicationId = "me.khodak.claudeusage"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 11
|
versionCode = 12
|
||||||
versionName = "1.10"
|
versionName = "1.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package me.khodak.claudeusage
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a usage bar as a Bitmap: rounded track + rounded fill + an optional vertical pace tick.
|
||||||
|
* Used both in the home-screen widget (setImageViewBitmap) and the in-app card, so the two render
|
||||||
|
* identically. Bitmaps are rendered at a fixed width and stretched horizontally via ImageView
|
||||||
|
* scaleType=fitXY; height is fixed so there is no vertical distortion.
|
||||||
|
*/
|
||||||
|
object BarRenderer {
|
||||||
|
|
||||||
|
private const val TRACK_COLOR = 0xFF252525.toInt()
|
||||||
|
|
||||||
|
fun render(
|
||||||
|
usedPct: Int,
|
||||||
|
markerPct: Int?,
|
||||||
|
fillColor: Int,
|
||||||
|
tierColor: Int?,
|
||||||
|
wPx: Int = 500,
|
||||||
|
hPx: Int = 14,
|
||||||
|
cornerPx: Float = 7f
|
||||||
|
): Bitmap {
|
||||||
|
val bmp = Bitmap.createBitmap(wPx, hPx, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bmp)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
// Track
|
||||||
|
paint.color = TRACK_COLOR
|
||||||
|
val full = RectF(0f, 0f, wPx.toFloat(), hPx.toFloat())
|
||||||
|
canvas.drawRoundRect(full, cornerPx, cornerPx, paint)
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
val pct = usedPct.coerceIn(0, 100)
|
||||||
|
if (pct > 0) {
|
||||||
|
paint.color = fillColor
|
||||||
|
val fillW = (wPx * pct / 100f).coerceAtLeast(cornerPx * 2)
|
||||||
|
val fill = RectF(0f, 0f, fillW, hPx.toFloat())
|
||||||
|
canvas.drawRoundRect(fill, cornerPx, cornerPx, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pace tick — "where you should be right now"
|
||||||
|
if (markerPct != null && tierColor != null) {
|
||||||
|
val m = markerPct.coerceIn(0, 100)
|
||||||
|
val tickW = (wPx * 0.012f).coerceIn(3f, 7f)
|
||||||
|
var x = wPx * m / 100f
|
||||||
|
x = x.coerceIn(tickW / 2f, wPx - tickW / 2f)
|
||||||
|
paint.color = tierColor
|
||||||
|
canvas.drawRect(x - tickW / 2f, 0f, x + tickW / 2f, hPx.toFloat(), paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,14 +87,15 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
|
|
||||||
private fun buildSmallViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
private fun buildSmallViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
||||||
val v = RemoteViews(context.packageName, R.layout.widget_layout_small)
|
val v = RemoteViews(context.packageName, R.layout.widget_layout_small)
|
||||||
|
applyPeak(v, showText = false)
|
||||||
if (!prefs.isLoggedIn()) {
|
if (!prefs.isLoggedIn()) {
|
||||||
v.setTextViewText(R.id.tv_session_value, "—")
|
v.setTextViewText(R.id.tv_session_value, "—")
|
||||||
v.setTextViewText(R.id.tv_session_label, "Not signed in")
|
v.setTextViewText(R.id.tv_session_label, "Not signed in")
|
||||||
v.setTextViewText(R.id.tv_weekly_value, "—")
|
v.setTextViewText(R.id.tv_weekly_value, "—")
|
||||||
v.setTextViewText(R.id.tv_weekly_label, "")
|
v.setTextViewText(R.id.tv_weekly_label, "")
|
||||||
v.setTextViewText(R.id.tv_status, "")
|
v.setTextViewText(R.id.tv_status, "")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
val hasUtilization = apiData != null && apiData.fiveHourUtilization >= 0f
|
val hasUtilization = apiData != null && apiData.fiveHourUtilization >= 0f
|
||||||
@@ -103,15 +104,17 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
when {
|
when {
|
||||||
hasUtilization -> {
|
hasUtilization -> {
|
||||||
val pct = apiData!!.fiveHourUtilization.toInt()
|
val pct = apiData!!.fiveHourUtilization.toInt()
|
||||||
|
val pace = PaceCalc.compute(apiData.fiveHourUtilization, apiData.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS)
|
||||||
v.setTextViewText(R.id.tv_session_value, "$pct%")
|
v.setTextViewText(R.id.tv_session_value, "$pct%")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, pct, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, pace?.markerPct, SESSION_FILL, pace?.tierColor))
|
||||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
|
v.setTextViewText(R.id.tv_session_label,
|
||||||
|
if (pace != null) PaceCalc.shortTag(apiData.fiveHourUtilization, pace) else formatReset(apiData.utilizationResetAtEpoch))
|
||||||
}
|
}
|
||||||
hasApiMessages -> {
|
hasApiMessages -> {
|
||||||
val rem = apiData!!.effectiveRemaining
|
val rem = apiData!!.effectiveRemaining
|
||||||
val lim = apiData.messagesLimit
|
val lim = apiData.messagesLimit
|
||||||
v.setTextViewText(R.id.tv_session_value, "$rem/$lim")
|
v.setTextViewText(R.id.tv_session_value, "$rem/$lim")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, apiData.progressPercent, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(apiData.progressPercent, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
||||||
}
|
}
|
||||||
sessionStart > 0 -> {
|
sessionStart > 0 -> {
|
||||||
@@ -119,25 +122,27 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
val h = TimeUnit.MILLISECONDS.toHours(elapsedMs)
|
val h = TimeUnit.MILLISECONDS.toHours(elapsedMs)
|
||||||
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
||||||
v.setTextViewText(R.id.tv_session_value, if (h > 0) "${h}h${m}m" else "${m}m")
|
v.setTextViewText(R.id.tv_session_value, if (h > 0) "${h}h${m}m" else "${m}m")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, "active")
|
v.setTextViewText(R.id.tv_session_label, "active")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
v.setTextViewText(R.id.tv_session_value, "—")
|
v.setTextViewText(R.id.tv_session_value, "—")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, "")
|
v.setTextViewText(R.id.tv_session_label, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
||||||
if (hasWeekly) {
|
if (hasWeekly) {
|
||||||
val wPct = apiData!!.weeklyUtilization.toInt()
|
val wPct = apiData!!.weeklyUtilization.toInt()
|
||||||
|
val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
|
||||||
v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
|
v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
|
||||||
v.setProgressBar(R.id.progress_bar_weekly, 100, wPct, false)
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor))
|
||||||
v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch))
|
v.setTextViewText(R.id.tv_weekly_label,
|
||||||
|
if (pace != null) PaceCalc.shortTag(apiData.weeklyUtilization, pace) else formatReset(apiData.weeklyResetAtEpoch))
|
||||||
} else {
|
} else {
|
||||||
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
||||||
v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d")
|
v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d")
|
||||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_weekly_label, "")
|
v.setTextViewText(R.id.tv_weekly_label, "")
|
||||||
}
|
}
|
||||||
val resetEpoch = apiData?.effectiveResetEpoch ?: -1L
|
val resetEpoch = apiData?.effectiveResetEpoch ?: -1L
|
||||||
@@ -157,6 +162,7 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
|
|
||||||
private fun buildViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
private fun buildViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
||||||
val v = RemoteViews(context.packageName, R.layout.widget_layout)
|
val v = RemoteViews(context.packageName, R.layout.widget_layout)
|
||||||
|
applyPeak(v, showText = true)
|
||||||
|
|
||||||
// ── Not logged in ────────────────────────────────────────────────
|
// ── Not logged in ────────────────────────────────────────────────
|
||||||
if (!prefs.isLoggedIn()) {
|
if (!prefs.isLoggedIn()) {
|
||||||
@@ -165,7 +171,8 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
v.setTextViewText(R.id.tv_weekly_value, "—")
|
v.setTextViewText(R.id.tv_weekly_value, "—")
|
||||||
v.setTextViewText(R.id.tv_weekly_label, "Open app to sign in")
|
v.setTextViewText(R.id.tv_weekly_label, "Open app to sign in")
|
||||||
v.setTextViewText(R.id.tv_status, "")
|
v.setTextViewText(R.id.tv_status, "")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,15 +184,16 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
when {
|
when {
|
||||||
hasUtilization -> {
|
hasUtilization -> {
|
||||||
val pct = apiData!!.fiveHourUtilization.toInt()
|
val pct = apiData!!.fiveHourUtilization.toInt()
|
||||||
|
val pace = PaceCalc.compute(apiData.fiveHourUtilization, apiData.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS)
|
||||||
v.setTextViewText(R.id.tv_session_value, "$pct%")
|
v.setTextViewText(R.id.tv_session_value, "$pct%")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, pct, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, pace?.markerPct, SESSION_FILL, pace?.tierColor))
|
||||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
|
v.setTextViewText(R.id.tv_session_label, resetWithPace(apiData.utilizationResetAtEpoch, apiData.fiveHourUtilization, pace))
|
||||||
}
|
}
|
||||||
hasApiMessages -> {
|
hasApiMessages -> {
|
||||||
val rem = apiData!!.effectiveRemaining
|
val rem = apiData!!.effectiveRemaining
|
||||||
val lim = apiData.messagesLimit
|
val lim = apiData.messagesLimit
|
||||||
v.setTextViewText(R.id.tv_session_value, "$rem / $lim")
|
v.setTextViewText(R.id.tv_session_value, "$rem / $lim")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, apiData.progressPercent, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(apiData.progressPercent, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
||||||
}
|
}
|
||||||
sessionStart > 0 -> {
|
sessionStart > 0 -> {
|
||||||
@@ -194,12 +202,12 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
||||||
val display = if (h > 0) "${h}h ${m}m" else if (m > 0) "${m}m" else "Just started"
|
val display = if (h > 0) "${h}h ${m}m" else if (m > 0) "${m}m" else "Just started"
|
||||||
v.setTextViewText(R.id.tv_session_value, display)
|
v.setTextViewText(R.id.tv_session_value, display)
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, "session active")
|
v.setTextViewText(R.id.tv_session_label, "session active")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
v.setTextViewText(R.id.tv_session_value, "—")
|
v.setTextViewText(R.id.tv_session_value, "—")
|
||||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_session_label, "")
|
v.setTextViewText(R.id.tv_session_label, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,13 +216,14 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
||||||
if (hasWeekly) {
|
if (hasWeekly) {
|
||||||
val wPct = apiData!!.weeklyUtilization.toInt()
|
val wPct = apiData!!.weeklyUtilization.toInt()
|
||||||
|
val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
|
||||||
v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
|
v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
|
||||||
v.setProgressBar(R.id.progress_bar_weekly, 100, wPct, false)
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor))
|
||||||
v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch))
|
v.setTextViewText(R.id.tv_weekly_label, resetWithPace(apiData.weeklyResetAtEpoch, apiData.weeklyUtilization, pace))
|
||||||
} else {
|
} else {
|
||||||
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
||||||
v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d")
|
v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d")
|
||||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||||
v.setTextViewText(R.id.tv_weekly_label, "active this week")
|
v.setTextViewText(R.id.tv_weekly_label, "active this week")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +247,27 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val SESSION_FILL = 0xFFCC785C.toInt()
|
||||||
|
private const val WEEKLY_FILL = 0xFF7B8FCC.toInt()
|
||||||
|
|
||||||
|
/** Tints the header burst icon and (optionally) the PEAK text by current peak state. */
|
||||||
|
private fun applyPeak(v: RemoteViews, showText: Boolean) {
|
||||||
|
val peak = PeakHours.isPeak()
|
||||||
|
v.setInt(R.id.img_peak, "setColorFilter",
|
||||||
|
if (peak) 0xFFCC785C.toInt() else 0xFF666666.toInt())
|
||||||
|
if (showText) {
|
||||||
|
v.setTextViewText(R.id.tv_peak, if (peak) "🔥 PEAK" else "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Resets at 3:00 PM · 8% under pace" — reset line with the pace tag appended. */
|
||||||
|
private fun resetWithPace(resetEpoch: Long, usedPct: Float, pace: PaceCalc.Pace?): CharSequence {
|
||||||
|
val reset = formatReset(resetEpoch)
|
||||||
|
if (pace == null) return reset
|
||||||
|
val tag = PaceCalc.shortTag(usedPct, pace)
|
||||||
|
return if (reset.isBlank()) tag else "$reset · $tag"
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatReset(epochMs: Long): String {
|
private fun formatReset(epochMs: Long): String {
|
||||||
if (epochMs <= 0) return ""
|
if (epochMs <= 0) return ""
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|||||||
@@ -104,15 +104,38 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (!loggedIn || data == null) return
|
if (!loggedIn || data == null) return
|
||||||
|
|
||||||
binding.progressBar.progress = data.progressPercent
|
// ── Peak-hours row ───────────────────────────────────────────────────
|
||||||
|
val peak = PeakHours.state()
|
||||||
|
binding.imgPeak.setColorFilter(if (peak.active) PEAK_ON else PEAK_OFF)
|
||||||
|
binding.tvPeak.setTextColor(if (peak.active) PEAK_ON else 0xFFAAAAAA.toInt())
|
||||||
|
binding.tvPeak.text = if (peak.active)
|
||||||
|
"🔥 Peak hours — ${peak.endsInLabel} · ${peak.windowLabel}"
|
||||||
|
else
|
||||||
|
"Off-peak · ${peak.windowLabel}"
|
||||||
|
|
||||||
|
// ── Session (5-hour) bar + pace ──────────────────────────────────────
|
||||||
|
val sessionPct = data.progressPercent
|
||||||
|
val sessionPace = if (data.fiveHourUtilization >= 0f)
|
||||||
|
PaceCalc.compute(data.fiveHourUtilization, data.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS)
|
||||||
|
else null
|
||||||
|
binding.barSession.setImageBitmap(
|
||||||
|
BarRenderer.render(sessionPct, sessionPace?.markerPct, SESSION_FILL, sessionPace?.tierColor)
|
||||||
|
)
|
||||||
|
binding.tvSessionPace.text = paceSentence(data.fiveHourUtilization, sessionPace)
|
||||||
|
|
||||||
|
// ── Weekly (7-day) bar + pace ────────────────────────────────────────
|
||||||
if (data.weeklyUtilization >= 0f) {
|
if (data.weeklyUtilization >= 0f) {
|
||||||
val wPct = data.weeklyUtilization.toInt()
|
val wPct = data.weeklyUtilization.toInt()
|
||||||
binding.progressBarWeekly.progress = wPct
|
val weeklyPace = PaceCalc.compute(data.weeklyUtilization, data.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
|
||||||
|
binding.barWeekly.setImageBitmap(
|
||||||
|
BarRenderer.render(wPct, weeklyPace?.markerPct, WEEKLY_FILL, weeklyPace?.tierColor)
|
||||||
|
)
|
||||||
binding.tvWeeklyUsage.text = "$wPct% this week"
|
binding.tvWeeklyUsage.text = "$wPct% this week"
|
||||||
|
binding.tvWeeklyPace.text = paceSentence(data.weeklyUtilization, weeklyPace)
|
||||||
} else {
|
} else {
|
||||||
binding.progressBarWeekly.progress = 0
|
binding.barWeekly.setImageBitmap(BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||||
binding.tvWeeklyUsage.text = "—"
|
binding.tvWeeklyUsage.text = "—"
|
||||||
|
binding.tvWeeklyPace.text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvUsage.text = when {
|
binding.tvUsage.text = when {
|
||||||
@@ -139,6 +162,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.tvError.visibility = if (data.errorMessage.isNotBlank()) View.VISIBLE else View.GONE
|
binding.tvError.visibility = if (data.errorMessage.isNotBlank()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** "20% over pace · will likely hit limit" — empty when no projection is available yet. */
|
||||||
|
private fun paceSentence(usedPct: Float, pace: PaceCalc.Pace?): String {
|
||||||
|
if (pace == null) return ""
|
||||||
|
return "${PaceCalc.shortTag(usedPct, pace)} · ${pace.label}"
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatReset(epochMs: Long): String {
|
private fun formatReset(epochMs: Long): String {
|
||||||
if (epochMs <= 0) return ""
|
if (epochMs <= 0) return ""
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
@@ -151,4 +180,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
else -> "Resets ${SimpleDateFormat("EEE", Locale.US).format(Date(epochMs))} $timeStr"
|
else -> "Resets ${SimpleDateFormat("EEE", Locale.US).format(Date(epochMs))} $timeStr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SESSION_FILL = 0xFFCC785C.toInt()
|
||||||
|
private const val WEEKLY_FILL = 0xFF7B8FCC.toInt()
|
||||||
|
private const val PEAK_ON = 0xFFCC785C.toInt()
|
||||||
|
private const val PEAK_OFF = 0xFF666666.toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package me.khodak.claudeusage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pace projection mirroring hamed-elfayome/Claude-Usage-Tracker's PaceStatus.
|
||||||
|
*
|
||||||
|
* Idea: if you keep your current burn rate, where do you land at reset?
|
||||||
|
* projected = (usedPct / 100) / elapsedFraction
|
||||||
|
* The "where you should be right now" marker sits at elapsedFraction*100 on the bar —
|
||||||
|
* i.e. the position that finishes at exactly 100% by reset.
|
||||||
|
*/
|
||||||
|
object PaceCalc {
|
||||||
|
|
||||||
|
const val SESSION_WINDOW_MS = 5L * 60 * 60 * 1000 // 5 hours
|
||||||
|
const val WEEKLY_WINDOW_MS = 7L * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
|
||||||
|
// System-style tier colors (ARGB).
|
||||||
|
private const val GREEN = 0xFF34C759.toInt()
|
||||||
|
private const val TEAL = 0xFF30B0C7.toInt()
|
||||||
|
private const val YELLOW = 0xFFFFCC00.toInt()
|
||||||
|
private const val ORANGE = 0xFFFF9500.toInt()
|
||||||
|
private const val RED = 0xFFFF3B30.toInt()
|
||||||
|
private const val PURPLE = 0xFFAF52DE.toInt()
|
||||||
|
|
||||||
|
data class Pace(
|
||||||
|
val markerPct: Int, // where you "should be" now (0..100)
|
||||||
|
val projected: Float, // projected end-of-period fraction (1.0 == exactly 100%)
|
||||||
|
val tierColor: Int, // ARGB color for the tier
|
||||||
|
val label: String // short interpretation, e.g. "sustainable pace"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param usedPct current utilization 0..100
|
||||||
|
* @param resetEpoch epoch millis when this window resets (period end)
|
||||||
|
* @param windowMs total length of the window
|
||||||
|
* @return Pace, or null if we can't meaningfully project yet
|
||||||
|
*/
|
||||||
|
fun compute(
|
||||||
|
usedPct: Float,
|
||||||
|
resetEpoch: Long,
|
||||||
|
windowMs: Long,
|
||||||
|
now: Long = System.currentTimeMillis()
|
||||||
|
): Pace? {
|
||||||
|
if (resetEpoch <= 0L || usedPct < 0f) return null
|
||||||
|
val start = resetEpoch - windowMs
|
||||||
|
val elapsed = now - start
|
||||||
|
val elapsedFraction = elapsed.toFloat() / windowMs.toFloat()
|
||||||
|
// Match reference: need >=3% elapsed and period not complete.
|
||||||
|
if (elapsedFraction < 0.03f || elapsedFraction >= 1.0f) return null
|
||||||
|
|
||||||
|
val markerPct = (elapsedFraction * 100f).toInt().coerceIn(0, 100)
|
||||||
|
if (usedPct == 0f) {
|
||||||
|
return Pace(markerPct, 0f, GREEN, "way under budget")
|
||||||
|
}
|
||||||
|
val projected = (usedPct / 100f) / elapsedFraction
|
||||||
|
val (color, label) = when {
|
||||||
|
projected < 0.50f -> GREEN to "way under budget"
|
||||||
|
projected < 0.75f -> TEAL to "sustainable pace"
|
||||||
|
projected < 0.90f -> YELLOW to "starting to push it"
|
||||||
|
projected < 1.00f -> ORANGE to "will likely hit limit"
|
||||||
|
projected < 1.20f -> RED to "on track to exceed"
|
||||||
|
else -> PURPLE to "burning way too fast"
|
||||||
|
}
|
||||||
|
return Pace(markerPct, projected, color, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short tag for the widget reset line, e.g. "8% under pace" / "20% over pace" / "on pace". */
|
||||||
|
fun shortTag(usedPct: Float, pace: Pace): String {
|
||||||
|
val delta = usedPct.toInt() - pace.markerPct
|
||||||
|
return when {
|
||||||
|
delta >= 2 -> "$delta% over pace"
|
||||||
|
delta <= -2 -> "${-delta}% under pace"
|
||||||
|
else -> "on pace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package me.khodak.claudeusage
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anthropic peak usage window, mirroring hamed-elfayome/Claude-Usage-Tracker's PeakHoursService:
|
||||||
|
* 5:00–11:00 AM America/Los_Angeles, Monday–Friday. Tokens burn faster during this window.
|
||||||
|
*/
|
||||||
|
object PeakHours {
|
||||||
|
|
||||||
|
private const val PEAK_START_HOUR = 5 // 5 AM PT inclusive
|
||||||
|
private const val PEAK_END_HOUR = 11 // 11 AM PT exclusive
|
||||||
|
private val PT: TimeZone = TimeZone.getTimeZone("America/Los_Angeles")
|
||||||
|
|
||||||
|
data class PeakState(
|
||||||
|
val active: Boolean,
|
||||||
|
val endsInLabel: String, // e.g. "ends in 2h 14m" (only meaningful when active)
|
||||||
|
val windowLabel: String // e.g. "5–11 AM PT · 8 AM–2 PM your time"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun isPeak(now: Long = System.currentTimeMillis()): Boolean {
|
||||||
|
val cal = GregorianCalendar(PT).apply { timeInMillis = now }
|
||||||
|
val dow = cal.get(Calendar.DAY_OF_WEEK) // Sun=1 .. Sat=7
|
||||||
|
val weekday = dow in Calendar.MONDAY..Calendar.FRIDAY
|
||||||
|
val hour = cal.get(Calendar.HOUR_OF_DAY)
|
||||||
|
return weekday && hour in PEAK_START_HOUR until PEAK_END_HOUR
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Epoch millis of 11:00 AM PT on the current PT day (peak window close). */
|
||||||
|
fun peakEndEpoch(now: Long = System.currentTimeMillis()): Long {
|
||||||
|
val cal = GregorianCalendar(PT).apply {
|
||||||
|
timeInMillis = now
|
||||||
|
set(Calendar.HOUR_OF_DAY, PEAK_END_HOUR)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
return cal.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
fun state(now: Long = System.currentTimeMillis()): PeakState {
|
||||||
|
val active = isPeak(now)
|
||||||
|
val endsIn = if (active) {
|
||||||
|
val rem = peakEndEpoch(now) - now
|
||||||
|
val h = TimeUnit.MILLISECONDS.toHours(rem)
|
||||||
|
val m = TimeUnit.MILLISECONDS.toMinutes(rem) % 60
|
||||||
|
if (h > 0) "ends in ${h}h ${m}m" else "ends in ${m}m"
|
||||||
|
} else ""
|
||||||
|
return PeakState(active, endsIn, localWindowString(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "5–11 AM PT · 8 AM–2 PM your time" — peak window translated to the device timezone. */
|
||||||
|
fun localWindowString(now: Long = System.currentTimeMillis()): String {
|
||||||
|
val local = TimeZone.getDefault()
|
||||||
|
if (local.id == PT.id) return "5–11 AM PT"
|
||||||
|
val startLocal = ptHourToLocal(PEAK_START_HOUR, now, local)
|
||||||
|
val endLocal = ptHourToLocal(PEAK_END_HOUR, now, local)
|
||||||
|
val fmt = SimpleDateFormat("h a", Locale.US).apply { timeZone = local }
|
||||||
|
val s = fmt.format(Date(startLocal)).replace(" ", " ")
|
||||||
|
val e = fmt.format(Date(endLocal)).replace(" ", " ")
|
||||||
|
return "5–11 AM PT · $s–$e your time"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ptHourToLocal(ptHour: Int, now: Long, local: TimeZone): Long {
|
||||||
|
val cal = GregorianCalendar(PT).apply {
|
||||||
|
timeInMillis = now
|
||||||
|
set(Calendar.HOUR_OF_DAY, ptHour)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
return cal.timeInMillis // absolute instant; formatting in `local` renders local wall time
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Claude "sunburst" mark. Drawn in white; tinted at runtime via setColorFilter
|
||||||
|
(calm #666666, peak #CC785C). -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="1.7"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="
|
||||||
|
M12,3 L12,7
|
||||||
|
M12,17 L12,21
|
||||||
|
M3,12 L7,12
|
||||||
|
M17,12 L21,12
|
||||||
|
M5.6,5.6 L8.4,8.4
|
||||||
|
M15.6,15.6 L18.4,18.4
|
||||||
|
M18.4,5.6 L15.6,8.4
|
||||||
|
M8.4,15.6 L5.6,18.4
|
||||||
|
M12,4.5 L12,6.2
|
||||||
|
M12,17.8 L12,19.5
|
||||||
|
M4.5,12 L6.2,12
|
||||||
|
M17.8,12 L19.5,12" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,9.6 a2.4,2.4 0 1,0 0.01,0 z" />
|
||||||
|
</vector>
|
||||||
@@ -92,6 +92,33 @@
|
|||||||
android:background="@drawable/widget_background"
|
android:background="@drawable/widget_background"
|
||||||
android:padding="20dp">
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<!-- Peak-hours row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="14dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imgPeak"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:src="@drawable/ic_claude_burst"
|
||||||
|
android:contentDescription="Peak hours" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPeak"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -110,16 +137,24 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/barSession"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="8dp"
|
android:layout_height="9dp"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#CC785C"
|
android:contentDescription="Session usage bar" />
|
||||||
android:progressBackgroundTint="#3A3A3A" />
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSessionPace"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvReset"
|
android:id="@+id/tvReset"
|
||||||
@@ -148,16 +183,24 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progressBarWeekly"
|
android:id="@+id/barWeekly"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="8dp"
|
android:layout_height="9dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#7B8FCC"
|
android:contentDescription="Weekly usage bar" />
|
||||||
android:progressBackgroundTint="#3A3A3A" />
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvWeeklyPace"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvWeeklyReset"
|
android:id="@+id/tvWeeklyReset"
|
||||||
|
|||||||
@@ -23,6 +23,24 @@
|
|||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/img_peak"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:src="@drawable/ic_claude_burst"
|
||||||
|
android:contentDescription="Peak hours" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_peak"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#CC785C"
|
||||||
|
android:textSize="9sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_refresh"
|
android:id="@+id/btn_refresh"
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
@@ -69,16 +87,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/bar_session"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="5dp"
|
android:layout_height="6dp"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#CC785C"
|
android:contentDescription="Session usage bar" />
|
||||||
android:progressBackgroundTint="#252525" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_session_label"
|
android:id="@+id/tv_session_label"
|
||||||
@@ -118,16 +134,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progress_bar_weekly"
|
android:id="@+id/bar_weekly"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="5dp"
|
android:layout_height="6dp"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#7B8FCC"
|
android:contentDescription="Weekly usage bar" />
|
||||||
android:progressBackgroundTint="#252525" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_weekly_label"
|
android:id="@+id/tv_weekly_label"
|
||||||
|
|||||||
@@ -23,6 +23,14 @@
|
|||||||
android:textSize="11sp"
|
android:textSize="11sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/img_peak"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:src="@drawable/ic_claude_burst"
|
||||||
|
android:contentDescription="Peak hours" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_refresh"
|
android:id="@+id/btn_refresh"
|
||||||
android:layout_width="28dp"
|
android:layout_width="28dp"
|
||||||
@@ -71,16 +79,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/bar_session"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="4dp"
|
android:layout_height="5dp"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#CC785C"
|
android:contentDescription="Session usage bar" />
|
||||||
android:progressBackgroundTint="#252525" />
|
|
||||||
|
|
||||||
<!-- 7-DAY row -->
|
<!-- 7-DAY row -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -120,16 +126,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<ImageView
|
||||||
android:id="@+id/progress_bar_weekly"
|
android:id="@+id/bar_weekly"
|
||||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="4dp"
|
android:layout_height="5dp"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
android:max="100"
|
android:scaleType="fitXY"
|
||||||
android:progress="0"
|
android:adjustViewBounds="false"
|
||||||
android:progressTint="#7B8FCC"
|
android:contentDescription="Weekly usage bar" />
|
||||||
android:progressBackgroundTint="#252525" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_status"
|
android:id="@+id/tv_status"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user