diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml index 0b272a4f7098..69c17922f4f0 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -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" diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 3b935e3cdde6..1d87e3f3b7c1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -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, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestAki.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestAki.kt new file mode 100644 index 000000000000..f5f3696cb612 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestAki.kt @@ -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() +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestHomeActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestHomeActivity.kt index 066f1e5eafd2..f640ff9c4af4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestHomeActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestHomeActivity.kt @@ -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) } @@ -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() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt index 2cbef00cc236..a37db88fd9c6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestNotifier.kt @@ -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)) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/heatmap/ReviewHeatmapView.kt b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/heatmap/ReviewHeatmapView.kt index 60772ffb1ab7..d68fb78d8876 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/heatmap/ReviewHeatmapView.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/deckpicker/heatmap/ReviewHeatmapView.kt @@ -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 @@ -90,6 +92,8 @@ class ReviewHeatmapView if (ReviewHeatmap.searchForDay(currentData, date) != null) onDaySelected?.invoke(currentData, date) } setLoading() + findViewById(R.id.review_heatmap_aki).isVisible = + AnkiDroidApp.sharedPrefsOrNull() != null && AnkiquestNavigation.enabled() } fun setData(data: ReviewHeatmapData) = setData(data, state) diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_celebrate.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_celebrate.png new file mode 100644 index 000000000000..103331a9f94b Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_celebrate.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_face.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_face.png new file mode 100644 index 000000000000..5e34a386b65c Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_face.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_freeze.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_freeze.png new file mode 100644 index 000000000000..db0d55812c2e Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_freeze.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_review.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_review.png new file mode 100644 index 000000000000..00de3195cad0 Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_review.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_streak.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_streak.png new file mode 100644 index 000000000000..ad3615e6141c Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_streak.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_welcome.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_welcome.png new file mode 100644 index 000000000000..6387c752c585 Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_welcome.png differ diff --git a/AnkiDroid/src/main/res/drawable-nodpi/aki_winner.png b/AnkiDroid/src/main/res/drawable-nodpi/aki_winner.png new file mode 100644 index 000000000000..61c8afd27842 Binary files /dev/null and b/AnkiDroid/src/main/res/drawable-nodpi/aki_winner.png differ diff --git a/AnkiDroid/src/main/res/drawable/aki_launcher_foreground.xml b/AnkiDroid/src/main/res/drawable/aki_launcher_foreground.xml new file mode 100644 index 000000000000..fe7973bdf834 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/aki_launcher_foreground.xml @@ -0,0 +1,3 @@ + + diff --git a/AnkiDroid/src/main/res/drawable/ic_aki_notify.xml b/AnkiDroid/src/main/res/drawable/ic_aki_notify.xml new file mode 100644 index 000000000000..e9edc3c3d1dc --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_aki_notify.xml @@ -0,0 +1,8 @@ + + + + diff --git a/AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml b/AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml index 99816de69683..545e5a18d703 100644 --- a/AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml +++ b/AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml @@ -14,6 +14,12 @@ android:orientation="horizontal" android:paddingStart="16dp" android:paddingEnd="8dp"> + + + + + + + + + diff --git a/AnkiDroid/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AnkiDroid/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 04091bd1024c..000000000000 --- a/AnkiDroid/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/AnkiDroid/src/main/res/mipmap-anydpi/ankiquest_launcher.xml b/AnkiDroid/src/main/res/mipmap-anydpi/ankiquest_launcher.xml new file mode 100644 index 000000000000..f77921d6f619 --- /dev/null +++ b/AnkiDroid/src/main/res/mipmap-anydpi/ankiquest_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/AnkiDroid/src/main/res/mipmap-hdpi/ic_launcher_round.png b/AnkiDroid/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index e8ed32be88d7..000000000000 Binary files a/AnkiDroid/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/AnkiDroid/src/main/res/mipmap-mdpi/ic_launcher_round.png b/AnkiDroid/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index bad855391489..000000000000 Binary files a/AnkiDroid/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/AnkiDroid/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/AnkiDroid/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index e85d67e5388c..000000000000 Binary files a/AnkiDroid/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/AnkiDroid/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/AnkiDroid/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index d5f8c835c3ee..000000000000 Binary files a/AnkiDroid/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/AnkiDroid/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/AnkiDroid/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index fb6d0326a086..000000000000 Binary files a/AnkiDroid/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/AnkiDroid/src/main/res/values-es/ankiquest_aki.xml b/AnkiDroid/src/main/res/values-es/ankiquest_aki.xml new file mode 100644 index 000000000000..faa9d980697d --- /dev/null +++ b/AnkiDroid/src/main/res/values-es/ankiquest_aki.xml @@ -0,0 +1,9 @@ + + + Aki está aquí para ayudarte. Una tarjeta, un pequeño paso. + Un pequeño paso empieza con una tarjeta. + Ya terminaste tus tarjetas por ahora. ¡Buen trabajo! + Hoy estuviste presente. Ese progreso vale la pena. + Un protector mantiene tu racha. Te espera un nuevo comienzo. + Tu compañero de estudio para avanzar y compartir el progreso + diff --git a/AnkiDroid/src/main/res/values/aki_colors.xml b/AnkiDroid/src/main/res/values/aki_colors.xml new file mode 100644 index 000000000000..05190d63669c --- /dev/null +++ b/AnkiDroid/src/main/res/values/aki_colors.xml @@ -0,0 +1,2 @@ + +#F7F1E8 diff --git a/AnkiDroid/src/main/res/values/ankiquest_aki.xml b/AnkiDroid/src/main/res/values/ankiquest_aki.xml new file mode 100644 index 000000000000..7ca2e0a633fc --- /dev/null +++ b/AnkiDroid/src/main/res/values/ankiquest_aki.xml @@ -0,0 +1,9 @@ + + + Aki is here to help. One card, one small step. + A small step starts with one card. + Your cards are done for now. Nice work! + You showed up today. That is progress worth keeping. + Your streak is protected by a freeze. A fresh start is waiting. + Your study companion for small steps and shared progress + diff --git a/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml b/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml index be4f0f1bdfbb..98a89c54c40d 100644 --- a/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml +++ b/AnkiDroid/src/main/res/xml/preferences_ankiquest.xml @@ -6,7 +6,9 @@ + android:title="@string/ankiquest_dashboard_title" + android:icon="@drawable/aki_face" + android:summary="@string/aki_settings_summary" />