Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@
android:name="com.ichi2.anki.AnkiDroidApp"
android:label="@string/app_name"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:icon="@mipmap/ankiquest_launcher"
android:roundIcon="@mipmap/ankiquest_launcher"
android:windowSoftInputMode="adjustResize"
android:usesCleartextTraffic="true"
android:largeHeap="true"
Expand Down
14 changes: 13 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,19 @@ open class DeckPicker :
AlertDialog
.Builder(this@DeckPicker)
.setTitle(R.string.ankiquest_summary_title)
.setMessage(
.setView(
com.ichi2.anki.ankiquest.AnkiquestAki.image(
this@DeckPicker,
if (summary.remaining ==
0
) {
R.drawable.aki_celebrate
} else {
R.drawable.aki_streak
},
112,
),
).setMessage(
getString(
R.string.ankiquest_summary_body,
summary.reviews,
Expand Down
100 changes: 100 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestAki.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.ankiquest

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.ichi2.anki.R
import com.ichi2.anki.common.time.TimeManager
import org.json.JSONObject

/** Aki encourages real progress without changing study, scoring or notification rules. */
internal object AnkiquestAki {
enum class Mood(
@DrawableRes val image: Int,
@StringRes val message: Int,
) {
WELCOME(R.drawable.aki_welcome, R.string.aki_welcome),
REVIEW(R.drawable.aki_review, R.string.aki_review),
CELEBRATE(R.drawable.aki_celebrate, R.string.aki_complete),
STREAK(R.drawable.aki_streak, R.string.aki_streak),
FREEZE(R.drawable.aki_freeze, R.string.aki_protected),
}

fun mood(
local: HomeLocal?,
profile: JSONObject?,
now: Long = TimeManager.time.intTimeMS(),
): Mood {
val dayEndsAt = profile?.optLong("day_ends_at") ?: 0L
val current = profile?.takeUnless { dayEndsAt > 0L && dayEndsAt <= now }
// The collection is the source of truth for cards still due locally.
if (current?.optString("streak_state") == "protected" && current.optJSONObject("today")?.optLong("reviews", 0) == 0L) {
return Mood.FREEZE
}
if (local?.decks?.isNotEmpty() == true && local.decks.all { it.due == 0 } &&
(current?.optJSONObject("today")?.optLong("reviews", 0) ?: 0) > 0
) {
return Mood.CELEBRATE
}
if ((local?.focus?.due ?: 0) > 0) return Mood.REVIEW
if (current?.optString("streak_state") == "studied" &&
(current.optJSONObject("today")?.optLong("reviews", 0) ?: 0) > 0
) {
return Mood.STREAK
}
return Mood.WELCOME
}

fun image(
context: Context,
@DrawableRes resource: Int,
size: Int = 80,
): ImageView =
ImageView(context).apply {
setImageResource(resource)
scaleType = ImageView.ScaleType.FIT_CENTER
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
layoutParams = LinearLayout.LayoutParams(dp(context, size), dp(context, size))
}

fun encouragement(
context: Context,
mood: Mood,
): LinearLayout =
LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(0, dp(context, 8), 0, dp(context, 8))
addView(image(context, mood.image))
addView(
TextView(context).apply {
text = context.getString(mood.message)
textSize = 16f
setTextColor(context.getColor(R.color.aq_home_text))
setPadding(dp(context, 12), 0, 0, 0)
},
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f),
)
}

fun notificationIcon(context: Context): Bitmap? =
BitmapFactory.decodeResource(
context.resources,
R.drawable.aki_face,
BitmapFactory.Options().apply { inSampleSize = 8 },
)

private fun dp(
context: Context,
value: Int,
) = (value * context.resources.displayMetrics.density).toInt()
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class AnkiquestHomeActivity : AnkiActivity(R.layout.activity_ankiquest_home) {

private fun renderToday() {
title(R.string.ankiquest_nav_today, R.string.aq_home_today_subtitle)
content.addView(AnkiquestAki.encouragement(this, AnkiquestAki.mood(local, profile)))
profile?.let {
text(content, getString(R.string.aq_home_streak_level, it.optInt("streak"), it.optInt("level")), small = true)
}
Expand Down Expand Up @@ -191,11 +192,13 @@ class AnkiquestHomeActivity : AnkiActivity(R.layout.activity_ankiquest_home) {
if (account == null) connectionCard()
AnkiquestStudySession.latest()?.let { summary ->
val panel = card()
panel.addView(AnkiquestAki.image(this, if (summary.remaining == 0) R.drawable.aki_celebrate else R.drawable.aki_streak, 56))
text(panel, getString(R.string.aq_home_last_session), heading = true)
text(panel, getString(R.string.aq_home_session_summary, summary.reviews, summary.duration(this), summary.remaining))
}
profile?.optJSONArray("quests")?.objects()?.let { quests ->
if (quests.isNotEmpty()) {
if (quests.all { it.optBoolean("done") }) content.addView(AnkiquestAki.image(this, R.drawable.aki_winner, 56))
text(content, getString(R.string.aq_home_quests), heading = true)
quests.forEach { quest ->
val panel = card()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ object AnkiquestNotifier {
val notification =
NotificationCompat
.Builder(context, channel)
.setSmallIcon(R.drawable.ic_star_notify)
.setSmallIcon(R.drawable.ic_aki_notify)
.setLargeIcon(AnkiquestAki.notificationIcon(context))
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.R
import com.ichi2.anki.ankiquest.AnkiquestNavigation
import java.text.NumberFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
Expand Down Expand Up @@ -90,6 +92,8 @@ class ReviewHeatmapView
if (ReviewHeatmap.searchForDay(currentData, date) != null) onDaySelected?.invoke(currentData, date)
}
setLoading()
findViewById<View>(R.id.review_heatmap_aki).isVisible =
AnkiDroidApp.sharedPrefsOrNull() != null && AnkiquestNavigation.enabled()
}

fun setData(data: ReviewHeatmapData) = setData(data, state)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions AnkiDroid/src/main/res/drawable/aki_launcher_foreground.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/aki_face" android:inset="23%" />
8 changes: 8 additions & 0 deletions AnkiDroid/src/main/res/drawable/ic_aki_notify.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M3,10 C1,6 2,2 5,3 L9,6 Q12,5 15,6 L19,3 C22,2 23,6 21,10 Q23,18 17,21 Q12,24 7,21 Q1,18 3,10 Z M6,10 Q3,12 6,16 Q8,17 10,14 Q8,10 6,10 Z M18,10 Q21,12 18,16 Q16,17 14,14 Q16,10 18,10 Z M10,17 Q12,20 14,17 Q12,15 10,17 Z" />
</vector>
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="8dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
android:src="@drawable/aki_face" />
<TextView
android:id="@+id/aq_home_brand"
android:layout_width="0dp"
Expand Down
8 changes: 8 additions & 0 deletions AnkiDroid/src/main/res/layout/view_review_heatmap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<ImageView
android:id="@+id/review_heatmap_aki"
android:layout_width="40dp"
android:layout_height="40dp"
android:importantForAccessibility="no"
android:background="@null"
android:visibility="gone"
android:src="@drawable/aki_streak" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/review_heatmap_collapse"
android:layout_width="48dp"
Expand Down
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/res/layout/widget_ankiquest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
android:paddingStart="4dp"
android:paddingBottom="4dp">

<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="6dp"
android:importantForAccessibility="no"
android:src="@drawable/aki_face" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
android:paddingStart="4dp"
android:paddingBottom="4dp">

<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="6dp"
android:importantForAccessibility="no"
android:src="@drawable/aki_face" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/aki_launcher_background" />
<foreground android:drawable="@drawable/aki_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_aki_notify" />
</adaptive-icon>

This file was deleted.

5 changes: 5 additions & 0 deletions AnkiDroid/src/main/res/mipmap-anydpi/ankiquest_launcher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item><shape><solid android:color="@color/aki_launcher_background" /><corners android:radius="12dp" /></shape></item>
<item android:drawable="@drawable/aki_face" android:top="5dp" android:bottom="5dp" android:left="5dp" android:right="5dp" />
</layer-list>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 9 additions & 0 deletions AnkiDroid/src/main/res/values-es/ankiquest_aki.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="aki_welcome">Aki está aquí para ayudarte. Una tarjeta, un pequeño paso.</string>
<string name="aki_review">Un pequeño paso empieza con una tarjeta.</string>
<string name="aki_complete">Ya terminaste tus tarjetas por ahora. ¡Buen trabajo!</string>
<string name="aki_streak">Hoy estuviste presente. Ese progreso vale la pena.</string>
<string name="aki_protected">Un protector mantiene tu racha. Te espera un nuevo comienzo.</string>
<string name="aki_settings_summary">Tu compañero de estudio para avanzar y compartir el progreso</string>
</resources>
2 changes: 2 additions & 0 deletions AnkiDroid/src/main/res/values/aki_colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources><color name="aki_launcher_background">#F7F1E8</color></resources>
9 changes: 9 additions & 0 deletions AnkiDroid/src/main/res/values/ankiquest_aki.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="aki_welcome">Aki is here to help. One card, one small step.</string>
<string name="aki_review">A small step starts with one card.</string>
<string name="aki_complete">Your cards are done for now. Nice work!</string>
<string name="aki_streak">You showed up today. That is progress worth keeping.</string>
<string name="aki_protected">Your streak is protected by a freeze. A fresh start is waiting.</string>
<string name="aki_settings_summary">Your study companion for small steps and shared progress</string>
</resources>
4 changes: 3 additions & 1 deletion AnkiDroid/src/main/res/xml/preferences_ankiquest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<Preference
android:key="@string/ankiquest_dashboard_key"
android:persistent="false"
android:title="@string/ankiquest_dashboard_title" />
android:title="@string/ankiquest_dashboard_title"
android:icon="@drawable/aki_face"
android:summary="@string/aki_settings_summary" />
<com.ichi2.preferences.VersatileTextPreference
android:key="@string/ankiquest_url_key"
android:title="@string/ankiquest_url_title"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.ankiquest

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.RobolectricTest
import org.json.JSONObject
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@Config(sdk = [33])
class AnkiquestAkiTest : RobolectricTest() {
private fun profile(
state: String,
reviews: Int,
) = JSONObject().put("streak_state", state).put("today", JSONObject().put("reviews", reviews))

@Test
fun `no history or empty decks never earns a celebration`() {
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(null, null))
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(HomeLocal(emptyList(), 0), null))
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(HomeLocal(listOf(HomeDeck(1, "Empty", 0, 0, 0)), 1), null))
}

@Test
fun `cards due locally keep the focus on studying after remote progress`() {
val local = HomeLocal(listOf(HomeDeck(1, "Spanish", 0, 0, 4)), 1)
assertEquals(AnkiquestAki.Mood.REVIEW, AnkiquestAki.mood(local, profile("studied", 20)))
}

@Test
fun `finishing selected deck does not claim all decks are done`() {
val local = HomeLocal(listOf(HomeDeck(1, "Spanish", 0, 0, 0), HomeDeck(2, "Geography", 0, 0, 2)), 1)
assertEquals(AnkiquestAki.Mood.REVIEW, AnkiquestAki.mood(local, profile("studied", 20)))
}

@Test
fun `celebration needs a real study day and no remaining local cards`() {
val local = HomeLocal(listOf(HomeDeck(1, "Spanish", 0, 0, 0)), 1)
assertEquals(AnkiquestAki.Mood.CELEBRATE, AnkiquestAki.mood(local, profile("studied", 20)))
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(local, profile("pending", 0)))
}

@Test
fun `only an actually protected streak gets the freeze pose`() {
val local = HomeLocal(listOf(HomeDeck(1, "Spanish", 0, 0, 4)), 1)
assertEquals(AnkiquestAki.Mood.FREEZE, AnkiquestAki.mood(local, profile("protected", 0)))
assertEquals(AnkiquestAki.Mood.REVIEW, AnkiquestAki.mood(local, profile("pending", 0).put("freezes", 3)))
}

@Test
fun `expired progress never promises today's protection or completion`() {
val local = HomeLocal(listOf(HomeDeck(1, "Spanish", 0, 0, 0)), 1)
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(local, profile("protected", 0).put("day_ends_at", 1000), now = 1000))
assertEquals(AnkiquestAki.Mood.WELCOME, AnkiquestAki.mood(local, profile("studied", 20).put("day_ends_at", 1000), now = 1001))
}

@Test
fun `notification art is downsampled and decorative art is not an extra control`() {
val bitmap = assertNotNull(AnkiquestAki.notificationIcon(targetContext))
assertTrue(bitmap.width <= 192 && bitmap.height <= 192)
assertEquals(
android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO,
AnkiquestAki.image(targetContext, com.ichi2.anki.R.drawable.aki_review).importantForAccessibility,
)
}
}
Loading
Loading