Add debug info screens

Nice and freshly converted from compose 😃
This commit is contained in:
Jays2Kings 2023-10-18 13:21:27 -07:00
parent 0b37080e47
commit 8ae9b09d68
17 changed files with 497 additions and 59 deletions

View file

@ -140,9 +140,9 @@ class MangaDetailsAdapter(
fun showFloatingActionMode(view: TextView, content: String? = null, isTag: Boolean = false)
fun showChapterFilter()
fun favoriteManga(longPress: Boolean)
fun copyToClipboard(content: String, label: Int, useToast: Boolean = false)
fun copyContentToClipboard(content: String, label: Int, useToast: Boolean = false)
fun customActionMode(view: TextView): ActionMode.Callback
fun copyToClipboard(content: String, label: String?, useToast: Boolean = false)
fun copyContentToClipboard(content: String, label: String?, useToast: Boolean = false)
fun zoomImageFromThumb(thumbView: View)
fun showTrackingSheet()
fun updateScroll()

View file

@ -4,7 +4,6 @@ import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
@ -109,6 +108,7 @@ import eu.kanade.tachiyomi.util.system.setCustomTitleAndMessage
import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.copyToClipboard
import eu.kanade.tachiyomi.util.view.findChild
import eu.kanade.tachiyomi.util.view.getText
import eu.kanade.tachiyomi.util.view.isControllerVisible
@ -1639,10 +1639,10 @@ class MangaDetailsController :
* @param content the actual text to copy to the board
* @param label Label to show to the user describing the content
*/
override fun copyToClipboard(content: String, label: Int, useToast: Boolean) {
override fun copyContentToClipboard(content: String, label: Int, useToast: Boolean) {
val view = view ?: return
val contentType = if (label != 0) view.context.getString(label) else null
copyToClipboard(content, contentType, useToast)
copyContentToClipboard(content, contentType, useToast)
}
/**
@ -1651,22 +1651,8 @@ class MangaDetailsController :
* @param content the actual text to copy to the board
* @param label Label to show to the user describing the content
*/
override fun copyToClipboard(content: String, label: String?, useToast: Boolean) {
if (content.isBlank()) return
val activity = activity ?: return
val view = view ?: return
val clipboard = activity.getSystemService(ClipboardManager::class.java)
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
label ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return
if (useToast) {
activity.toast(view.context.getString(R.string._copied_to_clipboard, label))
} else {
snack = view.snack(view.context.getString(R.string._copied_to_clipboard, label))
}
override fun copyContentToClipboard(content: String, label: String?, useToast: Boolean) {
snack = copyToClipboard(content, label, useToast)
}
override fun showTrackingSheet() {
@ -1897,7 +1883,7 @@ class MangaDetailsController :
item: MenuItem?,
): Boolean {
when (item?.itemId) {
R.id.action_copy -> copyToClipboard(text, null)
R.id.action_copy -> copyContentToClipboard(text, null)
R.id.action_source_search -> sourceSearch(text)
R.id.action_global_search, R.id.action_local_search -> {
if (authorText != null) {

View file

@ -139,7 +139,7 @@ class MangaHeaderHolder(
}
title.setOnLongClickListener {
title.text?.toString()?.toNormalized()?.let {
adapter.delegate.copyToClipboard(it, R.string.title)
adapter.delegate.copyContentToClipboard(it, R.string.title)
}
true
}
@ -150,7 +150,7 @@ class MangaHeaderHolder(
}
mangaAuthor.setOnLongClickListener {
mangaAuthor.text?.toString()?.let {
adapter.delegate.copyToClipboard(it, R.string.author)
adapter.delegate.copyContentToClipboard(it, R.string.author)
}
true
}
@ -523,7 +523,7 @@ class MangaHeaderHolder(
adapter.delegate.showFloatingActionMode(chip, isTag = true)
}
chip.setOnLongClickListener {
adapter.delegate.copyToClipboard(genreText, genreText)
adapter.delegate.copyContentToClipboard(genreText, genreText)
true
}
this.addView(chip)

View file

@ -241,7 +241,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
override fun onTitleLongClick(position: Int) {
val title = adapter?.getItem(position)?.track?.title ?: return
controller.copyToClipboard(title, R.string.title, true)
controller.copyContentToClipboard(title, R.string.title, true)
}
private fun startTransition(duration: Long = 100) {

View file

@ -114,7 +114,7 @@ class AboutController : SettingsController() {
preference {
key = "pref_build_time"
titleRes = R.string.build_time
summary = getFormattedBuildTime()
summary = getFormattedBuildTime(dateFormat)
}
preferenceCategory {
@ -234,15 +234,18 @@ class AboutController : SettingsController() {
}
}
private fun getFormattedBuildTime(): String {
try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime = inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
companion object {
fun getFormattedBuildTime(dateFormat: DateFormat): String {
try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime =
inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
}
}
}
}

View file

@ -40,6 +40,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.setting.database.ClearDatabaseController
import eu.kanade.tachiyomi.ui.setting.debug.DebugController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.disableItems
import eu.kanade.tachiyomi.util.system.isPackageInstalled
@ -107,35 +108,47 @@ class SettingsAdvancedController : SettingsController() {
}
}
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager?
if (pm != null) {
preference {
key = "disable_batt_opt"
titleRes = R.string.disable_battery_optimization
summaryRes = R.string.disable_if_issues_with_updating
preference {
key = "debug_info"
titleRes = R.string.pref_debug_info
onClick {
val packageName: String = context.packageName
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = "package:$packageName".toUri()
}
startActivity(intent)
} else {
context.toast(R.string.battery_optimization_disabled)
}
}
onClick {
router.pushController(DebugController().withFadeTransaction())
}
}
preference {
key = "pref_dont_kill_my_app"
title = "Don't kill my app!"
summaryRes = R.string.about_dont_kill_my_app
preferenceCategory {
titleRes = R.string.label_background_activity
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager?
if (pm != null) {
preference {
key = "disable_batt_opt"
titleRes = R.string.disable_battery_optimization
summaryRes = R.string.disable_if_issues_with_updating
onClick {
openInBrowser("https://dontkillmyapp.com/")
onClick {
val packageName: String = context.packageName
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = "package:$packageName".toUri()
}
startActivity(intent)
} else {
context.toast(R.string.battery_optimization_disabled)
}
}
}
}
preference {
key = "pref_dont_kill_my_app"
title = "Don't kill my app!"
summaryRes = R.string.about_dont_kill_my_app
onClick {
openInBrowser("https://dontkillmyapp.com/")
}
}
}

View file

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.ui.setting.debug
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.databinding.SubDebugControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.util.view.copyToClipboard
import eu.kanade.tachiyomi.util.view.scrollViewWith
import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator
class BackupSchemaController : BaseController<SubDebugControllerBinding>() {
companion object {
const val title = "Backup file schema"
}
private val itemAdapter = ItemAdapter<DebugInfoItem>()
private val fastAdapter = FastAdapter.with(itemAdapter)
private val schema = ProtoBufSchemaGenerator.generateSchemaText(Backup.serializer().descriptor)
override fun getTitle() = title
override fun createBinding(inflater: LayoutInflater) =
SubDebugControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
scrollViewWith(binding.recycler, padBottom = true)
fastAdapter.setHasStableIds(true)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = fastAdapter
itemAdapter.add(DebugInfoItem(schema, false))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.sub_debug_info, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_copy -> copyToClipboard(schema, "Backup file schema", true)
}
return super.onOptionsItemSelected(item)
}
}

View file

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.ui.setting.debug
import android.os.Build
import androidx.preference.PreferenceScreen
import androidx.webkit.WebViewCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.more.AboutController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.onClick
import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import java.text.DateFormat
class DebugController : SettingsController() {
override fun getTitle() = resources?.getString(R.string.pref_debug_info)
private val dateFormat: DateFormat by lazy {
preferences.dateFormat()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
preference {
title = WorkerInfoController.title
onClick {
router.pushController(WorkerInfoController().withFadeTransaction())
}
}
preference {
title = BackupSchemaController.title
onClick {
router.pushController(BackupSchemaController().withFadeTransaction())
}
}
preferenceCategory {
title = "App Info"
preference {
key = "pref_version"
title = "Version"
summary = if (BuildConfig.DEBUG) {
"r" + BuildConfig.COMMIT_COUNT
} else {
BuildConfig.VERSION_NAME
}
}
preference {
key = "pref_build_time"
title = "Build Time"
summary = AboutController.getFormattedBuildTime(dateFormat)
}
preference {
key = "pref_webview_version"
title = "WebView version"
summary = getWebViewVersion()
}
}
preferenceCategory {
title = "Device info"
preference {
title = "Model"
summary = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})"
}
if (DeviceUtil.oneUiVersion != null) {
preference {
title = "OneUI version"
summary = "${DeviceUtil.oneUiVersion}"
}
} else if (DeviceUtil.miuiMajorVersion != null) {
preference {
title = "MIUI version"
summary = "${DeviceUtil.miuiMajorVersion}"
}
}
val androidVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Build.VERSION.RELEASE_OR_PREVIEW_DISPLAY
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Build.VERSION.RELEASE_OR_CODENAME
} else {
Build.VERSION.RELEASE
}
preference {
title = "Android version"
summary = "$androidVersion (${Build.DISPLAY})"
}
}
}
private fun getWebViewVersion(): String {
val activity = activity ?: return "Unknown"
val webView =
WebViewCompat.getCurrentWebViewPackage(activity) ?: return "how did you get here?"
val label = webView.applicationInfo.loadLabel(activity.packageManager)
val version = webView.versionName
return "$label $version"
}
}

View file

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.setting.debug
import android.view.View
import androidx.core.view.isVisible
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.DebugInfoItemBinding
class DebugInfoItem(val text: String, val header: Boolean) : AbstractItem<FastAdapter.ViewHolder<DebugInfoItem>>() {
/** defines the type defining this item. must be unique. preferably an id */
override val type: Int = R.id.debug_title
/** defines the layout which will be used for this item in the list */
override val layoutRes: Int = R.layout.debug_info_item
override var identifier = text.hashCode().toLong()
override fun getViewHolder(v: View): FastAdapter.ViewHolder<DebugInfoItem> {
return ViewHolder(v)
}
class ViewHolder(view: View) : FastAdapter.ViewHolder<DebugInfoItem>(view) {
val binding = DebugInfoItemBinding.bind(view)
override fun bindView(item: DebugInfoItem, payloads: List<Any>) {
binding.debugTitle.isVisible = item.header
binding.debugSummary.isVisible = !item.header
if (item.header) {
binding.debugTitle.text = item.text
} else {
binding.debugSummary.text = item.text
}
}
override fun unbindView(item: DebugInfoItem) {
binding.debugTitle.text = ""
binding.debugSummary.text = ""
}
}
}

View file

@ -0,0 +1,70 @@
package eu.kanade.tachiyomi.ui.setting.debug
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SubDebugControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.copyToClipboard
import eu.kanade.tachiyomi.util.view.scrollViewWith
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.merge
class WorkerInfoController : BaseCoroutineController<SubDebugControllerBinding, WorkerInfoPresenter>() {
companion object {
const val title = "Worker info"
}
override var presenter = WorkerInfoPresenter()
private val itemAdapter = ItemAdapter<DebugInfoItem>()
private val fastAdapter = FastAdapter.with(itemAdapter)
override fun getTitle() = title
override fun createBinding(inflater: LayoutInflater) =
SubDebugControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
scrollViewWith(binding.recycler, padBottom = true)
fastAdapter.setHasStableIds(true)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = fastAdapter
binding.recycler.itemAnimator = null
viewScope.launchUI {
merge(presenter.enqueued, presenter.finished, presenter.running).collectLatest {
itemAdapter.clear()
itemAdapter.add(DebugInfoItem("Enqueued", true))
itemAdapter.add(DebugInfoItem(presenter.enqueued.value, false))
itemAdapter.add(DebugInfoItem("Finished", true))
itemAdapter.add(DebugInfoItem(presenter.finished.value, false))
itemAdapter.add(DebugInfoItem("Running", true))
itemAdapter.add(DebugInfoItem(presenter.running.value, false))
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.sub_debug_info, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_copy -> copyToClipboard(
"${presenter.enqueued.value}\n${presenter.finished.value}\n${presenter.running.value}",
"Backup file schema",
true,
)
}
return super.onOptionsItemSelected(item)
}
}

View file

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.ui.setting.debug
import android.app.Application
import androidx.lifecycle.asFlow
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class WorkerInfoPresenter : BaseCoroutinePresenter<WorkerInfoController>() {
private val workManager by lazy { WorkManager.getInstance(Injekt.get<Application>()) }
val finished by lazy {
workManager
.getWorkInfosLiveData(
WorkQuery.fromStates(
WorkInfo.State.SUCCEEDED,
WorkInfo.State.FAILED,
WorkInfo.State.CANCELLED,
),
)
.asFlow()
.map(::constructString)
.stateIn(presenterScope, SharingStarted.WhileSubscribed(), "")
}
val running by lazy {
workManager
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.asFlow()
.map(::constructString)
.stateIn(presenterScope, SharingStarted.WhileSubscribed(), "")
}
val enqueued by lazy {
workManager
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
.asFlow()
.map(::constructString)
.stateIn(presenterScope, SharingStarted.WhileSubscribed(), "")
}
private fun constructString(list: List<WorkInfo>) = buildString {
if (list.isEmpty()) {
appendLine("-")
} else {
val newList = list.toList()
newList.forEach { workInfo ->
appendLine("Id: ${workInfo.id}")
appendLine("Tags:")
workInfo.tags.forEach {
appendLine(" - $it")
}
appendLine("State: ${workInfo.state}")
appendLine()
}
}
}
}

View file

@ -10,6 +10,20 @@ object DeviceUtil {
getSystemProperty("ro.miui.ui.version.name")?.isNotEmpty() ?: false
}
/**
* Extracts the MIUI major version code from a string like "V12.5.3.0.QFGMIXM".
*
* @return MIUI major version code (e.g., 13) or null if can't be parsed.
*/
val miuiMajorVersion by lazy {
if (!isMiui) return@lazy null
Build.VERSION.INCREMENTAL
.substringBefore('.')
.trimStart('V')
.toIntOrNull()
}
@SuppressLint("PrivateApi")
fun isMiuiOptimizationDisabled(): Boolean {
val sysProp = getSystemProperty("persist.sys.miui_optimization")
@ -30,6 +44,20 @@ object DeviceUtil {
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
}
val oneUiVersion by lazy {
try {
val semPlatformIntField = Build.VERSION::class.java.getDeclaredField("SEM_PLATFORM_INT")
val version = semPlatformIntField.getInt(null) - 90000
if (version < 0) {
1.0
} else {
((version / 10000).toString() + "." + version % 10000 / 100).toDouble()
}
} catch (e: Exception) {
null
}
}
val invalidDefaultBrowsers = listOf(
"android",
"com.huawei.android.internal.app",

View file

@ -4,6 +4,8 @@ import android.Manifest
import android.animation.Animator
import android.animation.ValueAnimator
import android.app.ActivityManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@ -50,6 +52,7 @@ import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -824,6 +827,25 @@ fun Controller.openInBrowser(url: String) {
}
}
fun Controller.copyToClipboard(content: String, label: String?, useToast: Boolean): Snackbar? {
if (content.isBlank()) return null
val activity = activity ?: return null
val view = view ?: return null
val clipboard = activity.getSystemService(ClipboardManager::class.java)
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
label ?: return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return null
return if (useToast) {
activity.toast(view.context.getString(R.string._copied_to_clipboard, label))
null
} else {
view.snack(view.context.getString(R.string._copied_to_clipboard, label))
}
}
val Controller.activityBinding: MainActivityBinding?
get() = (activity as? MainActivity)?.binding

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/debug_title"
style="?textAppearanceTitleMedium"
android:padding="8dp"
android:layout_gravity="center_vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/card_view_group"
android:layout_width="match_parent"
tools:visibility="invisible"
android:layout_height="wrap_content"
tools:text="Enqueued" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/debug_summary"
android:typeface="monospace"
android:padding="8dp"
android:layout_gravity="center_vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/card_view_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Enqueued" />
</FrameLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"/>
</FrameLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_copy"
android:icon="@drawable/ic_content_copy_24dp"
android:title="@string/copy_value"
app:showAsAction="ifRoom" />
</menu>

View file

@ -877,6 +877,8 @@
not in your library \nCurrently using: %1$s</string>
<string name="most_entries">Most entries</string>
<string name="select_uninstalled_sources">Select uninstalled sources</string>
<string name="pref_debug_info">Debug info</string>
<string name="label_background_activity">Background activity</string>
<!-- Browse Settings -->
<string name="pref_global_search">Global search</string>