diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 212fe0ec..c73509ca 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -67,6 +67,7 @@ import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment +import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Utils @@ -118,6 +119,7 @@ class MainActivity : AppCompatActivity() { const val TARGET_HELP = 502 const val TARGET_FEEDBACK = 120 const val TARGET_MESSAGES_DETAILS = 503 + const val TARGET_WEB_PUSH = 140 const val HOME_ID = DRAWER_ITEM_HOME @@ -209,6 +211,7 @@ class MainActivity : AppCompatActivity() { list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class) list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class) list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class) + list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) list diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index af126adf..ccaea91d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull import pl.szczodrzynski.edziennik.utils.models.Date @@ -121,4 +123,34 @@ class SzkolnyApi(val app: App) { eventId = event.id )).execute() } + + fun pairBrowser(browserId: String?, pairToken: String?): List { + val response = api.webPush(WebPushRequest( + action = "pairBrowser", + deviceId = app.deviceId, + browserId = browserId, + pairToken = pairToken + )).execute().body() + + return response?.data?.browsers ?: emptyList() + } + + fun listBrowsers(): List { + val response = api.webPush(WebPushRequest( + action = "listBrowsers", + deviceId = app.deviceId + )).execute().body() + + return response?.data?.browsers ?: emptyList() + } + + fun unpairBrowser(browserId: String): List { + val response = api.webPush(WebPushRequest( + action = "unpairBrowser", + deviceId = app.deviceId, + browserId = browserId + )).execute().body() + + return response?.data?.browsers ?: emptyList() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt index 73918e67..b57dd99b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt @@ -6,8 +6,10 @@ package pl.szczodrzynski.edziennik.data.api.szkolny import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ServerSyncResponse +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST @@ -19,4 +21,7 @@ interface SzkolnyService { @POST("share") fun shareEvent(@Body request: EventShareRequest): Call> + + @POST("webPush") + fun webPush(@Body request: WebPushRequest): Call> } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/WebPushRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/WebPushRequest.kt new file mode 100644 index 00000000..209a63a8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/WebPushRequest.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.request + +data class WebPushRequest( + + val action: String, + val deviceId: String, + + val browserId: String? = null, + val pairToken: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/WebPushResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/WebPushResponse.kt new file mode 100644 index 00000000..d51f3be2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/WebPushResponse.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.response + +data class WebPushResponse(val browsers: List) { + data class Browser( + val id: Int, + val browserId: String, + val userAgent: String, + val dateRegistered: String + ) +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt new file mode 100644 index 00000000..8366862e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/TemplateAdapter.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding +import pl.szczodrzynski.edziennik.onClick + +class TemplateAdapter( + val context: Context, + val onItemClick: ((item: TemplateItem) -> Unit)? = null, + val onItemButtonClick: ((item: TemplateItem) -> Unit)? = null +) : RecyclerView.Adapter() { + + private val app by lazy { context.applicationContext as App } + + var items = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = TemplateListItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + onItemClick?.let { listener -> + b.root.onClick { listener(item) } + } + + /*b.someButton.visibility = if (buttonVisible) View.VISIBLE else View.GONE + onItemButtonClick?.let { listener -> + b.someButton.onClick { listener(item) } + }*/ + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root) + + data class TemplateItem(val text: String) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/QrScannerDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/QrScannerDialog.kt new file mode 100644 index 00000000..d43d9374 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/QrScannerDialog.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import me.dm7.barcodescanner.zxing.ZXingScannerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.dp +import kotlin.coroutines.CoroutineContext + +class QrScannerDialog( + val activity: AppCompatActivity, + val onCodeScanned: (text: String) -> Unit, + val onShowListener: ((tag: String) -> Unit)? = null, + val onDismissListener: ((tag: String) -> Unit)? = null +) : CoroutineScope { + companion object { + private const val TAG = "QrScannerDialog" + } + + private lateinit var app: App + private lateinit var scannerView: ZXingScannerView + private lateinit var dialog: AlertDialog + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + init { run { + if (activity.isFinishing) + return@run + onShowListener?.invoke(TAG) + app = activity.applicationContext as App + scannerView = ZXingScannerView(activity) + scannerView.setPadding(0, 16.dp, 0, 0) + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.qr_scanner_dialog_title) + .setView(scannerView) + .setPositiveButton(R.string.close) { dialog, _ -> + dialog.dismiss() + } + .setOnDismissListener { + onDismissListener?.invoke(TAG) + } + .show() + + scannerView.setResultHandler { + scannerView.stopCamera() + dialog.dismiss() + onCodeScanned(it.text) + } + scannerView.startCamera() + }} +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt index 6b055750..674f48dd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/TemplateFragment.kt @@ -5,25 +5,34 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.navigation.NavController -import androidx.navigation.Navigation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding import pl.szczodrzynski.edziennik.utils.Themes +import kotlin.coroutines.CoroutineContext -class TemplateFragment : Fragment() { +class TemplateFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TemplateFragment" + } private lateinit var app: App private lateinit var activity: MainActivity private lateinit var b: FragmentTemplateBinding - private val navController: NavController by lazy { Navigation.findNavController(b.root) } + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null - if (context == null) - return null + context ?: return null app = activity.application as App context!!.theme.applyStyle(Themes.appTheme, true) if (app.profile == null) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt index 49c213d0..74d01bd1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsFragment.kt @@ -34,8 +34,7 @@ class NotificationsFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null - if (context == null) - return null + context ?: return null app = activity.application as App context!!.theme.applyStyle(Themes.appTheme, true) if (app.profile == null) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java index 9ee687a8..9210259b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsNewFragment.java @@ -53,7 +53,6 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker; import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog; import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment; -import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushConfigActivity; import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Utils; import pl.szczodrzynski.edziennik.utils.models.Date; @@ -718,8 +717,7 @@ public class SettingsNewFragment extends MaterialAboutFragment { .color(IconicsColor.colorInt(iconColor)) ) .setOnClickAction(() -> { - Intent i = new Intent(activity, WebPushConfigActivity.class); - startActivity(i); + activity.loadTarget(MainActivity.TARGET_WEB_PUSH, null); }) ); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt index 9bd6ed1a..82018d30 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt @@ -53,8 +53,7 @@ class TimetableFragment : Fragment(), CoroutineScope { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null - if (context == null) - return null + context ?: return null app = activity.application as App job = Job() context!!.theme.applyStyle(Themes.appTheme, true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt new file mode 100644 index 00000000..72d9a963 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.ui.modules.webpush + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse +import pl.szczodrzynski.edziennik.databinding.WebPushBrowserItemBinding +import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.setText + +class WebPushBrowserAdapter( + val context: Context, + val onItemClick: ((browser: WebPushResponse.Browser) -> Unit)? = null, + val onUnpairButtonClick: ((browser: WebPushResponse.Browser) -> Unit)? = null +) : RecyclerView.Adapter() { + + private val app by lazy { context.applicationContext as App } + + var items = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = WebPushBrowserItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val browser = items[position] + val b = holder.b + + onItemClick?.let { listener -> + b.root.onClick { listener(browser) } + } + + b.browserName.text = browser.userAgent + b.datePaired.setText(R.string.web_push_date_paired_format, browser.dateRegistered) + + onUnpairButtonClick?.let { listener -> + b.unpair.onClick { listener(browser) } + } + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: WebPushBrowserItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt new file mode 100644 index 00000000..ad47ca7d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-19. + */ + +package pl.szczodrzynski.edziennik.ui.modules.webpush + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse +import pl.szczodrzynski.edziennik.databinding.WebPushFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.Themes +import kotlin.coroutines.CoroutineContext + +class WebPushFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TemplateFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: WebPushFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private lateinit var adapter: WebPushBrowserAdapter + private val api by lazy { + SzkolnyApi(app) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + context!!.theme.applyStyle(Themes.appTheme, true) + if (app.profile == null) + return inflater.inflate(R.layout.fragment_loading, container, false) + // activity, context and profile is valid + b = WebPushFragmentBinding.inflate(inflater) + return b.root + } + + @SuppressLint("DefaultLocale") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // TODO check if app, activity, b can be null + if (app.profile == null || !isAdded) + return + + b.scanQrCode.onClick { + val result = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) + if (result == PackageManager.PERMISSION_GRANTED) { + QrScannerDialog(activity, { + b.tokenEditText.setText(it.crc32().toString(36).toUpperCase()) + pairBrowser(browserId = it) + }) + } else { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1) + } + } + + b.tokenAccept.onClick { + val pairToken = b.tokenEditText.text.toString().toUpperCase() + if (!"[0-9A-Z]{3,13}".toRegex().matches(pairToken)) { + b.tokenLayout.error = app.getString(R.string.web_push_token_invalid) + return@onClick + } + b.tokenLayout.error = null + b.tokenEditText.setText(pairToken) + pairBrowser(pairToken = pairToken) + } + + adapter = WebPushBrowserAdapter( + activity, + onItemClick = null, + onUnpairButtonClick = { + unpairBrowser(it.browserId) + } + ) + + launch { + val browsers = withContext(Dispatchers.Default) { + api.listBrowsers() + } + updateBrowserList(browsers) + } + } + + private fun updateBrowserList(browsers: List) { + adapter.items = browsers + if (b.browsersView.adapter == null) { + b.browsersView.adapter = adapter + b.browsersView.apply { + isNestedScrollingEnabled = false + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + if (browsers.isNotEmpty()) { + b.browsersView.visibility = View.VISIBLE + b.browsersNoData.visibility = View.GONE + } else { + b.browsersView.visibility = View.GONE + b.browsersNoData.visibility = View.VISIBLE + } + } + + private fun pairBrowser(browserId: String? = null, pairToken: String? = null) { + b.scanQrCode.isEnabled = false + b.tokenAccept.isEnabled = false + b.tokenEditText.isEnabled = false + b.tokenEditText.clearFocus() + launch { + val browsers = withContext(Dispatchers.Default) { + api.pairBrowser(browserId, pairToken) + } + b.scanQrCode.isEnabled = true + b.tokenAccept.isEnabled = true + b.tokenEditText.isEnabled = true + updateBrowserList(browsers) + } + } + + private fun unpairBrowser(browserId: String) { + launch { + val browsers = withContext(Dispatchers.Default) { + api.unpairBrowser(browserId) + } + updateBrowserList(browsers) + } + } +} diff --git a/app/src/main/res/drawable/ic_web_push_no_browsers.xml b/app/src/main/res/drawable/ic_web_push_no_browsers.xml new file mode 100644 index 00000000..e2d53846 --- /dev/null +++ b/app/src/main/res/drawable/ic_web_push_no_browsers.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/template_list_item.xml b/app/src/main/res/layout/template_list_item.xml new file mode 100644 index 00000000..7d566475 --- /dev/null +++ b/app/src/main/res/layout/template_list_item.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/web_push_browser_item.xml b/app/src/main/res/layout/web_push_browser_item.xml new file mode 100644 index 00000000..02bea757 --- /dev/null +++ b/app/src/main/res/layout/web_push_browser_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + +