-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add support for various actionable Qr codes
add support for `sms`, `smsto`, `tel`, `facetime`, `facetime-audio`, `geo`, `mailto`, `vcard` and `mecard` [facetime and facetime audio spec](https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/FacetimeLinks/FacetimeLinks.html)
1 parent
2db0f15
commit f05b80e
Showing
12 changed files
with
610 additions
and
0 deletions.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
app/src/main/java/app/grapheneos/camera/qr/data/QrCards.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package app.grapheneos.camera.qr.data | ||
|
||
import android.content.ClipData | ||
import android.content.ClipboardManager | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.net.Uri | ||
import android.os.Build | ||
import androidx.appcompat.view.ContextThemeWrapper | ||
import androidx.core.net.MailTo | ||
import app.grapheneos.camera.R | ||
import app.grapheneos.camera.qr.handler.addToContact | ||
import app.grapheneos.camera.qr.handler.convertWifiQrDataToIntent | ||
import app.grapheneos.camera.qr.parser.vcardToIntent | ||
import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
|
||
sealed class QrIntent { | ||
abstract fun startIntent(context: Context): Boolean | ||
} | ||
|
||
enum class WifiSecurityType { | ||
Open, | ||
WPA, | ||
WPA2, | ||
WPA3 | ||
} | ||
|
||
data class Wifi( | ||
val ssid: String, | ||
val securityType: WifiSecurityType, | ||
val sharedKey: String, | ||
val isHidden: Boolean | ||
) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||
context.startActivity(convertWifiQrDataToIntent(this)) | ||
} else { | ||
showWifiDialog(context) | ||
} | ||
return true | ||
} | ||
|
||
private fun showWifiDialog(context: Context) { | ||
val dialogContext = ContextThemeWrapper( | ||
context, | ||
com.google.android.material.R.style.Theme_MaterialComponents_DayNight | ||
) | ||
MaterialAlertDialogBuilder(dialogContext) | ||
.setTitle(R.string.wifi_dialog_title) | ||
.setMessage(context.getString(R.string.wifi_dialog_message, ssid)) | ||
.setPositiveButton(R.string.wifi_dialog_button_positive) { _, _ -> | ||
copySharedKeyToClipboard(context) | ||
} | ||
.setNegativeButton(R.string.wifi_dialog_button_negative, null) | ||
.show() | ||
} | ||
|
||
private fun copySharedKeyToClipboard(context: Context) { | ||
val sharedKeyClipData = ClipData.newPlainText( | ||
context.getString(R.string.wifi_password_clipboard_label), | ||
sharedKey | ||
) | ||
context.getSystemService(ClipboardManager::class.java).setPrimaryClip(sharedKeyClipData) | ||
} | ||
|
||
} | ||
|
||
data class SMS(val number: String, val message: String) : QrIntent() { | ||
|
||
companion object { | ||
private const val EXTRA_SMS_BODY = "sms_body" | ||
private const val SMS_URI = "sms" | ||
} | ||
|
||
override fun startIntent(context: Context): Boolean { | ||
val messageIntent = Intent(Intent.ACTION_SENDTO).apply { | ||
data = Uri.parse("$SMS_URI:$number") | ||
putExtra(EXTRA_SMS_BODY, message) | ||
putExtra(Intent.EXTRA_TEXT, message) | ||
} | ||
context.startActivity(Intent.createChooser(messageIntent, null)) | ||
return true | ||
} | ||
} | ||
|
||
data class Phone(val number: Int) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
context.startActivity( | ||
Intent.createChooser( | ||
Intent(Intent.ACTION_DIAL).apply { | ||
data = Uri.parse("tel:${number}") | ||
}, null | ||
) | ||
) | ||
return true | ||
} | ||
} | ||
|
||
data class Mail(val mailTo: MailTo, val uri: Uri) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
context.startActivity( | ||
Intent.createChooser( | ||
Intent(Intent.ACTION_SENDTO, uri), null | ||
) | ||
) | ||
return true | ||
} | ||
} | ||
|
||
data class GEO(val lat: String, val long: String, val altitude: String, val uri: Uri) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
context.startActivity( | ||
Intent.createChooser( | ||
Intent(Intent.ACTION_VIEW).apply { | ||
data = uri | ||
}, null | ||
) | ||
) | ||
return true | ||
} | ||
} | ||
|
||
data class VCard(val input: String) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
context.startActivity(vcardToIntent(input, context)) | ||
return true | ||
} | ||
} | ||
|
||
data class MeCard( | ||
|
||
val name: String, | ||
val email: String, | ||
val note: String, | ||
val sound: String, | ||
val telephoneNumber: String, | ||
|
||
//supported in v2+// | ||
|
||
val telephoneNumberAv: String, | ||
|
||
//supported in v3+// | ||
|
||
val birthDate: String, //YYYY-MM-DD | ||
val address: String, | ||
val nickName: String, | ||
val url: String | ||
) : QrIntent() { | ||
override fun startIntent(context: Context): Boolean { | ||
context.startActivity(addToContact()) | ||
return true | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
app/src/main/java/app/grapheneos/camera/qr/handler/MeCardIntents.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package app.grapheneos.camera.qr.handler | ||
|
||
|
||
import android.content.ContentValues | ||
import android.content.Intent | ||
import android.provider.ContactsContract | ||
import android.provider.ContactsContract.CommonDataKinds | ||
import android.provider.ContactsContract.CommonDataKinds.Nickname | ||
import android.provider.ContactsContract.CommonDataKinds.Website | ||
import android.provider.ContactsContract.Intents | ||
import app.grapheneos.camera.qr.data.MeCard | ||
|
||
private fun urlToContactField(url: String, type: Int = Website.TYPE_HOMEPAGE): ContentValues { | ||
return ContentValues().apply { | ||
put(ContactsContract.Data.MIMETYPE, Website.CONTENT_ITEM_TYPE) | ||
put(Website.TYPE, type) | ||
put(Website.URL, url) | ||
} | ||
} | ||
|
||
private fun nickNameToContactField(name: String, type: Int = Nickname.TYPE_DEFAULT): ContentValues { | ||
return ContentValues().apply { | ||
put(ContactsContract.Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) | ||
put(Nickname.TYPE, type) | ||
put(Nickname.NAME, name) | ||
} | ||
} | ||
|
||
private fun birthdayToContactField(date: String): ContentValues { | ||
return ContentValues().apply { | ||
put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) | ||
put(CommonDataKinds.Event.TYPE, CommonDataKinds.Event.TYPE_BIRTHDAY) | ||
put(CommonDataKinds.Event.START_DATE, date) | ||
} | ||
} | ||
|
||
private fun phoneticNameToContactField(name: String): ContentValues { | ||
return ContentValues().apply { | ||
put(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) | ||
put(CommonDataKinds.StructuredName.DISPLAY_NAME, name) | ||
} | ||
} | ||
|
||
fun MeCard.addToContact(): Intent { | ||
|
||
val data = ArrayList<ContentValues>().apply { | ||
if (url.isNotBlank()) add(urlToContactField(url)) | ||
if (nickName.isNotBlank()) add(nickNameToContactField(nickName)) | ||
if (birthDate.isNotBlank()) add(birthdayToContactField(birthDate)) | ||
if (sound.isNotBlank()) add(phoneticNameToContactField(sound)) | ||
} | ||
|
||
return Intent(Intents.Insert.ACTION).apply { | ||
type = ContactsContract.RawContacts.CONTENT_TYPE | ||
|
||
if (name.isNotBlank()) putExtra(Intents.Insert.NAME, name) | ||
if (email.isNotBlank()) putExtra(Intents.Insert.EMAIL, email) | ||
if (note.isNotBlank()) putExtra(Intents.Insert.NOTES, note) | ||
if (telephoneNumber.isNotBlank()) putExtra(Intents.Insert.PHONE, telephoneNumber) | ||
if (address.isNotBlank()) putExtra(Intents.Insert.POSTAL, address) | ||
if (data.isNotEmpty()) putParcelableArrayListExtra(Intents.Insert.DATA, data) | ||
if (telephoneNumberAv.isNotBlank()) { | ||
putExtra(Intents.Insert.SECONDARY_PHONE, telephoneNumberAv) | ||
} | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
app/src/main/java/app/grapheneos/camera/qr/handler/WifiQrIntents.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package app.grapheneos.camera.qr.handler | ||
|
||
import android.content.Intent | ||
import android.net.wifi.WifiNetworkSuggestion | ||
import android.os.Build | ||
import android.provider.Settings | ||
import androidx.annotation.RequiresApi | ||
import app.grapheneos.camera.qr.data.Wifi | ||
import app.grapheneos.camera.qr.data.WifiSecurityType | ||
|
||
@RequiresApi(Build.VERSION_CODES.R) | ||
fun convertWifiQrDataToIntent(wifi: Wifi): Intent { | ||
|
||
return Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply { | ||
putExtra( | ||
Settings.EXTRA_WIFI_NETWORK_LIST, | ||
arrayListOf(convertWifiQrDataToWifiNetworkSuggestion(wifi)) | ||
) | ||
} | ||
} | ||
|
||
fun convertWifiQrDataToWifiNetworkSuggestion(wifi: Wifi): WifiNetworkSuggestion { | ||
|
||
val builder = WifiNetworkSuggestion.Builder() | ||
.setIsHiddenSsid(wifi.isHidden) | ||
.setSsid(wifi.ssid) | ||
|
||
return when (wifi.securityType) { | ||
WifiSecurityType.Open -> builder.build() | ||
|
||
WifiSecurityType.WPA, WifiSecurityType.WPA2 -> | ||
builder.setWpa2Passphrase(wifi.sharedKey) | ||
.build() | ||
|
||
WifiSecurityType.WPA3 -> builder.setWpa3Passphrase(wifi.sharedKey).build() | ||
} | ||
|
||
} |
24 changes: 24 additions & 0 deletions
24
app/src/main/java/app/grapheneos/camera/qr/parser/GeoParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import android.net.Uri | ||
import app.grapheneos.camera.qr.data.GEO | ||
import app.grapheneos.camera.util.removePrefixCaseInsensitive | ||
import java.util.regex.Pattern | ||
|
||
const val KEY_GEO = "geo:" | ||
|
||
fun parseGeo(input: String): GEO? { | ||
|
||
if (!input.startsWith(KEY_GEO, ignoreCase = true)) return null | ||
val rawText = input.removePrefixCaseInsensitive(KEY_GEO) | ||
val geoDividerFinder = Regex(Pattern.quote(",")) | ||
val parts = rawText.split(geoDividerFinder) | ||
val defaultValue = "" | ||
|
||
return GEO( | ||
lat = parts.getOrElse(0) { defaultValue }, | ||
long = parts.getOrElse(1) { defaultValue }, | ||
altitude = parts.getOrElse(2) { defaultValue }, | ||
uri = Uri.parse(input) | ||
) | ||
} |
12 changes: 12 additions & 0 deletions
12
app/src/main/java/app/grapheneos/camera/qr/parser/MailParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import android.net.Uri | ||
import androidx.core.net.MailTo | ||
import app.grapheneos.camera.qr.data.Mail | ||
|
||
fun parseMail(input: String): Mail? { | ||
val uri = Uri.parse(input) ?: return null | ||
if (!MailTo.isMailTo(uri)) return null | ||
val mailTo = MailTo.parse(uri) | ||
return Mail(mailTo, uri) | ||
} |
93 changes: 93 additions & 0 deletions
93
app/src/main/java/app/grapheneos/camera/qr/parser/MeCardParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import app.grapheneos.camera.qr.data.MeCard | ||
import app.grapheneos.camera.util.removePrefixCaseInsensitive | ||
|
||
const val KEY_MECARD = "MECARD:" | ||
const val MECARD_KEY_ADDRESS = "ADR:" | ||
const val MECARD_KEY_BIRTHDAY = "BDAY:" | ||
const val MECARD_KEY_EMAIL = "EMAIL:" | ||
const val MECARD_KEY_NAME = "N:" | ||
const val MECARD_KYE_NICKNAME = "NICKNAME:" | ||
const val MECARD_KEY_NOTE = "NOTE:" | ||
const val MECARD_KYE_SOUND = "SOUND:" | ||
const val MECARD_KEY_TELEPHONE = "TEL:" | ||
const val MECARD_KEY_TELEPHONE_AV = "TEL-AV:" | ||
const val MECARD_KEY_URL = "URL:" | ||
|
||
fun parseMeCard(input: String): MeCard? { | ||
|
||
if (!input.startsWith(KEY_MECARD, ignoreCase = true)) { | ||
return null | ||
} | ||
|
||
val rawText = input.removePrefixCaseInsensitive(KEY_MECARD) | ||
|
||
val escapeChar = Regex.escape("\\") | ||
val splitAt = Regex.escape(";") | ||
val pattern = Regex("(?<!${escapeChar})${splitAt}") | ||
val fields = rawText.splitToSequence(pattern) | ||
|
||
var name = "" | ||
var nickname = "" | ||
var email = "" | ||
var note = "" | ||
var sound = "" | ||
var telephoneNumber = "" | ||
var telephoneNumberAv = "" | ||
var birthDate = "" | ||
var address = "" | ||
var url = "" | ||
|
||
for (field in fields) { | ||
when { | ||
field.startsWith(MECARD_KEY_NAME, ignoreCase = true) -> { | ||
name = field.removePrefixCaseInsensitive(MECARD_KEY_NAME) | ||
} | ||
|
||
field.startsWith(MECARD_KYE_NICKNAME, ignoreCase = true) -> { | ||
nickname = field.removePrefixCaseInsensitive(MECARD_KYE_NICKNAME) | ||
} | ||
|
||
field.startsWith(MECARD_KYE_SOUND, ignoreCase = true) -> { | ||
sound = field.removePrefixCaseInsensitive(MECARD_KYE_SOUND) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_ADDRESS, ignoreCase = true) -> { | ||
address = field.removePrefixCaseInsensitive(MECARD_KEY_ADDRESS) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_TELEPHONE, ignoreCase = true) -> { | ||
telephoneNumber = field.removePrefixCaseInsensitive(MECARD_KEY_TELEPHONE) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_TELEPHONE_AV, ignoreCase = true) -> { | ||
telephoneNumberAv = field.removePrefixCaseInsensitive(MECARD_KEY_TELEPHONE_AV) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_EMAIL, ignoreCase = true) -> { | ||
email = field.removePrefixCaseInsensitive(MECARD_KEY_EMAIL) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_URL, ignoreCase = true) -> { | ||
url = field.removePrefixCaseInsensitive(MECARD_KEY_URL) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_NOTE, ignoreCase = true) -> { | ||
note = field.removePrefixCaseInsensitive(MECARD_KEY_NOTE) | ||
} | ||
|
||
field.startsWith(MECARD_KEY_BIRTHDAY, ignoreCase = true) -> { | ||
birthDate = field.removePrefixCaseInsensitive(MECARD_KEY_BIRTHDAY) | ||
} | ||
|
||
} | ||
} | ||
|
||
return MeCard( | ||
name = name, email = email, note = note, sound = sound, | ||
telephoneNumber = telephoneNumber, telephoneNumberAv = telephoneNumberAv, | ||
birthDate = birthDate, address = address, nickName = nickname, url = url | ||
) | ||
|
||
} |
40 changes: 40 additions & 0 deletions
40
app/src/main/java/app/grapheneos/camera/qr/parser/PhoneParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import android.util.Patterns | ||
import app.grapheneos.camera.qr.data.Phone | ||
import app.grapheneos.camera.util.removePrefixCaseInsensitive | ||
|
||
const val KEY_PHONE = "tel:" | ||
const val KEY_FACETIME = "facetime:" | ||
const val KEY_FACETIME_AUDIO = "facetime-audio:" | ||
|
||
fun parsePhoneOrFacetime(input: String): Phone? { | ||
return when { | ||
input.startsWith(KEY_PHONE, ignoreCase = true) -> { | ||
|
||
val rawText = input.removePrefixCaseInsensitive(KEY_PHONE).replace("-", "") | ||
if (!Patterns.PHONE.matcher(rawText).find()) { | ||
return null | ||
} | ||
val phoneNumber = rawText.toIntOrNull() ?: 0 | ||
return Phone(phoneNumber) | ||
} | ||
|
||
input.startsWith(KEY_FACETIME, ignoreCase = true) || | ||
input.startsWith(KEY_FACETIME_AUDIO, ignoreCase = true) -> { | ||
|
||
val rawText = input.removePrefixCaseInsensitive(KEY_FACETIME) | ||
.removePrefixCaseInsensitive(KEY_FACETIME_AUDIO) | ||
.replace("-", "") | ||
|
||
if (!Patterns.PHONE.matcher(rawText).find()) { | ||
return null | ||
} | ||
val number = rawText.toIntOrNull() ?: 0 | ||
return Phone(number) | ||
} | ||
|
||
else -> null | ||
} | ||
|
||
} |
28 changes: 28 additions & 0 deletions
28
app/src/main/java/app/grapheneos/camera/qr/parser/SmsParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import app.grapheneos.camera.qr.data.SMS | ||
import app.grapheneos.camera.util.removePrefixCaseInsensitive | ||
import java.util.regex.Pattern | ||
import kotlin.math.min | ||
|
||
const val KEY_SMSTO = "smsto:" | ||
const val KEY_SMS = "sms:" | ||
|
||
fun parseSMS(input: String): SMS? { | ||
|
||
if (!input.startsWith(KEY_SMSTO, ignoreCase = true) && | ||
!input.startsWith(KEY_SMS, ignoreCase = true) | ||
) { | ||
return null | ||
} | ||
|
||
val rawText = input.removePrefixCaseInsensitive(KEY_SMSTO).removePrefixCaseInsensitive(KEY_SMS) | ||
|
||
val numberEndMatch = Regex(Pattern.quote(":")).find(rawText) | ||
val numberEndIndex = numberEndMatch?.range?.endInclusive ?: rawText.length | ||
|
||
val number = rawText.substring(0, numberEndIndex) | ||
val message = rawText.substring(min(numberEndIndex.plus(1), rawText.length), rawText.length) | ||
|
||
return SMS(number, message) | ||
} |
78 changes: 78 additions & 0 deletions
78
app/src/main/java/app/grapheneos/camera/qr/parser/WifiQrParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import app.grapheneos.camera.qr.data.Wifi | ||
import app.grapheneos.camera.qr.data.WifiSecurityType | ||
import app.grapheneos.camera.util.removePrefixCaseInsensitive | ||
|
||
const val WIFI_BEGINNING = "WIFI:" | ||
const val KYE_SSID = "S:" | ||
const val KYE_SECURITY_TYPE = "T:" | ||
const val KYE_SHARED_KEY = "P:" | ||
const val KYE_IS_HIDDEN = "H:" | ||
|
||
fun parseWifi(input: String): Wifi? { | ||
if (!input.startsWith(WIFI_BEGINNING, ignoreCase = true)) { | ||
return null | ||
} | ||
|
||
val rawText = input.removePrefixCaseInsensitive(WIFI_BEGINNING) | ||
|
||
val escapeChar = Regex.escape("\\") | ||
val splitAt = Regex.escape(";") | ||
val pattern = Regex("(?<!${escapeChar})${splitAt}") | ||
val fields = rawText.splitToSequence(pattern) | ||
|
||
var ssid = "" | ||
var type: WifiSecurityType? = null | ||
var sharedKey = "" | ||
var isHidden = false | ||
|
||
|
||
for (field in fields) { | ||
when { | ||
field.startsWith(KYE_SSID, ignoreCase = true) -> { | ||
ssid = field.removePrefixCaseInsensitive(KYE_SSID) | ||
} | ||
|
||
field.startsWith(KYE_SECURITY_TYPE, ignoreCase = true) -> { | ||
type = wifiQrTypeToSecurityType( | ||
field.removePrefixCaseInsensitive(KYE_SECURITY_TYPE) | ||
) | ||
} | ||
|
||
field.startsWith(KYE_SHARED_KEY, ignoreCase = true) -> { | ||
sharedKey = field.removePrefixCaseInsensitive(KYE_SHARED_KEY) | ||
} | ||
|
||
field.startsWith(KYE_IS_HIDDEN, ignoreCase = true) -> { | ||
isHidden = field.removePrefixCaseInsensitive(KYE_IS_HIDDEN) == "true" | ||
} | ||
} | ||
|
||
} | ||
|
||
if (ssid.isBlank() || | ||
type == null || | ||
(type != WifiSecurityType.Open && sharedKey.isEmpty()) | ||
) { | ||
return null | ||
} | ||
|
||
return Wifi( | ||
ssid, | ||
type, | ||
sharedKey, | ||
isHidden | ||
) | ||
} | ||
|
||
|
||
private fun wifiQrTypeToSecurityType(type: String): WifiSecurityType? { | ||
return when (type.uppercase()) { | ||
"NOPASS" -> WifiSecurityType.Open | ||
"WPA" -> WifiSecurityType.WPA | ||
"WPA2" -> WifiSecurityType.WPA2 | ||
"WPA3" -> WifiSecurityType.WPA3 | ||
else -> null | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
app/src/main/java/app/grapheneos/camera/qr/parser/vCardParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package app.grapheneos.camera.qr.parser | ||
|
||
import android.content.ContentResolver | ||
import android.content.ContentValues | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.net.Uri | ||
import android.os.Environment | ||
import android.provider.MediaStore | ||
import android.webkit.MimeTypeMap | ||
import app.grapheneos.camera.qr.data.VCard | ||
import java.text.SimpleDateFormat | ||
import java.util.Date | ||
import java.util.Locale | ||
|
||
const val VCARD_BEGINNING = "BEGIN:VCARD" | ||
const val VCARD_ENDING = "END:VCARD" | ||
|
||
fun parseVCard(input: String): VCard? { | ||
if (!input.startsWith(VCARD_BEGINNING, ignoreCase = true) | ||
|| !input.endsWith(VCARD_ENDING, ignoreCase = true) | ||
) { | ||
return null | ||
} | ||
|
||
return VCard(input) | ||
} | ||
|
||
fun vcardToIntent(input: String, context: Context): Intent { | ||
val time = SimpleDateFormat("yyyy-MM-dd_HH-mmss-SSS", Locale.US).format(Date()) | ||
val name = "vcard-${time}.vcf" | ||
val uri = saveViaMediaStore(context.contentResolver, name, input) | ||
|
||
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension("vcf") | ||
return Intent(Intent.ACTION_VIEW).apply { | ||
setDataAndType(uri, type) | ||
putExtra(Intent.EXTRA_STREAM, uri) | ||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
} | ||
} | ||
|
||
private fun saveViaMediaStore( | ||
contentResolver: ContentResolver, | ||
name: String, | ||
content: String | ||
): Uri? { | ||
val values = ContentValues() | ||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) | ||
values.put(MediaStore.MediaColumns.IS_PENDING, true) | ||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, name) | ||
|
||
val target = MediaStore.Downloads.EXTERNAL_CONTENT_URI | ||
val uri = contentResolver.insert(target, values) ?: return null | ||
|
||
contentResolver.openOutputStream(uri, "rw")?.use { | ||
it.write(content.toByteArray()) | ||
} | ||
|
||
contentResolver.update(uri, ContentValues().apply { | ||
put(MediaStore.MediaColumns.IS_PENDING, false) | ||
}, null, null) | ||
|
||
return uri | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package app.grapheneos.camera.util | ||
|
||
fun String.removePrefixCaseInsensitive(prefix: String): String { | ||
if (startsWith(prefix, ignoreCase = true)) { | ||
return substring(prefix.length) | ||
} | ||
return this | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters