Fork and stuff, first commit

main
Mia Raindrops 5 months ago
parent 97beb3d814
commit b9b11bc150
Signed by: Mia Raindrops
GPG Key ID: EFBDC68435A574B7

@ -1,50 +1,25 @@
# ntfy Android App
This is the Android app for [ntfy](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)). You can find the app in [F-Droid](https://f-droid.org/packages/io.heckel.ntfy/) or the [Play Store](https://play.google.com/store/apps/details?id=io.heckel.ntfy),
or as .apk files on the [releases page](https://github.com/binwiederhier/ntfy-android/releases).
# Ponypush
This is a fork of the [ntfy Android app](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)) made to work with Equestria.dev's notification servers and other technologies. It is not meant to be used outside of Equestria.dev's services.
# Build
## Build
## Building without Firebase (F-Droid flavor)
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
if you're self-hosting the server. Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
### Building without Firebase ("F-Droid flavor", using WebSockets)
In Android Studio, go to Build > Generate Signed Bundle/APK and select `fdroidDebug` or `fdroidRelease`.
# To build a bundle .aab (app/fdroid/release/*.aab)
./gradlew bundleFdroidRelease
```
## Building with Firebase (FCM, Google Play flavor)
### Building with Firebase ("Google Play flavor", using Firebase Cloud Messaging)
To build your own version with Firebase, you must:
* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease
```
* Create a Firebase account and create an Android app on it
* Place your account file at `app/google-services.json`
* In Android Studio, go to Build > Generate Signed Bundle/APK and select `playDebug` or `playRelease`.
## Translations
We're using [Weblate](https://hosted.weblate.org/projects/ntfy/) to translate the ntfy Android app. We'd love your participation.
## Notable differences
- Revamped UI, with a lot of options removed
- Most notably, it is not possible to use a third-party server other than notifications.equestria.dev
- Integration with Equestria.dev's tags
- Firebase Cloud Messaging support for notifications.equestria.dev
<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
## Upstream integration
When notable changes are made upstream (on the official ntfy app), Ponypush will attempt to integrate such changes as much as possible. However, we cannot guarantee that the implemented features will work the same as with the official ntfy app.
## License
Made with โค๏ธ by [Philipp C. Heckel](https://heckel.io), distributed under the [Apache License 2.0](LICENSE).
Thank you to these fantastic resources:
* [RecyclerViewKotlin](https://github.com/android/views-widgets-samples/tree/main/RecyclerViewKotlin) (Apache 2.0)
* [Just another Hacker News Android client](https://github.com/manoamaro/another-hacker-news-client) (MIT)
* [Android Room with a View](https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin) (Apache 2.0)
* [Firebase Messaging Example](https://github.com/firebase/quickstart-android/blob/7147f60451b3eeaaa05fc31208ffb67e2df73c3c/messaging/app/src/main/java/com/google/firebase/quickstart/fcm/kotlin/MyFirebaseMessagingService.kt) (Apache 2.0)
* [Designing a logo with Inkscape](https://www.youtube.com/watch?v=r2Kv61cd2P4)
* [Foreground service](https://robertohuertas.com/2019/06/29/android_foreground_services/)
* [github/gemoji](https://github.com/github/gemoji) (MIT) for as data source for an up-to-date [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file
* [emoji-java](https://github.com/vdurmont/emoji-java) (MIT) has been stripped and inlined to use the emoji.json file
This is a fork of ntfy by [Philipp C. Heckel](https://heckel.io), distributed under the [Apache License 2.0](LICENSE) like the original project.

@ -10,12 +10,13 @@ android {
compileSdkVersion 33
defaultConfig {
applicationId "io.heckel.ntfy"
applicationId "dev.equestria.notifications"
minSdkVersion 21
targetSdkVersion 33
versionCode 32
versionName "1.16.0"
versionCode 135
versionName "1.6.0"
buildConfigField 'String', "NTFY_VERSION", '"1.16.0"'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

@ -75,19 +75,6 @@
android:parentActivityName=".ui.DetailActivity">
</activity>
<!-- Share file activity, incoming files/shares -->
<activity android:name=".ui.ShareActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="text/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
<data android:mimeType="application/*" />
</intent-filter>
</activity>
<!-- Hack: Activity used for "view" action button with "clear=true" (to be able to cancel notifications and show a URL) -->
<activity
android:name=".msg.NotificationService$ViewActionWithClearActivity"

@ -69,7 +69,7 @@ class Backuper(val context: Context) {
repository.setDarkMode(settings.darkMode)
}
if (settings.connectionProtocol != null) {
repository.setConnectionProtocol(settings.connectionProtocol)
repository.setConnectionProtocol()
}
if (settings.broadcastEnabled != null) {
repository.setBroadcastEnabled(settings.broadcastEnabled)
@ -337,7 +337,7 @@ class Backuper(val context: Context) {
companion object {
private const val FILE_MAGIC = "ntfy2586"
private const val FILE_VERSION = 1
private const val TAG = "NtfyExporter"
private const val TAG = "PonypushExporter"
}
}

@ -272,14 +272,14 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
return sharedPrefs.getInt(SHARED_PREFS_DARK_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
fun setConnectionProtocol(connectionProtocol: String) {
fun setConnectionProtocol() {
sharedPrefs.edit()
.putString(SHARED_PREFS_CONNECTION_PROTOCOL, connectionProtocol)
.putString(SHARED_PREFS_CONNECTION_PROTOCOL, "ws")
.apply()
}
fun getConnectionProtocol(): String {
return sharedPrefs.getString(SHARED_PREFS_CONNECTION_PROTOCOL, null) ?: CONNECTION_PROTOCOL_JSONHTTP
return sharedPrefs.getString(SHARED_PREFS_CONNECTION_PROTOCOL, null) ?: CONNECTION_PROTOCOL_WS
}
fun getBroadcastEnabled(): Boolean {
@ -512,7 +512,6 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val INSISTENT_MAX_PRIORITY_USE_GLOBAL = -1 // Values must match values.xml
const val INSISTENT_MAX_PRIORITY_ENABLED = 1 // 0 = Disabled (but not needed in code)
const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp"
const val CONNECTION_PROTOCOL_WS = "ws"
const val BATTERY_OPTIMIZATIONS_REMIND_TIME_ALWAYS = 1L
@ -521,7 +520,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val WEBSOCKET_REMIND_TIME_ALWAYS = 1L
const val WEBSOCKET_REMIND_TIME_NEVER = Long.MAX_VALUE
private const val TAG = "NtfyRepository"
private const val TAG = "PonypushRepository"
private var instance: Repository? = null
fun getInstance(context: Context): Repository {

@ -169,8 +169,8 @@ class ApiService {
class EntityTooLargeException : Exception()
companion object {
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
private const val TAG = "NtfyApiService"
val USER_AGENT = "ponypush/${BuildConfig.VERSION_NAME} (Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"
private const val TAG = "PonypushApiService"
// These constants have corresponding values in the server codebase!
const val CONTROL_TOPIC = "~control"

@ -119,7 +119,7 @@ class BroadcastService(private val ctx: Context) {
}
companion object {
private const val TAG = "NtfyBroadcastService"
private const val TAG = "PonypushBroadcastService"
private const val DOES_NOT_EXIST = -2586000
// These constants cannot be changed without breaking the contract; also see manifest

@ -210,7 +210,7 @@ class DownloadAttachmentWorker(private val context: Context, params: WorkerParam
const val INPUT_DATA_USER_ACTION = "userAction"
const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
private const val TAG = "NtfyAttachDownload"
private const val TAG = "PonypushAttachDownload"
private const val ATTACHMENT_CACHE_DIR = "attachments"
private const val BUFFER_SIZE = 8 * 1024
private const val NOTIFICATION_UPDATE_INTERVAL_MILLIS = 800

@ -162,7 +162,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
const val MAX_CACHE_MILLIS = 1000*60*60*24 // 24 hours
const val ICON_CACHE_DIR = "icons"
private const val TAG = "NtfyIconDownload"
private const val TAG = "PonypushIconDownload"
private const val BUFFER_SIZE = 8 * 1024
}
}

@ -14,7 +14,7 @@ import io.heckel.ntfy.util.Log
* in a doze state and Internet may not be available. It's also best practice, apparently.
*/
object DownloadManager {
private const val TAG = "NtfyDownloadManager"
private const val TAG = "PonypushDownloadManager"
private const val DOWNLOAD_WORK_ATTACHMENT_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_"
private const val DOWNLOAD_WORK_ICON_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_ICON_"
private const val DOWNLOAD_WORK_BOTH_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_BOTH_"

@ -107,6 +107,6 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
}
companion object {
private const val TAG = "NtfyNotifDispatch"
private const val TAG = "PonypushNotifDispatch"
}
}

@ -22,6 +22,7 @@ import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
import java.util.*
import kotlin.reflect.typeOf
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -88,13 +89,35 @@ class NotificationService(val context: Context) {
}
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
val tags = notification.tags.split(",").map { it.trim() }
val icon = if (tags.contains("bits")) {
R.drawable.ic_toll_white_24dp
} else if (tags.contains("switch")) {
R.drawable.ic_layers_white_24dp
} else if (tags.contains("wakeup")) {
R.drawable.ic_bed_white_24dp
} else if (tags.contains("pleasure")) {
R.drawable.ic_play_for_work_white_24dp
} else if (tags.contains("delta")) {
R.drawable.ic_change_history_white_24dp
} else if (tags.contains("emergency")) {
R.drawable.ic_warning_amber_white_24dp
} else if (tags.contains("travelling")) {
R.drawable.ic_explore_white_24dp
} else if (tags.contains("alarm")) {
R.drawable.ic_av_timer_white_24dp
} else {
R.drawable.ic_notification
}
val title = formatTitle(subscription, notification)
val groupId = if (subscription.dedicatedChannels) subscriptionGroupId(subscription) else DEFAULT_GROUP
val channelId = toChannelId(groupId, notification.priority)
val insistent = notification.priority == PRIORITY_MAX &&
(repository.getInsistentMaxPriorityEnabled() || subscription.insistent == Repository.INSISTENT_MAX_PRIORITY_ENABLED)
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(icon)
.setColor(ContextCompat.getColor(context, Colors.notificationIcon(context)))
.setContentTitle(title)
.setOnlyAlertOnce(true) // Do not vibrate or play sound if already showing (updates!)
@ -527,7 +550,7 @@ class NotificationService(val context: Context) {
const val BROADCAST_TYPE_DOWNLOAD_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL"
const val BROADCAST_TYPE_USER_ACTION = "io.heckel.ntfy.USER_ACTION_RUN"
private const val TAG = "NtfyNotifService"
private const val TAG = "PonypushNotifService"
private const val DEFAULT_GROUP = "ntfy"
private const val SUBSCRIPTION_GROUP_PREFIX = "ntfy-subscription-"

@ -14,7 +14,7 @@ import io.heckel.ntfy.util.Log
* in a doze state and Internet may not be available. It's also best practice, apparently.
*/
object UserActionManager {
private const val TAG = "NtfyUserActionEx"
private const val TAG = "PonypushUserActionEx"
private const val WORK_NAME_PREFIX = "io.heckel.ntfy.USER_ACTION_"
fun enqueue(context: Context, notificationId: String, actionId: String) {

@ -110,6 +110,6 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) :
const val INPUT_DATA_ACTION_ID = "actionId"
private const val DEFAULT_HTTP_ACTION_METHOD = "POST" // Cannot be changed without changing the contract
private const val TAG = "NtfyUserActWrk"
private const val TAG = "PonypushUserActWrk"
}
}

@ -102,7 +102,7 @@ class JsonConnection(
}
companion object {
private const val TAG = "NtfySubscriberConn"
private const val TAG = "PonypushSubscriberConn"
private const val CONNECTION_LOOP_DELAY_MILLIS = 30_000L
private const val RETRY_STEP_MILLIS = 5_000L
private const val RETRY_MAX_MILLIS = 60_000L

@ -347,9 +347,9 @@ class SubscriberService : Service() {
}
companion object {
const val TAG = "NtfySubscriberService"
const val TAG = "PonypushSubscriberService"
const val SERVICE_START_WORKER_VERSION = BuildConfig.VERSION_CODE
const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic" // Do not change!
const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "PonypushAutoRestartWorkerPeriodic" // Do not change!
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"

@ -63,7 +63,7 @@ class SubscriberServiceManager(private val context: Context) {
}
companion object {
const val TAG = "NtfySubscriberMgr"
const val TAG = "PonypushSubscriberMgr"
const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
fun refresh(context: Context) {

@ -192,7 +192,7 @@ class WsConnection(
}
companion object {
private const val TAG = "NtfyWsConnection"
private const val TAG = "PonypushWsConnection"
private const val RECONNECT_TAG = "WsReconnect"
private const val WS_CLOSE_NORMAL = 1000
private val RETRY_SECONDS = listOf(5, 10, 15, 20, 30, 45, 60, 120)

@ -92,12 +92,8 @@ class AddFragment : DialogFragment() {
subscribeBaseUrlText = view.findViewById(R.id.add_dialog_subscribe_base_url_text)
subscribeBaseUrlText.background = view.background
subscribeBaseUrlText.hint = defaultBaseUrl ?: appBaseUrl
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box)
subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_checkbox)
subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_description)
subscribeUseAnotherServerCheckbox = view.findViewById(R.id.add_dialog_subscribe_use_another_server_checkbox)
subscribeUseAnotherServerDescription = view.findViewById(R.id.add_dialog_subscribe_use_another_server_description)
subscribeForegroundDescription = view.findViewById(R.id.add_dialog_subscribe_foreground_description)
subscribeProgress = view.findViewById(R.id.add_dialog_subscribe_progress)
subscribeErrorText = view.findViewById(R.id.add_dialog_subscribe_error_text)
subscribeErrorText.visibility = View.GONE
@ -111,14 +107,6 @@ class AddFragment : DialogFragment() {
loginErrorText = view.findViewById(R.id.add_dialog_login_error_text)
loginErrorTextImage = view.findViewById(R.id.add_dialog_login_error_text_image)
// Set foreground description text
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
if (!BuildConfig.FIREBASE_AVAILABLE) {
subscribeInstantDeliveryBox.visibility = View.GONE
}
// Add baseUrl auto-complete behavior
lifecycleScope.launch(Dispatchers.IO) {
val baseUrlsRaw = repository.getSubscriptions()
@ -173,12 +161,6 @@ class AddFragment : DialogFragment() {
}
subscribeTopicText.addTextChangedListener(subscribeTextWatcher)
subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher)
subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ ->
validateInputSubscribeView()
}
subscribeUseAnotherServerCheckbox.setOnCheckedChangeListener { _, _ ->
validateInputSubscribeView()
}
validateInputSubscribeView()
// Focus topic text (keyboard is shown too, see above)
@ -294,29 +276,7 @@ class AddFragment : DialogFragment() {
// Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
false
} else if (subscribeUseAnotherServerCheckbox.isChecked && subscribeBaseUrlText.text.toString() == appBaseUrl) {
true
} else if (!subscribeUseAnotherServerCheckbox.isChecked && defaultBaseUrl == null) {
true
} else {
false
}
if (subscribeUseAnotherServerCheckbox.isChecked) {
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
subscribeBaseUrlLayout.visibility = View.VISIBLE
} else {
subscribeUseAnotherServerDescription.visibility = View.GONE
subscribeBaseUrlLayout.visibility = View.GONE
}
if (instantToggleAllowed) {
subscribeInstantDeliveryBox.visibility = View.VISIBLE
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
subscribeForegroundDescription.visibility = View.GONE
} else {
subscribeInstantDeliveryBox.visibility = View.GONE
subscribeInstantDeliveryDescription.visibility = View.GONE
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
}
} else defaultBaseUrl == null
// Enable/disable "Subscribe" button
lifecycleScope.launch(Dispatchers.IO) {
@ -328,8 +288,6 @@ class AddFragment : DialogFragment() {
it.runOnUiThread {
if (subscription != null || DISALLOWED_TOPICS.contains(topic)) {
positiveButton.isEnabled = false
} else if (subscribeUseAnotherServerCheckbox.isChecked) {
positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl)
} else {
positiveButton.isEnabled = validTopic(topic)
}
@ -356,18 +314,14 @@ class AddFragment : DialogFragment() {
activity.runOnUiThread {
val topic = subscribeTopicText.text.toString()
val baseUrl = getBaseUrl()
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
val instant = !BuildConfig.FIREBASE_AVAILABLE
subscribeListener.onSubscribe(topic, baseUrl, instant)
dialog?.dismiss()
}
}
private fun getBaseUrl(): String {
return if (subscribeUseAnotherServerCheckbox.isChecked) {
subscribeBaseUrlText.text.toString()
} else {
return defaultBaseUrl ?: appBaseUrl
}
return defaultBaseUrl ?: appBaseUrl
}
private fun showSubscribeView() {
@ -398,8 +352,6 @@ class AddFragment : DialogFragment() {
private fun enableSubscribeView(enable: Boolean) {
subscribeTopicText.isEnabled = enable
subscribeBaseUrlText.isEnabled = enable
subscribeInstantDeliveryCheckbox.isEnabled = enable
subscribeUseAnotherServerCheckbox.isEnabled = enable
positiveButton.isEnabled = enable
}
@ -432,7 +384,7 @@ class AddFragment : DialogFragment() {
}
companion object {
const val TAG = "NtfyAddFragment"
const val TAG = "PonypushAddFragment"
private val DISALLOWED_TOPICS = listOf("docs", "static", "file") // If updated, also update in server
}
}

@ -176,17 +176,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
title = subscriptionDisplayName
// Set "how to instructions"
val howToExample: TextView = findViewById(R.id.detail_how_to_example)
howToExample.linksClickable = true
val howToText = getString(R.string.detail_how_to_example, topicUrl)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
} else {
howToExample.text = Html.fromHtml(howToText)
}
// Swipe to refresh
mainListContainer = findViewById(R.id.detail_notification_list_container)
mainListContainer.setOnRefreshListener { refresh() }
@ -345,10 +334,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.detail_menu_test -> {
onTestClick()
true
}
R.id.detail_menu_notifications_enabled -> {
onMutedUntilClick(enable = false)
true
@ -766,7 +751,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
companion object {
const val TAG = "NtfyDetailActivity"
const val TAG = "PonypushDetailActivity"
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
const val EXTRA_SUBSCRIPTION_BASE_URL = "baseUrl"
const val EXTRA_SUBSCRIPTION_TOPIC = "topic"

@ -544,7 +544,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
}
companion object {
const val TAG = "NtfyDetailAdapter"
const val TAG = "PonypushDetailAdapter"
const val REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD = 9876
const val IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024 // Too large images crash the app with "Canvas: trying to draw too large(233280000bytes) bitmap."
}

@ -510,7 +510,7 @@ class DetailSettingsActivity : AppCompatActivity() {
}
companion object {
private const val TAG = "NtfyDetailSettingsActiv"
private const val TAG = "PonypushDetailSettingsActiv"
private const val SUBSCRIPTION_ICONS = "subscriptionIcons"
private const val SUBSCRIPTION_ICON_MAX_SIZE_BYTES = 4194304
private const val SUBSCRIPTION_ICON_MAX_WIDTH = 2048

@ -175,27 +175,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}
}
// WebSocket banner
val wsBanner = findViewById<View>(R.id.main_banner_websocket) // Banner visibility is toggled in onResume()
val wsText = findViewById<TextView>(R.id.main_banner_websocket_text)
val wsDismissButton = findViewById<Button>(R.id.main_banner_websocket_dontaskagain)
val wsRemindButton = findViewById<Button>(R.id.main_banner_websocket_remind_later)
val wsEnableButton = findViewById<Button>(R.id.main_banner_websocket_enable)
wsText.movementMethod = LinkMovementMethod.getInstance() // Make links clickable
wsDismissButton.setOnClickListener {
wsBanner.visibility = View.GONE
repository.setWebSocketRemindTime(Repository.WEBSOCKET_REMIND_TIME_NEVER)
}
wsRemindButton.setOnClickListener {
wsBanner.visibility = View.GONE
repository.setWebSocketRemindTime(System.currentTimeMillis() + ONE_DAY_MILLIS)
}
wsEnableButton.setOnClickListener {
repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS)
SubscriberServiceManager(this).restart()
wsBanner.visibility = View.GONE
}
// Create notification channels right away, so we can configure them immediately after installing the app
dispatcher?.init()
@ -349,10 +328,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}
val mutedUntilSeconds = repository.getGlobalMutedUntil()
runOnUiThread {
// Show/hide in-app rate widget
val rateAppItem = menu.findItem(R.id.main_menu_rate)
rateAppItem.isVisible = BuildConfig.RATE_APP_AVAILABLE
// Pause notification icons
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
@ -385,26 +360,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
startActivity(Intent(this, SettingsActivity::class.java))
true
}
R.id.main_menu_report_bug -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_report_bug_url))))
true
}
R.id.main_menu_rate -> {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")))
} catch (e: ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")))
}
true
}
R.id.main_menu_donate -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url))))
true
}
R.id.main_menu_docs -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_docs_url))))
true
}
else -> super.onOptionsItemSelected(item)
}
}
@ -690,7 +645,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}
companion object {
const val TAG = "NtfyMainActivity"
const val TAG = "PonypushMainActivity"
const val EXTRA_SUBSCRIPTION_ID = "subscriptionId"
const val EXTRA_SUBSCRIPTION_BASE_URL = "subscriptionBaseUrl"
const val EXTRA_SUBSCRIPTION_TOPIC = "subscriptionTopic"

@ -131,6 +131,6 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
}
companion object {
const val TAG = "NtfyMainAdapter"
const val TAG = "PonypushMainAdapter"
}
}

@ -92,6 +92,6 @@ class NotificationFragment : DialogFragment() {
}
companion object {
const val TAG = "NtfyNotificationFragment"
const val TAG = "PonypushNotificationFragment"
}
}

@ -373,8 +373,8 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
when (v) {
EXPORT_LOGS_COPY_ORIGINAL -> copyLogsToClipboard(scrub = false)
EXPORT_LOGS_COPY_SCRUBBED -> copyLogsToClipboard(scrub = true)
EXPORT_LOGS_UPLOAD_ORIGINAL -> uploadLogsToNopaste(scrub = false)
EXPORT_LOGS_UPLOAD_SCRUBBED -> uploadLogsToNopaste(scrub = true)
EXPORT_LOGS_UPLOAD_ORIGINAL -> copyLogsToClipboard(scrub = false)
EXPORT_LOGS_UPLOAD_SCRUBBED -> copyLogsToClipboard(scrub = true)
}
false
}
@ -511,7 +511,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
connectionProtocol?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val proto = value ?: repository.getConnectionProtocol()
repository.setConnectionProtocol(proto)
repository.setConnectionProtocol()
restartService()
}
override fun getString(key: String?, defValue: String?): String {
@ -528,12 +528,19 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Version
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
val versionPref: Preference? = findPreference(versionPrefId)
val version = getString(R.string.settings_about_version_format, BuildConfig.VERSION_NAME, BuildConfig.FLAVOR)
val type = if (BuildConfig.FIREBASE_AVAILABLE) {
"Firebase Cloud Messaging"
} else {
"WebSocket"
}
val version = "Ponypush " + BuildConfig.VERSION_NAME + " (ntfy " + BuildConfig.NTFY_VERSION + ", " + type + ")"
versionPref?.summary = version
versionPref?.onPreferenceClickListener = OnPreferenceClickListener {
val context = context ?: return@OnPreferenceClickListener false
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("ntfy version", version)
val clip = ClipData.newPlainText("Ponypush version", version)
clipboard.setPrimaryClip(clip)
Toast
.makeText(context, getString(R.string.settings_about_version_copied_to_clipboard_message), Toast.LENGTH_LONG)
@ -790,7 +797,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
companion object {
private const val TAG = "NtfySettingsActivity"
private const val TAG = "PonypushSettingsActivity"
private const val TITLE_TAG = "title"
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L

@ -355,6 +355,6 @@ class ShareActivity : AppCompatActivity() {
}
companion object {
const val TAG = "NtfyShareActivity"
const val TAG = "PonypushShareActivity"
}
}

@ -161,7 +161,7 @@ class UserFragment : DialogFragment() {
}
companion object {
const val TAG = "NtfyUserFragment"
const val TAG = "PonypushUserFragment"
private const val BUNDLE_BASE_URL = "baseUrl"
private const val BUNDLE_USERNAME = "username"
private const val BUNDLE_PASSWORD = "password"

@ -133,7 +133,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
}
companion object {
private const val TAG = "NtfyUpBroadcastRecv"
private const val TAG = "PonypushUpBroadcastRecv"
private const val UP_PREFIX = "up"
private const val TOPIC_RANDOM_ID_LENGTH = 12

@ -52,6 +52,6 @@ class Distributor(val context: Context) {
}
companion object {
private const val TAG = "NtfyUpDistributor"
private const val TAG = "PonypushUpDistributor"
}
}

@ -47,13 +47,18 @@ class Log(private val logsDao: LogDao) {
}
private fun prependDeviceInfo(logs: String, settings: String, scrubLine: Boolean): String {
val maybeScrubLine = if (scrubLine) "Server URLs (aside from ntfy.sh) and topics have been replaced with fruits ๐ŸŒ๐Ÿฅ๐Ÿ‹๐Ÿฅฅ๐Ÿฅ‘๐ŸŠ๐ŸŽ๐Ÿ‘.\n" else ""
val maybeScrubLine = if (scrubLine) "Server URLs (aside from notifications.equestria.dev) and topics have been replaced with fruits ๐ŸŒ๐Ÿฅ๐Ÿ‹๐Ÿฅฅ๐Ÿฅ‘๐ŸŠ๐ŸŽ๐Ÿ‘.\n" else ""
val type = if (BuildConfig.FIREBASE_AVAILABLE) {
"Firebase Cloud Messaging"
} else {
"WebSocket"
}
return """
This is a log of the ntfy Android app. The log shows up to 1,000 entries.
This is a log of the Ponypush Android app. The log shows up to 1,000 entries.
$maybeScrubLine
Device info:
--
ntfy: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})
Ponypush: ${BuildConfig.VERSION_NAME + " (ntfy " + BuildConfig.NTFY_VERSION + ", " + type + ")"}
OS: ${System.getProperty("os.version")}
Android: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Model: ${Build.DEVICE}
@ -134,10 +139,10 @@ class Log(private val logsDao: LogDao) {
)
companion object {
private const val TAG = "NtfyLog"
private const val TAG = "PonypushLog"
private const val PRUNE_EVERY = 100
private const val ENTRIES_MAX = 1000
private val IGNORE_TERMS = listOf("ntfy.sh")
private val IGNORE_TERMS = listOf("notifications.equestria.dev")
private val REPLACE_TERMS = listOf(
"banana", "kiwi", "lemon", "coconut", "avocado", "orange", "apple", "peach",
"pineapple", "dragonfruit", "durian", "starfruit"

@ -65,7 +65,7 @@ fun subscriptionTopicShortUrl(subscription: Subscription) : String {
}
fun displayName(subscription: Subscription) : String {
return subscription.displayName ?: subscriptionTopicShortUrl(subscription)
return subscription.displayName ?: subscription.topic
}
fun shortUrl(url: String) = url

@ -133,8 +133,8 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
companion object {
const val VERSION = BuildConfig.VERSION_CODE
const val TAG = "NtfyDeleteWorker"
const val WORK_NAME_PERIODIC_ALL = "NtfyDeleteWorkerPeriodic" // Do not change
const val TAG = "PonypushDeleteWorker"
const val WORK_NAME_PERIODIC_ALL = "PonypushDeleteWorkerPeriodic" // Do not change
private const val ONE_DAY_SECONDS = 24 * 60 * 60L
const val HARD_DELETE_AFTER_SECONDS = 4 * 30 * ONE_DAY_SECONDS // 4 months

@ -66,9 +66,9 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
companion object {
const val VERSION = BuildConfig.VERSION_CODE
const val TAG = "NtfyPollWorker"
const val WORK_NAME_PERIODIC_ALL = "NtfyPollWorkerPeriodic" // Do not change
const val WORK_NAME_ONCE_SINGE_PREFIX = "NtfyPollWorkerSingle" // e.g. NtfyPollWorkerSingle_https://ntfy.sh_mytopic
const val TAG = "PonypushPollWorker"
const val WORK_NAME_PERIODIC_ALL = "PonypushPollWorkerPeriodic" // Do not change
const val WORK_NAME_ONCE_SINGE_PREFIX = "PonypushPollWorkerSingle" // e.g. NtfyPollWorkerSingle_https://ntfy.sh_mytopic
const val INPUT_DATA_BASE_URL = "baseUrl"
const val INPUT_DATA_TOPIC = "topic"
}

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M11,17c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1zM11,3v4h2L13,5.08c3.39,0.49 6,3.39 6,6.92 0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-1.68 0.59,-3.22 1.58,-4.42L12,13l1.41,-1.41 -6.8,-6.8v0.02C4.42,6.45 3,9.05 3,12c0,4.97 4.02,9 9,9 4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9h-1zM18,12c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1 0.45,1 1,1 1,-0.45 1,-1zM6,12c0,0.55 0.45,1 1,1s1,-0.45 1,-1 -0.45,-1 -1,-1 -1,0.45 -1,1z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M21,10.78V8c0,-1.65 -1.35,-3 -3,-3h-4c-0.77,0 -1.47,0.3 -2,0.78 -0.53,-0.48 -1.23,-0.78 -2,-0.78H6C4.35,5 3,6.35 3,8v2.78c-0.61,0.55 -1,1.34 -1,2.22v6h2v-2h16v2h2v-6c0,-0.88 -0.39,-1.67 -1,-2.22zM14,7h4c0.55,0 1,0.45 1,1v2h-6V8c0,-0.55 0.45,-1 1,-1zM5,8c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1v2H5V8z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12,7.77L18.39,18H5.61L12,7.77M12,4L2,20h20L12,4z"/>
</vector>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="#338574" />
<solid android:color="#8E7799" />
</shape>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM6.5,17.5l7.51,-3.49L17.5,6.5 9.99,9.99 6.5,17.5zM12,10.9c0.61,0 1.1,0.49 1.1,1.1s-0.49,1.1 -1.1,1.1 -1.1,-0.49 -1.1,-1.1 0.49,-1.1 1.1,-1.1z"/>
</vector>

@ -1,31 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
android:strokeWidth="0.754022"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<vector android:autoMirrored="true" android:height="50dp"
android:viewportHeight="141.7" android:viewportWidth="141.7"
android:width="50dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13.2,46.1c0,0 58.1,-28.7 111.3,-20.8c0,0 32.2,49 -18.6,91.9c0,0 -37.6,-14.6 -92.7,3.8"
android:strokeColor="#FFFFFF" android:strokeWidth="12"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16zM12,4.53L17.74,9 12,13.47 6.26,9 12,4.53z"/>
</vector>

@ -1,31 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
android:strokeWidth="0.754022"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<vector android:autoMirrored="true" android:height="50dp"
android:viewportHeight="141.7" android:viewportWidth="141.7"
android:width="50dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13.2,46.1c0,0 58.1,-28.7 111.3,-20.8c0,0 32.2,49 -18.6,91.9c0,0 -37.6,-14.6 -92.7,3.8"
android:strokeColor="#FFFFFF" android:strokeWidth="12"/>
</vector>

@ -1,26 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:pathData="M7.8398,6.35C4.2598,6.35 1.1932,9.1672 1.1932,12.7486L1.1932,12.7512L1.2283,40.6182L0.3292,47.2529L12.5553,44.0051L35.9384,44.0051L36.5848,39.4849L11.8923,39.4849L5.6808,41.3618L5.7444,40.9954L5.7097,12.7486C5.7097,11.7821 6.5805,10.866 7.8398,10.866L42.9488,10.866L42.9519,10.866C44.2097,10.8673 45.0794,11.7828 45.0794,12.7486L45.0794,25.7395L49.5954,25.7395L49.5954,12.7481C49.5954,9.1677 46.5305,6.3517 42.9519,6.35L42.9488,6.35L7.8398,6.35zM49.5954,31.4735C45.6685,38.326 43.7215,41.7259 42.4176,44.0051L42.9488,44.0051C46.5288,44.0051 49.5954,41.1842 49.5954,37.6029L49.5954,31.4735z"
android:strokeWidth="2.84985"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
android:strokeWidth="0.525121"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<path
android:pathData="m41.6796,15.3498c-4.1394,7.2567 -7.8235,13.7146 -10.2159,17.9374h8.5829l-1.8531,12.962c1.2024,-2.1043 3.8969,-6.8095 10.2878,-17.9601h-8.6543z"
android:strokeWidth="1"
android:fillColor="#FFFFFFFF"
android:strokeColor="#00000000"/>
<vector android:autoMirrored="true" android:height="50dp"
android:viewportHeight="141.7" android:viewportWidth="141.7"
android:width="50dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13.2,46.1c0,0 58.1,-28.7 111.3,-20.8c0,0 32.2,49 -18.6,91.9c0,0 -37.6,-14.6 -92.7,3.8"
android:strokeColor="#FFFFFF" android:strokeWidth="12"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M11,5v5.59L7.5,10.59l4.5,4.5 4.5,-4.5L13,10.59L13,5h-2zM6,14c0,3.31 2.69,6 6,6s6,-2.69 6,-6h-2c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4L6,14z"/>
</vector>

@ -1,26 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m12.195,20.8283a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#999999"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,15.694a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372A1.2746,1.2746 0,0 0,19.9307 9.7205,1.2746 1.2746,0 0,0 18.1798,9.2928L12.195,12.9281 6.2102,9.2928a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#b3b3b3"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.1168,10.4268a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277l-5.9848,3.6353 -5.9848,-3.6353a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511L11.455,10.2416a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#cccccc"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#1F8AFF" android:pathData="M11,7h2v2h-2V7zM11,11h2v6h-2V11zM12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM12,20c-4.4,0 -8,-3.6 -8,-8s3.6,-8 8,-8s8,3.6 8,8S16.4,20 12,20z"/>
</vector>

@ -1,19 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m12.1727,17.7744a1.2747,1.2747 0,0 0,0.6616 -0.1852l6.6466,-4.0372a1.2746,1.2746 0,0 0,0.4275 -1.7511,1.2746 1.2746,0 0,0 -1.7509,-0.4277L12.1727,15.0085 6.1879,11.3731a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#999999"
android:strokeLineCap="round"/>
<path
android:pathData="m12.1727,12.64a1.2747,1.2747 0,0 0,0.6616 -0.1852L19.4809,8.4177A1.2746,1.2746 0,0 0,19.9084 6.6666,1.2746 1.2746,0 0,0 18.1575,6.2388L12.1727,9.8742 6.1879,6.2388a1.2746,1.2746 0,0 0,-1.7509 0.4277,1.2746 1.2746,0 0,0 0.4275,1.7511l6.6464,4.0372a1.2747,1.2747 0,0 0,0.6618 0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#b3b3b3"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#19CF46" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4C10,21.1 10.9,22 12,22zM18,16v-5c0,-3.1 -1.6,-5.6 -4.5,-6.3V4c0,-0.8 -0.7,-1.5 -1.5,-1.5S10.5,3.2 10.5,4v0.7C7.6,5.4 6,7.9 6,11v5l-2,2v1h16v-1L18,16z"/>
</vector>

@ -1,20 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.1168,6.5394A1.2747,1.2747 0,0 0,11.4552 6.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 12.5129,1.2746 1.2746,0 0,0 19.425,10.7618L12.7786,6.7246A1.2747,1.2747 0,0 0,12.1168 6.5394Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#c60000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,11.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#de0000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFDA1F" android:pathData="M1,21h22L12,2L1,21zM13,18h-2v-2h2V18zM13,14h-2v-4h2V14z"/>
</vector>

@ -1,26 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.1168,3.4051A1.2747,1.2747 0,0 0,11.4552 3.5903L4.8086,7.6275A1.2746,1.2746 0,0 0,4.381 9.3786,1.2746 1.2746,0 0,0 6.132,9.8063L12.1168,6.171 18.1016,9.8063A1.2746,1.2746 0,0 0,19.8525 9.3786,1.2746 1.2746,0 0,0 19.425,7.6275L12.7786,3.5903A1.2747,1.2747 0,0 0,12.1168 3.4051Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#aa0000"
android:strokeLineCap="round"/>
<path
android:pathData="M12.1168,8.5394A1.2747,1.2747 0,0 0,11.4552 8.7246l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353A1.2746,1.2746 0,0 0,19.8525 14.5129,1.2746 1.2746,0 0,0 19.425,12.7618L12.7786,8.7246A1.2747,1.2747 0,0 0,12.1168 8.5394Z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#c60000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.195,13.8067a1.2747,1.2747 0,0 0,-0.6616 0.1852l-6.6466,4.0372a1.2746,1.2746 0,0 0,-0.4275 1.7511,1.2746 1.2746,0 0,0 1.7509,0.4277l5.9848,-3.6353 5.9848,3.6353a1.2746,1.2746 0,0 0,1.7509 -0.4277,1.2746 1.2746,0 0,0 -0.4275,-1.7511l-6.6464,-4.0372a1.2747,1.2747 0,0 0,-0.6618 -0.1852z"
android:strokeLineJoin="round"
android:strokeWidth="0.0919748"
android:fillColor="#de0000"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF1F1F" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM13,17h-2v-2h2V17zM13,13h-2V7h2V13z"/>
</vector>

@ -1,18 +1,7 @@
<vector android:height="24dp" android:viewportHeight="50"
android:viewportWidth="50" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#888888"
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
android:strokeColor="#00000000" android:strokeWidth="0.754022"/>
<path android:fillColor="#888888"
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="141.7" android:viewportWidth="141.7"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13.2,46.1c0,0 58.1,-28.7 111.3,-20.8c0,0 32.2,49 -18.6,91.9c0,0 -37.6,-14.6 -92.7,3.8"
android:strokeColor="#777777" android:strokeWidth="12"/>
</vector>

@ -1,18 +1,7 @@
<vector android:height="48dp" android:viewportHeight="50"
android:viewportWidth="50" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#888888"
android:pathData="m7.8399,6.35c-3.58,0 -6.6469,2.817 -6.6469,6.3983v0.003l0.0351,27.8668 -0.8991,6.6347 12.2261,-3.248L42.9487,44.0049c3.58,0 6.6469,-2.8208 6.6469,-6.4022v-24.8545c0,-3.5803 -3.0652,-6.3967 -6.6438,-6.3983h-0.0031zM7.8399,10.8662h35.1088,0.0031c1.2579,0.0013 2.1277,0.9164 2.1277,1.8821v24.8544c0,0.9666 -0.8714,1.8821 -2.1307,1.8821L11.8924,39.4849l-6.2114,1.8768 0.0633,-0.366 -0.0343,-28.2473c0,-0.9665 0.8706,-1.8821 2.13,-1.8821z"
android:strokeColor="#00000000" android:strokeWidth="0.754022"/>
<path android:fillColor="#888888"
android:pathData="m11.5278,32.0849l0,-3.346l7.0363,-3.721q0.3397,-0.1732 0.6551,-0.2596 0.3397,-0.1153 0.6066,-0.1732 0.2912,-0.0288 0.5823,-0.0576l0,-0.2308q-0.2912,-0.0288 -0.5823,-0.1153 -0.2669,-0.0576 -0.6066,-0.1443 -0.3154,-0.1153 -0.6551,-0.2884l-7.0363,-3.721l0,-3.3749l10.8699,5.9132l0,3.6056z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m10.9661,15.6112l0,4.8516l7.3742,3.9002c0.0157,0.0077 0.0305,0.0128 0.0461,0.0204 -0.0157,0.0077 -0.0305,0.0128 -0.0461,0.0204l-7.3742,3.9002l0,4.8267l0.7961,-0.4333 11.1995,-6.0969l0,-4.463zM12.0931,17.6933 L21.8346,22.9981l0,2.7446l-9.7414,5.2999l0,-1.8679l6.6912,-3.5416 0.0084,-0.0051c0.1961,-0.0992 0.3826,-0.1724 0.5531,-0.2191l0.0127,0l0.0167,-0.0051c0.2034,-0.0691 0.3777,-0.1209 0.5279,-0.1545l1.0684,-0.1046l0,-1.4644l-0.5154,-0.0497c-0.1632,-0.0153 -0.3288,-0.0505 -0.4944,-0.0997l-0.0167,-0.0051 -0.0167,-0.0051c-0.1632,-0.0352 -0.3552,-0.0811 -0.5656,-0.1344 -0.1802,-0.0668 -0.3706,-0.1479 -0.5698,-0.2492l-0.0084,-0.0051 -6.6912,-3.5416z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m26.7503,30.9206l11.6118,0l0,3.1388L26.7503,34.0594Z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<path android:fillColor="#888888"
android:pathData="m26.1875,30.2775l0,0.6427 0,3.7845l12.7371,0l0,-4.4272zM27.3113,31.563l10.4896,0l0,1.8515l-10.4896,0z"
android:strokeColor="#00000000" android:strokeWidth="0.525121"/>
<vector android:autoMirrored="true" android:height="48dp"
android:viewportHeight="141.7" android:viewportWidth="141.7"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13.2,46.1c0,0 58.1,-28.7 111.3,-20.8c0,0 32.2,49 -18.6,91.9c0,0 -37.6,-14.6 -92.7,3.8"
android:strokeColor="#777777" android:strokeWidth="12"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M15,4c-4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8 -3.58,-8 -8,-8zM15,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zM3,12c0,-2.61 1.67,-4.83 4,-5.65L7,4.26C3.55,5.15 1,8.27 1,12s2.55,6.85 6,7.74v-2.09c-2.33,-0.82 -4,-3.04 -4,-5.65z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M12,2L1,21h22L12,2zM13,16h-2v2h2v-2zM13,10h-2v4h2v-4z"/>
</vector>

@ -45,32 +45,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:padding="10dp" android:gravity="center_horizontal"
android:paddingStart="50dp" android:paddingEnd="50dp"/>
<TextView
android:text="@string/detail_how_to_intro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/detail_how_to_intro"
android:layout_marginTop="20dp"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"/>
<TextView
android:text="@string/detail_how_to_example"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/detail_how_to_example"
android:layout_marginTop="7dp"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"/>
<TextView
android:text="@string/detail_how_to_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/detail_how_to_link"
android:layout_marginTop="7dp"
android:layout_marginStart="50dp"
android:layout_marginEnd="50dp"
android:linksClickable="true"
android:autoLink="web"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -86,67 +86,7 @@
android:layout_height="wrap_content"
app:shapeAppearance="?shapeAppearanceLargeComponent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/main_banner_battery"
android:id="@+id/main_banner_websocket" android:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/main_banner_websocket_constraint" android:elevation="5dp">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp" app:srcCompat="@drawable/ic_announcement_orange_24dp"
android:id="@+id/main_banner_websocket_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/main_banner_websocket_text"
app:layout_constraintEnd_toStartOf="@id/main_banner_websocket_text"
app:layout_constraintBottom_toBottomOf="@+id/main_banner_websocket_text"