Aperture: Refactor QrTextClassifier

Change-Id: If922f3aa9227943ab5d2f2ae48fa4811f2955f6a
diff --git a/app/src/main/java/org/lineageos/aperture/ext/AddressBookParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/AddressBookParsedResult.kt
new file mode 100644
index 0000000..3a5397d
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/AddressBookParsedResult.kt
@@ -0,0 +1,130 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.provider.ContactsContract
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.AddressBookParsedResult
+import org.lineageos.aperture.R
+
+fun AddressBookParsedResult.createIntent() = Intent(
+    Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI
+).apply {
+    names.firstOrNull()?.let {
+        putExtra(ContactsContract.Intents.Insert.NAME, it)
+    }
+
+    pronunciation?.let {
+        putExtra(ContactsContract.Intents.Insert.PHONETIC_NAME, it)
+    }
+
+    phoneNumbers?.let { phoneNumbers ->
+        val phoneTypes = phoneTypes ?: arrayOf()
+
+        for ((i, keys) in listOf(
+            listOf(
+                ContactsContract.Intents.Insert.PHONE,
+                ContactsContract.Intents.Insert.PHONE_TYPE,
+            ),
+            listOf(
+                ContactsContract.Intents.Insert.SECONDARY_PHONE,
+                ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE,
+            ),
+            listOf(
+                ContactsContract.Intents.Insert.TERTIARY_PHONE,
+                ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE,
+            ),
+        ).withIndex()) {
+            phoneNumbers.getOrNull(i)?.let { phone ->
+                putExtra(keys.first(), phone)
+                phoneTypes.getOrNull(i)?.let {
+                    putExtra(keys.last(), it)
+                }
+            }
+        }
+    }
+
+    emails?.let { emails ->
+        val emailTypes = emailTypes ?: arrayOf()
+
+        for ((i, keys) in listOf(
+            listOf(
+                ContactsContract.Intents.Insert.EMAIL,
+                ContactsContract.Intents.Insert.EMAIL_TYPE,
+            ),
+            listOf(
+                ContactsContract.Intents.Insert.SECONDARY_EMAIL,
+                ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE,
+            ),
+            listOf(
+                ContactsContract.Intents.Insert.TERTIARY_EMAIL,
+                ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE,
+            ),
+        ).withIndex()) {
+            emails.getOrNull(i)?.let { phone ->
+                putExtra(keys.first(), phone)
+                emailTypes.getOrNull(i)?.let {
+                    putExtra(keys.last(), it)
+                }
+            }
+        }
+    }
+
+    instantMessenger?.let {
+        putExtra(ContactsContract.Intents.Insert.IM_HANDLE, it)
+    }
+
+    note?.let {
+        putExtra(ContactsContract.Intents.Insert.NOTES, it)
+    }
+
+    addresses?.let { emails ->
+        val addressTypes = addressTypes ?: arrayOf()
+
+        for ((i, keys) in listOf(
+            listOf(
+                ContactsContract.Intents.Insert.POSTAL,
+                ContactsContract.Intents.Insert.POSTAL_TYPE,
+            ),
+        ).withIndex()) {
+            emails.getOrNull(i)?.let { phone ->
+                putExtra(keys.first(), phone)
+                addressTypes.getOrNull(i)?.let {
+                    putExtra(keys.last(), it)
+                }
+            }
+        }
+    }
+
+    org?.let {
+        putExtra(ContactsContract.Intents.Insert.COMPANY, it)
+    }
+}
+
+fun AddressBookParsedResult.createTextClassification(
+    context: Context
+) = TextClassification.Builder()
+    .setText(title ?: names.firstOrNull() ?: "")
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_contact_phone,
+                    R.string.qr_address_title,
+                    R.string.qr_address_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/CalendarParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/CalendarParsedResult.kt
new file mode 100644
index 0000000..913d67c
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/CalendarParsedResult.kt
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.provider.CalendarContract
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import androidx.core.os.bundleOf
+import com.google.zxing.client.result.CalendarParsedResult
+import org.lineageos.aperture.R
+
+fun CalendarParsedResult.createIntent() = Intent(
+    Intent.ACTION_INSERT, CalendarContract.Events.CONTENT_URI
+).apply {
+    summary?.let {
+        putExtra(CalendarContract.Events.TITLE, it)
+    }
+    description?.let {
+        putExtra(CalendarContract.Events.DESCRIPTION, it)
+    }
+    location?.let {
+        putExtra(CalendarContract.Events.EVENT_LOCATION, it)
+    }
+    organizer?.let {
+        putExtra(CalendarContract.Events.ORGANIZER, it)
+    }
+    attendees?.let {
+        putExtra(Intent.EXTRA_EMAIL, it.joinToString(","))
+    }
+
+    putExtras(
+        bundleOf(
+            CalendarContract.EXTRA_EVENT_BEGIN_TIME to startTimestamp,
+            CalendarContract.EXTRA_EVENT_END_TIME to endTimestamp,
+            CalendarContract.EXTRA_EVENT_ALL_DAY to (isStartAllDay && isEndAllDay),
+        )
+    )
+}
+
+fun CalendarParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(summary)
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_calendar_add_on,
+                    R.string.qr_calendar_title,
+                    R.string.qr_calendar_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/Context.kt b/app/src/main/java/org/lineageos/aperture/ext/Context.kt
new file mode 100644
index 0000000..33751a3
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/Context.kt
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.content.Context
+import android.util.TypedValue
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+
+@ColorInt
+fun Context.getThemeColor(@AttrRes attribute: Int) = TypedValue().let {
+    theme.resolveAttribute(attribute, it, true)
+    it.data
+}
diff --git a/app/src/main/java/org/lineageos/aperture/ext/EmailAddressParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/EmailAddressParsedResult.kt
new file mode 100644
index 0000000..91a37e3
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/EmailAddressParsedResult.kt
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import androidx.core.os.bundleOf
+import com.google.zxing.client.result.EmailAddressParsedResult
+import org.lineageos.aperture.R
+
+fun EmailAddressParsedResult.createIntent() = Intent(
+    Intent.ACTION_SENDTO,
+    Uri.parse("mailto:${tos?.firstOrNull() ?: ""}")
+).apply {
+    putExtras(
+        bundleOf(
+            Intent.EXTRA_EMAIL to tos,
+            Intent.EXTRA_CC to cCs,
+            Intent.EXTRA_BCC to bcCs,
+            Intent.EXTRA_SUBJECT to subject,
+            Intent.EXTRA_TEXT to body,
+        )
+    )
+}
+
+fun EmailAddressParsedResult.createTextClassification(
+    context: Context
+) = TextClassification.Builder()
+    .setText(tos.joinToString())
+    .setEntityType(TextClassifier.TYPE_EMAIL, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_email,
+                    R.string.qr_email_title,
+                    R.string.qr_email_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/GeoParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/GeoParsedResult.kt
new file mode 100644
index 0000000..dabfd5e
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/GeoParsedResult.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.GeoParsedResult
+import org.lineageos.aperture.R
+
+fun GeoParsedResult.createIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(geoURI))
+
+fun GeoParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(displayResult)
+    .setEntityType(TextClassifier.TYPE_ADDRESS, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_location_on,
+                    R.string.qr_geo_title,
+                    R.string.qr_geo_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/ISBNParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/ISBNParsedResult.kt
new file mode 100644
index 0000000..aa4baf3
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/ISBNParsedResult.kt
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.ISBNParsedResult
+import org.lineageos.aperture.R
+
+fun ISBNParsedResult.createIntent() = Intent(
+    Intent.ACTION_VIEW, Uri.parse("https://isbnsearch.org/isbn/${isbn}")
+)
+
+fun ISBNParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(isbn)
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_book,
+                    R.string.qr_isbn_title,
+                    R.string.qr_isbn_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/ParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/ParsedResult.kt
new file mode 100644
index 0000000..56a6885
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/ParsedResult.kt
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.content.Context
+import com.google.zxing.client.result.AddressBookParsedResult
+import com.google.zxing.client.result.CalendarParsedResult
+import com.google.zxing.client.result.EmailAddressParsedResult
+import com.google.zxing.client.result.GeoParsedResult
+import com.google.zxing.client.result.ISBNParsedResult
+import com.google.zxing.client.result.ParsedResult
+import com.google.zxing.client.result.ProductParsedResult
+import com.google.zxing.client.result.SMSParsedResult
+import com.google.zxing.client.result.TelParsedResult
+import com.google.zxing.client.result.TextParsedResult
+import com.google.zxing.client.result.URIParsedResult
+import com.google.zxing.client.result.VINParsedResult
+import com.google.zxing.client.result.WifiParsedResult
+
+fun ParsedResult.createTextClassification(context: Context) = when (this) {
+    is AddressBookParsedResult -> createTextClassification(context)
+
+    is CalendarParsedResult -> createTextClassification(context)
+
+    is EmailAddressParsedResult -> createTextClassification(context)
+
+    is GeoParsedResult -> createTextClassification(context)
+
+    is ISBNParsedResult -> createTextClassification(context)
+
+    is ProductParsedResult -> createTextClassification(context)
+
+    is SMSParsedResult -> createTextClassification(context)
+
+    is TelParsedResult -> createTextClassification(context)
+
+    is TextParsedResult -> null // Try with the next methods
+
+    is URIParsedResult -> null // We handle this manually
+
+    is VINParsedResult -> createTextClassification(context)
+
+    is WifiParsedResult -> createTextClassification(context)
+
+    else -> null
+}
diff --git a/app/src/main/java/org/lineageos/aperture/ext/ProductParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/ProductParsedResult.kt
new file mode 100644
index 0000000..63003b3
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/ProductParsedResult.kt
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.ProductParsedResult
+import org.lineageos.aperture.R
+
+fun ProductParsedResult.createIntent() = Intent(
+    Intent.ACTION_VIEW, Uri.parse("https://www.barcodelookup.com/${productID}")
+)
+
+fun ProductParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(productID)
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_shopping_cart,
+                    R.string.qr_product_title,
+                    R.string.qr_product_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/RemoteAction.kt b/app/src/main/java/org/lineageos/aperture/ext/RemoteAction.kt
new file mode 100644
index 0000000..1a93366
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/RemoteAction.kt
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import androidx.annotation.AttrRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.graphics.drawable.toBitmap
+import org.lineageos.aperture.R
+import kotlin.reflect.KClass
+
+fun KClass<RemoteAction>.build(
+    context: Context,
+    @DrawableRes iconRes: Int,
+    @StringRes titleRes: Int,
+    @StringRes contentDescriptionRes: Int,
+    intent: Intent,
+    requestCode: Int = 0,
+    flags: Int = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+    @AttrRes iconTint: Int = com.google.android.material.R.attr.colorOnBackground,
+) = RemoteAction(
+    Icon.createWithBitmap(
+        AppCompatResources.getDrawable(context, iconRes)?.let {
+            DrawableCompat.wrap(it.mutate()).apply {
+                DrawableCompat.setTint(
+                    this,
+                    context.getThemeColor(iconTint)
+                )
+            }
+        }?.toBitmap()
+    ),
+    context.getString(titleRes),
+    context.getString(contentDescriptionRes),
+    PendingIntent.getActivity(context, requestCode, intent, flags)
+)
diff --git a/app/src/main/java/org/lineageos/aperture/ext/SMSParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/SMSParsedResult.kt
new file mode 100644
index 0000000..9839caf
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/SMSParsedResult.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.SMSParsedResult
+import org.lineageos.aperture.R
+
+fun SMSParsedResult.createIntent() = Intent(Intent.ACTION_SENDTO, Uri.parse(smsuri))
+
+fun SMSParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(numbers.first())
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_sms,
+                    R.string.qr_sms_title,
+                    R.string.qr_sms_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/TelParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/TelParsedResult.kt
new file mode 100644
index 0000000..524bdc8
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/TelParsedResult.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.TelParsedResult
+import org.lineageos.aperture.R
+
+fun TelParsedResult.createIntent() = Intent(Intent.ACTION_SENDTO, Uri.parse(telURI))
+
+fun TelParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(number)
+    .setEntityType(TextClassifier.TYPE_PHONE, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_phone,
+                    R.string.qr_tel_title,
+                    R.string.qr_tel_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/URIParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/URIParsedResult.kt
new file mode 100644
index 0000000..cd9f232
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/URIParsedResult.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.URIParsedResult
+import org.lineageos.aperture.R
+
+fun URIParsedResult.createIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
+
+fun URIParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(uri)
+    .setEntityType(TextClassifier.TYPE_URL, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_open_in_browser,
+                    R.string.qr_uri_title,
+                    R.string.qr_uri_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/VINParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/VINParsedResult.kt
new file mode 100644
index 0000000..7478ad3
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/VINParsedResult.kt
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import com.google.zxing.client.result.VINParsedResult
+import org.lineageos.aperture.R
+
+fun VINParsedResult.createIntent() = Intent(
+    Intent.ACTION_VIEW, Uri.parse("https://www.vindecoderz.com/EN/check-lookup/${vin}")
+)
+
+fun VINParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(vin)
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_directions_car,
+                    R.string.qr_vin_title,
+                    R.string.qr_vin_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/ext/WifiParsedResult.kt b/app/src/main/java/org/lineageos/aperture/ext/WifiParsedResult.kt
new file mode 100644
index 0000000..fef4c55
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ext/WifiParsedResult.kt
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ext
+
+import android.app.RemoteAction
+import android.content.Context
+import android.content.Intent
+import android.net.wifi.WifiNetworkSuggestion
+import android.os.Build
+import android.provider.Settings
+import android.view.textclassifier.TextClassification
+import android.view.textclassifier.TextClassifier
+import androidx.annotation.RequiresApi
+import com.google.zxing.client.result.WifiParsedResult
+import org.lineageos.aperture.R
+
+@RequiresApi(Build.VERSION_CODES.R)
+fun WifiParsedResult.createIntent() = Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
+    putExtra(
+        Settings.EXTRA_WIFI_NETWORK_LIST,
+        arrayListOf(
+            WifiNetworkSuggestion.Builder()
+                .setSsid(ssid)
+                .setIsHiddenSsid(isHidden)
+                .apply {
+                    password?.let {
+                        when (networkEncryption) {
+                            "WPA" -> {
+                                // Per specs, Wi-Fi QR codes are only used for
+                                // WPA2 and WPA-Mixed networks, we can safely assume
+                                // this networks supports WPA2
+                                setWpa2Passphrase(it)
+                            }
+
+                            "SAE" -> {
+                                setWpa3Passphrase(it)
+                            }
+                        }
+                    }
+                }
+                .build()
+        )
+    )
+}
+
+fun WifiParsedResult.createTextClassification(context: Context) = TextClassification.Builder()
+    .setText(ssid)
+    .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+    .apply {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            addAction(
+                RemoteAction::class.build(
+                    context,
+                    R.drawable.ic_network_wifi,
+                    R.string.qr_wifi_title,
+                    R.string.qr_wifi_content_description,
+                    createIntent()
+                )
+            )
+        }
+    }
+    .build()
diff --git a/app/src/main/java/org/lineageos/aperture/qr/QrImageAnalyzer.kt b/app/src/main/java/org/lineageos/aperture/qr/QrImageAnalyzer.kt
index 3621a1c..0219337 100644
--- a/app/src/main/java/org/lineageos/aperture/qr/QrImageAnalyzer.kt
+++ b/app/src/main/java/org/lineageos/aperture/qr/QrImageAnalyzer.kt
@@ -7,25 +7,27 @@
 
 import android.app.Activity
 import android.app.KeyguardManager
+import android.app.PendingIntent
 import android.content.ClipData
 import android.content.ClipDescription
 import android.content.ClipboardManager
 import android.content.Intent
 import android.os.Build
 import android.os.Handler
-import android.os.LocaleList
 import android.os.Looper
-import android.text.SpannableString
 import android.text.method.LinkMovementMethod
+import android.view.textclassifier.TextClassificationManager
 import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
+import android.widget.Toast
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.appcompat.widget.LinearLayoutCompat.LayoutParams
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageProxy
 import androidx.cardview.widget.CardView
+import androidx.core.graphics.drawable.DrawableCompat
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.google.android.material.button.MaterialButton
 import com.google.zxing.BinaryBitmap
@@ -34,8 +36,10 @@
 import com.google.zxing.common.HybridBinarizer
 import org.lineageos.aperture.R
 import org.lineageos.aperture.ext.*
+import kotlin.reflect.cast
 
 class QrImageAnalyzer(private val activity: Activity) : ImageAnalysis.Analyzer {
+    // Views
     private val bottomSheetDialog by lazy {
         BottomSheetDialog(activity).apply {
             setContentView(R.layout.qr_bottom_sheet_dialog)
@@ -48,9 +52,7 @@
         bottomSheetDialog.findViewById<TextView>(R.id.title)!!
     }
     private val bottomSheetDialogData by lazy {
-        bottomSheetDialog.findViewById<TextView>(R.id.data)!!.apply {
-            setTextClassifier(QrTextClassifier(context, textClassifier))
-        }
+        bottomSheetDialog.findViewById<TextView>(R.id.data)!!
     }
     private val bottomSheetDialogIcon by lazy {
         bottomSheetDialog.findViewById<ImageView>(R.id.icon)!!
@@ -65,26 +67,36 @@
         bottomSheetDialog.findViewById<LinearLayout>(R.id.actionsLayout)!!
     }
 
-    private val reader by lazy { MultiFormatReader() }
-
+    // System services
     private val clipboardManager by lazy { activity.getSystemService(ClipboardManager::class.java) }
     private val keyguardManager by lazy { activity.getSystemService(KeyguardManager::class.java) }
+    private val textClassificationManager by lazy {
+        activity.getSystemService(TextClassificationManager::class.java)
+    }
+
+    // QR
+    private val reader by lazy { MultiFormatReader() }
+
+    private val qrTextClassifier by lazy {
+        QrTextClassifier(activity, textClassificationManager.textClassifier)
+    }
 
     override fun analyze(image: ImageProxy) {
-        val source = image.planarYUVLuminanceSource
+        image.use {
+            val source = image.planarYUVLuminanceSource
 
-        val result = runCatching {
-            reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
-        }.getOrNull() ?: runCatching {
-            reader.decodeWithState(BinaryBitmap(HybridBinarizer(source.invert())))
-        }.getOrNull()
+            val result = runCatching {
+                reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
+            }.getOrNull() ?: runCatching {
+                reader.decodeWithState(BinaryBitmap(HybridBinarizer(source.invert())))
+            }.getOrNull()
 
-        result?.let {
-            showQrDialog(it)
+            result?.let {
+                showQrDialog(it)
+            }
+
+            reader.reset()
         }
-
-        reader.reset()
-        image.close()
     }
 
     private fun showQrDialog(result: Result) {
@@ -93,13 +105,12 @@
                 return@runOnUiThread
             }
 
+            val text = result.text ?: return@runOnUiThread
+            bottomSheetDialogData.text = text
+
             // Classify message
-            val span = SpannableString(result.text)
-            bottomSheetDialogData.text = span
             Thread {
-                val textClassification = bottomSheetDialogData.textClassifier.classifyText(
-                    span, 0, span.length, LocaleList.getDefault()
-                )
+                val textClassification = qrTextClassifier.classifyText(result)
 
                 activity.runOnUiThread {
                     bottomSheetDialogData.text = textClassification.text
@@ -108,17 +119,39 @@
                         textClassification.actions.isNotEmpty()
                     ) {
                         with(textClassification.actions[0]) {
-                            bottomSheetDialogCardView.setOnClickListener { actionIntent.send() }
+                            bottomSheetDialogCardView.setOnClickListener {
+                                try {
+                                    actionIntent.send()
+                                } catch (e: PendingIntent.CanceledException) {
+                                    Toast.makeText(
+                                        activity,
+                                        R.string.qr_no_app_available_for_action,
+                                        Toast.LENGTH_SHORT
+                                    ).show()
+                                }
+                            }
+                            bottomSheetDialogCardView.contentDescription = contentDescription
                             bottomSheetDialogData.movementMethod = null
-                            bottomSheetDialogTitle.text = this.title
+                            bottomSheetDialogTitle.text = title
                             this.icon.loadDrawableAsync(activity, {
                                 bottomSheetDialogIcon.setImageDrawable(it)
                             }, Handler(Looper.getMainLooper()))
                         }
                         for (action in textClassification.actions.drop(1)) {
                             bottomSheetDialogActionsLayout.addView(inflateButton().apply {
-                                setOnClickListener { action.actionIntent.send() }
-                                text = action.title
+                                setOnClickListener {
+                                    try {
+                                        action.actionIntent.send()
+                                    } catch (e: PendingIntent.CanceledException) {
+                                        Toast.makeText(
+                                            activity,
+                                            R.string.qr_no_app_available_for_action,
+                                            Toast.LENGTH_SHORT
+                                        ).show()
+                                    }
+                                }
+                                contentDescription = action.contentDescription
+                                this.text = action.title
                                 action.icon.loadDrawableAsync(activity, {
                                     it.setBounds(0, 0, 15.px, 15.px)
                                     setCompoundDrawables(
@@ -131,7 +164,18 @@
                         bottomSheetDialogCardView.setOnClickListener {}
                         bottomSheetDialogTitle.text = activity.resources.getText(R.string.qr_text)
                         bottomSheetDialogIcon.setImageDrawable(
-                            AppCompatResources.getDrawable(activity, R.drawable.ic_qr_type_text)
+                            AppCompatResources.getDrawable(
+                                activity, R.drawable.ic_text_snippet
+                            )?.let {
+                                DrawableCompat.wrap(it.mutate()).apply {
+                                    DrawableCompat.setTint(
+                                        this,
+                                        activity.getThemeColor(
+                                            com.google.android.material.R.attr.colorOnBackground
+                                        )
+                                    )
+                                }
+                            }
                         )
                     }
                 }
@@ -146,7 +190,7 @@
             bottomSheetDialogCopy.setOnClickListener {
                 clipboardManager.setPrimaryClip(
                     ClipData.newPlainText(
-                        "", result.text
+                        "", text
                     )
                 )
             }
@@ -171,12 +215,13 @@
         }
     }
 
-    private fun inflateButton(): MaterialButton {
-        val button = activity.layoutInflater.inflate(
-            R.layout.qr_bottom_sheet_action_button, bottomSheetDialogActionsLayout, false
-        ) as MaterialButton
-        return button.apply {
-            layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
-        }
+    private fun inflateButton() = MaterialButton::class.cast(
+        activity.layoutInflater.inflate(
+            R.layout.qr_bottom_sheet_action_button,
+            bottomSheetDialogActionsLayout,
+            false
+        )
+    ).apply {
+        layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
     }
 }
diff --git a/app/src/main/java/org/lineageos/aperture/qr/QrTextClassifier.kt b/app/src/main/java/org/lineageos/aperture/qr/QrTextClassifier.kt
index 4542743..68ebf5e 100644
--- a/app/src/main/java/org/lineageos/aperture/qr/QrTextClassifier.kt
+++ b/app/src/main/java/org/lineageos/aperture/qr/QrTextClassifier.kt
@@ -5,96 +5,75 @@
 
 package org.lineageos.aperture.qr
 
-import android.app.PendingIntent
 import android.app.RemoteAction
 import android.content.Context
 import android.content.Intent
-import android.graphics.drawable.Icon
 import android.net.Uri
 import android.net.wifi.WifiManager
 import android.os.Build
 import android.os.LocaleList
 import android.provider.Settings
+import android.text.SpannableString
 import android.view.textclassifier.TextClassification
 import android.view.textclassifier.TextClassifier
+import com.google.zxing.Result
+import com.google.zxing.client.result.ResultParser
+import com.google.zxing.client.result.URIParsedResult
 import org.lineageos.aperture.R
+import org.lineageos.aperture.ext.*
+import kotlin.reflect.safeCast
 
 class QrTextClassifier(
-    private val context: Context, private val parent: TextClassifier
-) : TextClassifier {
+    private val context: Context, private val textClassifier: TextClassifier
+) {
     private val wifiManager by lazy {
         runCatching { context.getSystemService(WifiManager::class.java) }.getOrNull()
     }
 
-    override fun classifyText(
-        text: CharSequence,
-        startIndex: Int,
-        endIndex: Int,
-        defaultLocales: LocaleList?
-    ): TextClassification = when {
-        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
-                isValidDppUri(text.toString()) &&
-                wifiManager?.isEasyConnectSupported == true -> {
-            TextClassification.Builder()
-                .setText(context.getString(R.string.qr_dpp_description))
-                .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
-                .addAction(
-                    RemoteAction(
-                        Icon.createWithResource(context, R.drawable.ic_network_wifi),
-                        context.getString(R.string.qr_dpp_title),
-                        context.getString(R.string.qr_dpp_description),
-                        PendingIntent.getActivity(
-                            context,
-                            0,
-                            Intent(Settings.ACTION_PROCESS_WIFI_EASY_CONNECT_URI).apply {
-                                data = Uri.parse(text.toString())
-                            },
-                            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
-                        )
-                    )
-                )
-                .build()
+    fun classifyText(result: Result): TextClassification {
+        // Try with ZXing parser
+        val parsedResult = ResultParser.parseResult(result)
+        parsedResult?.createTextClassification(context)?.let {
+            return it
         }
 
-        Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isValidWifiUri(text.toString()) -> {
-            val wifiNetwork = WifiNetwork.fromQr(text.toString())!!
-            val networkSuggestion = wifiNetwork.toNetworkSuggestion()!!
+        // We handle URIParsedResult here
+        val text = URIParsedResult::class.safeCast(parsedResult)?.uri ?: result.text
 
-            TextClassification.Builder()
-                .setText(wifiNetwork.ssid)
-                .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
-                .addAction(
-                    RemoteAction(
-                        Icon.createWithResource(context, R.drawable.ic_network_wifi),
-                        context.getString(R.string.qr_wifi_title),
-                        wifiNetwork.ssid,
-                        PendingIntent.getActivity(
-                            context,
-                            0,
-                            Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
-                                putExtra(
-                                    Settings.EXTRA_WIFI_NETWORK_LIST,
-                                    arrayListOf(networkSuggestion)
-                                )
-                            },
-                            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+        // Try parsing it as a Uri
+        Uri.parse(text.toString()).let { uri ->
+            when (uri.scheme?.lowercase()) {
+                // Wi-Fi DPP
+                SCHEME_DPP -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
+                    wifiManager?.isEasyConnectSupported == true
+                ) {
+                    return TextClassification.Builder()
+                        .setText(context.getString(R.string.qr_dpp_description))
+                        .setEntityType(TextClassifier.TYPE_OTHER, 1.0f)
+                        .addAction(
+                            RemoteAction::class.build(
+                                context,
+                                R.drawable.ic_network_wifi,
+                                R.string.qr_dpp_title,
+                                R.string.qr_dpp_description,
+                                Intent(Settings.ACTION_PROCESS_WIFI_EASY_CONNECT_URI).apply {
+                                    data = uri
+                                }
+                            )
                         )
-                    )
-                )
-                .build()
+                        .build()
+                }
+            }
         }
 
-        else -> parent.classifyText(text, startIndex, endIndex, defaultLocales)
+        // Let Android classify it
+        val spannableString = SpannableString(text)
+        return textClassifier.classifyText(
+            spannableString, 0, spannableString.length, LocaleList.getDefault()
+        )
     }
 
     companion object {
-        private fun isValidDppUri(text: String): Boolean =
-            text.startsWith("DPP:") &&
-                    text.split(";").firstOrNull { it.startsWith("K:") } != null &&
-                    runCatching { Uri.parse(text) }.getOrNull() != null
-
-        private fun isValidWifiUri(text: String) =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
-                    WifiNetwork.fromQr(text)?.toNetworkSuggestion() != null
+        private const val SCHEME_DPP = "dpp"
     }
 }
diff --git a/app/src/main/java/org/lineageos/aperture/qr/WifiNetwork.kt b/app/src/main/java/org/lineageos/aperture/qr/WifiNetwork.kt
deleted file mode 100644
index 32193d2..0000000
--- a/app/src/main/java/org/lineageos/aperture/qr/WifiNetwork.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2023 The LineageOS Project
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.lineageos.aperture.qr
-
-import android.net.wifi.WifiNetworkSuggestion
-import android.os.Build
-import androidx.annotation.RequiresApi
-import com.google.zxing.BarcodeFormat
-import com.google.zxing.Result
-import com.google.zxing.client.result.WifiResultParser
-
-data class WifiNetwork(
-    val ssid: String,
-    val isSsidHidden: Boolean = false,
-    val password: String? = null,
-    val encryptionType: EncryptionType = EncryptionType.NONE
-) {
-    enum class EncryptionType {
-        NONE,
-        WEP,
-        WPA,
-        SAE;
-    }
-
-    init {
-        assert((encryptionType == EncryptionType.NONE) == (password == null)) {
-            "Invalid encryption type/password combination"
-        }
-    }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    fun toNetworkSuggestion(): WifiNetworkSuggestion? = when (encryptionType) {
-        EncryptionType.WEP -> null
-        else -> WifiNetworkSuggestion.Builder()
-            .setSsid(ssid)
-            .setIsHiddenSsid(isSsidHidden)
-            .apply {
-                password?.let {
-                    when (encryptionType) {
-                        EncryptionType.WPA -> {
-                            // Per specs, Wi-Fi QR codes are only used for
-                            // WPA2 and WPA-Mixed networks, we can safely assume
-                            // this networks supports WPA2
-                            setWpa2Passphrase(it)
-                        }
-
-                        EncryptionType.SAE -> {
-                            setWpa3Passphrase(it)
-                        }
-
-                        else -> throw Exception("Invalid encryption type/password combination")
-                    }
-                }
-            }
-            .build()
-    }
-
-    companion object {
-        fun fromQr(text: String): WifiNetwork? {
-            val result = WifiResultParser().parse(
-                Result(text, null, null, BarcodeFormat.QR_CODE)
-            ) ?: return null
-
-            val encryptionType = when (result.networkEncryption) {
-                "WEP" -> EncryptionType.WEP
-                "WPA" -> EncryptionType.WPA
-                "SAE" -> EncryptionType.SAE
-                else -> EncryptionType.NONE
-            }
-
-            return runCatching {
-                WifiNetwork(result.ssid, result.isHidden, result.password, encryptionType)
-            }.getOrNull()
-        }
-    }
-}
diff --git a/app/src/main/res/drawable/ic_book.xml b/app/src/main/res/drawable/ic_book.xml
new file mode 100644
index 0000000..1c6d452
--- /dev/null
+++ b/app/src/main/res/drawable/ic_book.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM9,4h2v5l-1,-0.75L9,9L9,4zM18,20L6,20L6,4h1v9l3,-2.25L13,13L13,4h5v16z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_calendar_add_on.xml b/app/src/main/res/drawable/ic_calendar_add_on.xml
new file mode 100644
index 0000000..d4cfef3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_calendar_add_on.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M680,880L680,760L560,760L560,680L680,680L680,560L760,560L760,680L880,680L880,760L760,760L760,880L680,880ZM200,800Q167,800 143.5,776.5Q120,753 120,720L120,240Q120,207 143.5,183.5Q167,160 200,160L240,160L240,80L320,80L320,160L560,160L560,80L640,80L640,160L680,160Q713,160 736.5,183.5Q760,207 760,240L760,484Q740,481 720,481Q700,481 680,484L680,400L200,400L200,720Q200,720 200,720Q200,720 200,720L480,720Q480,740 483,760Q486,780 494,800L200,800ZM200,320L680,320L680,240Q680,240 680,240Q680,240 680,240L200,240Q200,240 200,240Q200,240 200,240L200,320ZM200,320L200,240Q200,240 200,240Q200,240 200,240L200,240Q200,240 200,240Q200,240 200,240L200,320L200,320Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_contact_phone.xml b/app/src/main/res/drawable/ic_contact_phone.xml
new file mode 100644
index 0000000..19d5ef7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contact_phone.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M80,840Q47,840 23.5,816.5Q0,793 0,760L0,200Q0,167 23.5,143.5Q47,120 80,120L880,120Q913,120 936.5,143.5Q960,167 960,200L960,760Q960,793 936.5,816.5Q913,840 880,840L80,840ZM636,760L880,760Q880,760 880,760Q880,760 880,760L880,200Q880,200 880,200Q880,200 880,200L80,200Q80,200 80,200Q80,200 80,200L80,760Q80,760 80,760Q80,760 80,760L84,760Q126,685 200,642.5Q274,600 360,600Q446,600 520,642.5Q594,685 636,760ZM360,560Q410,560 445,525Q480,490 480,440Q480,390 445,355Q410,320 360,320Q310,320 275,355Q240,390 240,440Q240,490 275,525Q310,560 360,560ZM760,720L840,640L780,560L714,560Q708,542 704,521.5Q700,501 700,480Q700,459 704,439.5Q708,420 714,400L780,400L840,320L760,240Q706,282 673,346.5Q640,411 640,480Q640,549 673,613.5Q706,678 760,720ZM182,760L538,760Q504,722 457.5,701Q411,680 360,680Q309,680 263,701Q217,722 182,760ZM360,480Q343,480 331.5,468.5Q320,457 320,440Q320,423 331.5,411.5Q343,400 360,400Q377,400 388.5,411.5Q400,423 400,440Q400,457 388.5,468.5Q377,480 360,480ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_directions_car.xml b/app/src/main/res/drawable/ic_directions_car.xml
new file mode 100644
index 0000000..b39f533
--- /dev/null
+++ b/app/src/main/res/drawable/ic_directions_car.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11c-0.66,0 -1.21,0.42 -1.42,1.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8l-2.08,-5.99zM6.85,7h10.29l1.08,3.11H5.77L6.85,7zM19,17H5v-5h14v5z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M7.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M16.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml
new file mode 100644
index 0000000..63a9bd8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_email.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_location_on.xml b/app/src/main/res/drawable/ic_location_on.xml
new file mode 100644
index 0000000..f1db4bd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_location_on.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,9m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_open_in_browser.xml b/app/src/main/res/drawable/ic_open_in_browser.xml
new file mode 100644
index 0000000..271ba6e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_open_in_browser.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml
new file mode 100644
index 0000000..ae7ecad
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6.54,5c0.06,0.89 0.21,1.76 0.45,2.59l-1.2,1.2c-0.41,-1.2 -0.67,-2.47 -0.76,-3.79h1.51m9.86,12.02c0.85,0.24 1.72,0.39 2.6,0.45v1.49c-1.32,-0.09 -2.59,-0.35 -3.8,-0.75l1.2,-1.19M7.5,3H4c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.49c0,-0.55 -0.45,-1 -1,-1 -1.24,0 -2.45,-0.2 -3.57,-0.57 -0.1,-0.04 -0.21,-0.05 -0.31,-0.05 -0.26,0 -0.51,0.1 -0.71,0.29l-2.2,2.2c-2.83,-1.45 -5.15,-3.76 -6.59,-6.59l2.2,-2.2c0.28,-0.28 0.36,-0.67 0.25,-1.02C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml
new file mode 100644
index 0000000..3cd15b2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shopping_cart.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15.55,13c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49c0.37,-0.66 -0.11,-1.48 -0.87,-1.48L5.21,4l-0.94,-2L1,2v2h2l3.6,7.59 -1.35,2.44C4.52,15.37 5.48,17 7,17h12v-2L7,15l1.1,-2h7.45zM6.16,6h12.15l-2.76,5L8.53,11L6.16,6zM7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2 -0.9,-2 -2,-2zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_sms.xml b/app/src/main/res/drawable/ic_sms.xml
new file mode 100644
index 0000000..b20d6f0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sms.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L5.17,16L4,17.17L4,4h16v12zM7,9h2v2L7,11zM15,9h2v2h-2zM11,9h2v2h-2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_qr_type_text.xml b/app/src/main/res/drawable/ic_text_snippet.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_qr_type_text.xml
rename to app/src/main/res/drawable/ic_text_snippet.xml
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a5ba743..f8655a1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,10 +61,34 @@
     <string name="qr_copy_description">Copy to clipboard</string>
     <string name="qr_icon_description">Icon</string>
     <string name="qr_share_description">Share</string>
-    <string name="qr_wifi_title">Connect to this Wi-Fi network</string>
+    <string name="qr_no_app_available_for_action">No app available to handle this action</string>
+
+    <!-- QR types -->
+    <string name="qr_address_title">Add contact</string>
+    <string name="qr_address_content_description">Add contact</string>
+    <string name="qr_calendar_title">Add event to calendar</string>
+    <string name="qr_calendar_content_description">Add this event to the calendar</string>
     <string name="qr_dpp_title" translatable="false">Wi-Fi Easy Connect™ (DPP)</string>
     <string name="qr_dpp_description">Configure this device</string>
+    <string name="qr_email_title">Send a new email</string>
+    <string name="qr_email_content_description">Compose a new email to the specified emails</string>
+    <string name="qr_geo_title">Open this location</string>
+    <string name="qr_geo_content_description">Open this location</string>
+    <string name="qr_isbn_title">Lookup this ISBN</string>
+    <string name="qr_isbn_content_description">Search this ISBN on isbnsearch.org</string>
+    <string name="qr_product_title">Lookup product</string>
+    <string name="qr_product_content_description">Lookup this product ID barcode</string>
+    <string name="qr_sms_title">Send a new SMS</string>
+    <string name="qr_sms_content_description">Send a new SMS to the specified recipients</string>
+    <string name="qr_tel_title">Call phone number</string>
+    <string name="qr_tel_content_description">Call the phone number</string>
     <string name="qr_text">Text</string>
+    <string name="qr_uri_title">Open URL</string>
+    <string name="qr_uri_content_description">Open this URL with the appropriate app if supported</string>
+    <string name="qr_vin_title">Lookup VIN</string>
+    <string name="qr_vin_content_description">Lookup this Vehicle Identification Number (VIN)</string>
+    <string name="qr_wifi_title">Connect to this Wi-Fi network</string>
+    <string name="qr_wifi_content_description">Add this Wi-Fi network to the list of known networks and connect the device to it</string>
 
     <!-- Settings title -->
     <string name="title_activity_settings">Settings</string>