diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a5b3ddbc..3248108c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -365,7 +365,32 @@ android:value="org.kde.kdeconnect.UserInterface.MainActivity" /> + + + + + + + + + + diff --git a/res/drawable/ic_baseline_info_24.xml b/res/drawable/ic_baseline_info_24.xml new file mode 100644 index 00000000..17255b7a --- /dev/null +++ b/res/drawable/ic_baseline_info_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/ic_baseline_send_24.xml b/res/drawable/ic_baseline_send_24.xml new file mode 100644 index 00000000..f0d63e17 --- /dev/null +++ b/res/drawable/ic_baseline_send_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/res/drawable/ic_baseline_web_24.xml b/res/drawable/ic_baseline_web_24.xml new file mode 100644 index 00000000..48696cd5 --- /dev/null +++ b/res/drawable/ic_baseline_web_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/ic_kde_24dp.xml b/res/drawable/ic_kde_24dp.xml new file mode 100644 index 00000000..674b8c34 --- /dev/null +++ b/res/drawable/ic_kde_24dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/res/drawable/ic_kde_48dp.xml b/res/drawable/ic_kde_48dp.xml new file mode 100644 index 00000000..b1b16f8f --- /dev/null +++ b/res/drawable/ic_kde_48dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/res/drawable/konqi.png b/res/drawable/konqi.png new file mode 100644 index 00000000..68216c8c Binary files /dev/null and b/res/drawable/konqi.png differ diff --git a/res/layout/about_person_list_item_entry.xml b/res/layout/about_person_list_item_entry.xml new file mode 100644 index 00000000..54fd627b --- /dev/null +++ b/res/layout/about_person_list_item_entry.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + diff --git a/res/layout/activity_about_kde.xml b/res/layout/activity_about_kde.xml new file mode 100644 index 00000000..5995d5fa --- /dev/null +++ b/res/layout/activity_about_kde.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/activity_easter_egg.xml b/res/layout/activity_easter_egg.xml new file mode 100644 index 00000000..60b7f7d2 --- /dev/null +++ b/res/layout/activity_easter_egg.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/activity_licenses.xml b/res/layout/activity_licenses.xml new file mode 100644 index 00000000..f14d58f6 --- /dev/null +++ b/res/layout/activity_licenses.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/fragment_about.xml b/res/layout/fragment_about.xml new file mode 100644 index 00000000..dc2dec85 --- /dev/null +++ b/res/layout/fragment_about.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/menu_licenses.xml b/res/menu/menu_licenses.xml new file mode 100644 index 00000000..4f32b6f0 --- /dev/null +++ b/res/menu/menu_licenses.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index bfd6fb90..37919e90 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -1,4 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 24cdb865..fac1dbb3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -454,5 +454,68 @@ Source Code https://invent.kde.org/network/kdeconnect-android/ Licenses + Website + https://kdeconnect.kde.org/ + + (С) 2013-2021 The KDE Connect Developers + + About + Authors + Thanks To + Easter Egg + Email contributor\n%s + Visit contributor\'s homepage\n%s + Version %s + About KDE + KDE - Be Free! + KDE + Konqi + Rise Up + Rise Down + + Multi-platform app that allows your devices to communicate (e.g., your phone and your computer) + + About +

KDE is a world-wide community of software engineers, artists, writers, translators and creators who are committed to Free Software development. KDE produces the Plasma desktop environment, hundreds of applications, and the many software libraries that support them.

+

KDE is a cooperative enterprise: no single entity controls its direction or products. Instead, we work together to achieve the common goal of building the world\'s finest Free Software. Everyone is welcome to join and contribute to KDE, including you.

+ Visit https://www.kde.org/ for more information about the KDE community and the software we produce. + ]]>
+ + Report Bugs or Wishes +

Software can always be improved, and the KDE team is ready to do so. However, you - the user - must tell us when something does not work as expected or could be done better.

+

KDE has a bug tracking system. Visit https://bugs.kde.org/ or use the \"Report Bug\" button from the about screen to report bugs.

+ If you have a suggestion for improvement then you are welcome to use the bug tracking system to register your wish. Make sure you use the severity called \"Wishlist\". + ]]>
+ + Join KDE +

You do not have to be a software developer to be a member of the KDE team. You can join the national teams that translate program interfaces. You can provide graphics, themes, sounds, and improved documentation. You decide!

+

Visit https://community.kde.org/Get_Involved for information on some projects in which you can participate.

+ If you need more information or documentation, then a visit to https://techbase.kde.org/ will provide you with what you need. + ]]>
+ + Support KDE +

KDE software is and will always be available free of charge, however creating it is not free.

+

To support development the KDE community has formed the KDE e.V., a non-profit organization legally founded in Germany. KDE e.V. represents the KDE community in legal and financial matters. See https://ev.kde.org/ for information on KDE e.V.

+

KDE benefits from many kinds of contributions, including financial. We use the funds to reimburse members and others for expenses they incur when contributing. Further funds are used for legal support and organizing conferences and meetings.

+

We would like to encourage you to support our efforts with a financial donation, using one of the ways described at https://www.kde.org/community/donations/.

+ Thank you very much in advance for your support. + ]]>
+ + + Maintainer and developer + Developer + macOS support. Working on iOS support + Bug fixes and general improvements + SFTP implementation, bug fixes and general improvements + SMS plugin improvements + Contacts plugin improvements + UI improvements and this about page + Remote keyboard plugin and bug fixes + Support for using the keyboard in the remote input plugin, bug fixes and general improvements + Everyone else who has contributed to KDE Connect over the years diff --git a/res/values/styles.xml b/res/values/styles.xml index a9fb8470..3ac5f9fc 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -63,4 +63,20 @@ true false + + + diff --git a/src/org/kde/kdeconnect/UserInterface/About/AboutData.kt b/src/org/kde/kdeconnect/UserInterface/About/AboutData.kt new file mode 100644 index 00000000..fd9595a8 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AboutData.kt @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable + +class AboutData(var name: String, var description: Int, var icon: Int, var versionName: String, var copyrightStatement: String? = null, + var bugURL: String? = null, var websiteURL: String? = null, var sourceCodeURL: String? = null, var donateURL: String? = null, + var authorsFooterText: Int? = null) : Parcelable { + val authors: MutableList = mutableListOf() + + constructor(parcel: Parcel) : this(parcel.readString()!!, parcel.readInt(), parcel.readInt(), parcel.readString()!!, parcel.readString(), + parcel.readString(), parcel.readString(), parcel.readString(), parcel.readString(), + if (parcel.readByte() == 0x01.toByte()) parcel.readInt() else null) { + parcel.readList(authors as List<*>, AboutPerson::class.java.classLoader) + } + + fun getDescriptionString(context: Context): String = context.resources.getString(description) + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AboutData = AboutData(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + parcel.writeInt(description) + parcel.writeInt(icon) + parcel.writeString(versionName) + parcel.writeString(copyrightStatement) + parcel.writeList(authors.toList()) + + parcel.writeString(bugURL) + parcel.writeString(websiteURL) + parcel.writeString(sourceCodeURL) + parcel.writeString(donateURL) + + if (authorsFooterText == null) { + parcel.writeByte(0x00) + } else { + parcel.writeByte(0x01) + parcel.writeInt(authorsFooterText!!) + } + } + + override fun describeContents(): Int = 0 +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/AboutFragment.kt b/src/org/kde/kdeconnect/UserInterface/About/AboutFragment.kt new file mode 100644 index 00000000..9331db92 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AboutFragment.kt @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import org.kde.kdeconnect.UserInterface.List.ListAdapter +import org.kde.kdeconnect.UserInterface.MainActivity +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.FragmentAboutBinding + +class AboutFragment : Fragment() { + private var binding: FragmentAboutBinding? = null + private lateinit var aboutData: AboutData + private var tapCount = 0 + private var firstTapMillis: Long? = null + + companion object { + @JvmStatic + fun newInstance(aboutData: AboutData): Fragment { + val fragment = AboutFragment() + + val args = Bundle(1) + args.putParcelable("ABOUT_DATA", aboutData) + fragment.arguments = args + + return fragment + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + if (activity != null) { + (requireActivity() as MainActivity).supportActionBar?.setTitle(R.string.about) + } + + aboutData = requireArguments().getParcelable("ABOUT_DATA")!! + binding = FragmentAboutBinding.inflate(inflater, container, false) + + updateData() + return binding!!.root + } + + @SuppressLint("SetTextI18n") + fun updateData() { + // Update general info + + binding!!.appName.text = aboutData.name + binding!!.appDescription.text = this.context?.let { aboutData.getDescriptionString(it) } + if (aboutData.copyrightStatement == null) "" else "\n\n" + aboutData.copyrightStatement + binding!!.appIcon.setImageDrawable(this.context?.let { ContextCompat.getDrawable(it, aboutData.icon) }) + binding!!.appVersion.text = this.context?.getString(R.string.version, aboutData.versionName) + + // Setup Easter Egg onClickListener + + binding!!.generalInfoCard.setOnClickListener { + if (firstTapMillis == null) { + firstTapMillis = System.currentTimeMillis() + } + + if (++tapCount == 3) { + tapCount = 0 + + if (firstTapMillis!! >= (System.currentTimeMillis() - 500)) { + startActivity(Intent(context, EasterEggActivity::class.java)) + } + + firstTapMillis = null + } + } + + // Update button onClickListeners + + setupInfoButton(aboutData.bugURL, binding!!.reportBugButton) + setupInfoButton(aboutData.donateURL, binding!!.donateButton) + setupInfoButton(aboutData.sourceCodeURL, binding!!.sourceCodeButton) + + binding!!.licensesButton.setOnClickListener { + startActivity(Intent(context, LicensesActivity::class.java)) + } + + binding!!.aboutKdeButton.setOnClickListener { + startActivity(Intent(context, AboutKDEActivity::class.java)) + } + + setupInfoButton(aboutData.websiteURL, binding!!.websiteButton) + + // Update authors + binding!!.authorsList.adapter = ListAdapter(this.context, aboutData.authors.map { AboutPersonEntryItem(it) }, false) + if (aboutData.authorsFooterText != null) { + binding!!.authorsFooterText.text = context?.getString(aboutData.authorsFooterText!!) + } + } + + private fun setupInfoButton(url: String?, button: FrameLayout) { + if (url == null) { + button.visibility = View.GONE + } else { + button.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/AboutKDEActivity.kt b/src/org/kde/kdeconnect/UserInterface/About/AboutKDEActivity.kt new file mode 100644 index 00000000..0c29284a --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AboutKDEActivity.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.os.Bundle +import android.text.Html +import android.text.Spanned +import android.text.method.LinkMovementMethod +import androidx.appcompat.app.AppCompatActivity +import org.kde.kdeconnect.UserInterface.ThemeUtil +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.ActivityAboutKdeBinding + +class AboutKDEActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + ThemeUtil.setUserPreferredTheme(this) + super.onCreate(savedInstanceState) + + val binding = ActivityAboutKdeBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbarLayout.toolbar) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar!!.setDisplayShowHomeEnabled(true) + + binding.aboutTextView.text = fromHtml(resources.getString(R.string.about_kde_about)) + binding.reportBugsOrWishesTextView.text = fromHtml(resources.getString(R.string.about_kde_report_bugs_or_wishes)) + binding.joinKdeTextView.text = fromHtml(resources.getString(R.string.about_kde_join_kde)) + binding.supportKdeTextView.text = fromHtml(resources.getString(R.string.about_kde_support_kde)) + + binding.aboutTextView.movementMethod = LinkMovementMethod.getInstance() + binding.reportBugsOrWishesTextView.movementMethod = LinkMovementMethod.getInstance() + binding.joinKdeTextView.movementMethod = LinkMovementMethod.getInstance() + binding.supportKdeTextView.movementMethod = LinkMovementMethod.getInstance() + } + + private fun fromHtml(html: String): Spanned { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") Html.fromHtml(html) + } + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/AboutPerson.kt b/src/org/kde/kdeconnect/UserInterface/About/AboutPerson.kt new file mode 100644 index 00000000..c64aa6a1 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AboutPerson.kt @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.os.Parcel +import android.os.Parcelable + +class AboutPerson @JvmOverloads constructor(val name: String, val task: Int? = null, val emailAddress: String? = null, val webAddress: String? = null) : Parcelable { + constructor(parcel: Parcel) : this(parcel.readString().toString(), if (parcel.readByte() == 0x01.toByte()) parcel.readInt() else null, parcel.readString(), parcel.readString()) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + + if (task != null) { + parcel.writeByte(0x01) + parcel.writeInt(task) + } else { + parcel.writeByte(0x00) + } + + parcel.writeString(emailAddress) + parcel.writeString(webAddress) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): AboutPerson = AboutPerson(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } +} diff --git a/src/org/kde/kdeconnect/UserInterface/About/AboutPersonEntryItem.kt b/src/org/kde/kdeconnect/UserInterface/About/AboutPersonEntryItem.kt new file mode 100644 index 00000000..d7489def --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AboutPersonEntryItem.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.content.Intent +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.widget.TooltipCompat +import org.kde.kdeconnect.UserInterface.List.ListAdapter +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.AboutPersonListItemEntryBinding + +class AboutPersonEntryItem(val person: AboutPerson) : ListAdapter.Item { + override fun inflateView(layoutInflater: LayoutInflater): View { + val binding = AboutPersonListItemEntryBinding.inflate(layoutInflater) + + binding.aboutPersonListItemEntryName.text = person.name + + if (person.task != null) { + binding.aboutPersonListItemEntryTask.visibility = View.VISIBLE + binding.aboutPersonListItemEntryTask.text = layoutInflater.context.getString(person.task) + } + + if (person.emailAddress != null) { + binding.aboutPersonListItemEntryEmailButton.visibility = View.VISIBLE + TooltipCompat.setTooltipText(binding.aboutPersonListItemEntryEmailButton, layoutInflater.context.getString(R.string.email_contributor, person.emailAddress)) + binding.aboutPersonListItemEntryEmailButton.setOnClickListener { + layoutInflater.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("mailto:" + person.emailAddress))) + } + } + + if (person.webAddress != null) { + binding.aboutPersonListItemEntryVisitHomepageButton.visibility = View.VISIBLE + TooltipCompat.setTooltipText(binding.aboutPersonListItemEntryVisitHomepageButton, layoutInflater.context.resources.getString(R.string.visit_contributors_homepage, person.webAddress)) + binding.aboutPersonListItemEntryVisitHomepageButton.setOnClickListener { + layoutInflater.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(person.webAddress))) + } + } + + return binding.root + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/AdapterLinearLayout.kt b/src/org/kde/kdeconnect/UserInterface/About/AdapterLinearLayout.kt new file mode 100644 index 00000000..57c6da5f --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AdapterLinearLayout.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.content.Context +import android.database.DataSetObserver +import android.util.AttributeSet +import android.widget.Adapter +import android.widget.LinearLayout + +class AdapterLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : LinearLayout(context, attrs, defStyle) { + var adapter: Adapter? = null + set(adapter) { + if (field !== adapter) { + field = adapter + field?.registerDataSetObserver(dataSetObserver) + reloadChildViews() + } + } + + private val dataSetObserver: DataSetObserver = object : DataSetObserver() { + override fun onChanged() { + super.onChanged() + reloadChildViews() + } + } + + init { + orientation = VERTICAL + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + adapter?.unregisterDataSetObserver(dataSetObserver) + } + + private fun reloadChildViews() { + removeAllViews() + + if (adapter != null) { + for (position in 0 until adapter!!.count) { + adapter!!.getView(position, null, this)?.let { addView(it) } + } + + requestLayout() + } + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/ApplicationAboutData.kt b/src/org/kde/kdeconnect/UserInterface/About/ApplicationAboutData.kt new file mode 100644 index 00000000..fc662e72 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/ApplicationAboutData.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.content.Context +import org.kde.kdeconnect_tp.BuildConfig +import org.kde.kdeconnect_tp.R + +/** +* Add authors and credits here + */ +fun getApplicationAboutData(context: Context): AboutData { + val aboutData = AboutData(context.getString(R.string.kde_connect), R.string.app_description, R.drawable.icon, BuildConfig.VERSION_NAME, context.getString(R.string.copyright_statement), + context.getString(R.string.report_bug_url), context.getString(R.string.website_url), context.getString(R.string.source_code_url), context.getString(R.string.donate_url), + R.string.everyone_else) + + aboutData.authors += AboutPerson("Albert Vaca Cintora", R.string.maintainer_and_developer, "albertvaka+kde@gmail.com") + aboutData.authors += AboutPerson("Aleix Pol", R.string.developer, "aleixpol@kde.org") + aboutData.authors += AboutPerson("Inoki Shaw", R.string.apple_support, "veyx.shaw@gmail.com") + aboutData.authors += AboutPerson("Matthijs Tijink", R.string.developer, "matthijstijink@gmail.com") + aboutData.authors += AboutPerson("Nicolas Fella", R.string.developer, "nicolas.fella@gmx.de") + aboutData.authors += AboutPerson("Philip Cohn-Cort", R.string.developer, "cliabhach@gmail.com") + aboutData.authors += AboutPerson("Piyush Aggarwal", R.string.developer, "piyushaggarwal002@gmail.com") + aboutData.authors += AboutPerson("Simon Redman", R.string.developer, "simon@ergotech.com") + aboutData.authors += AboutPerson("Erik Duisters", R.string.developer, "e.duisters1@gmail.com") + aboutData.authors += AboutPerson("Isira Seneviratne", R.string.developer, "isirasen96@gmail.com") + aboutData.authors += AboutPerson("Vineet Garg", R.string.developer, "grg.vineet@gmail.com") + aboutData.authors += AboutPerson("Anjani Kumar", R.string.bug_fixes_and_general_improvements, "anjanik012@gmail.com") + aboutData.authors += AboutPerson("Samoilenko Yuri", R.string.samoilenko_yuri_task, "kinnalru@gmail.com") + aboutData.authors += AboutPerson("Aniket Kumar", R.string.aniket_kumar_task, "anikketkumar786@gmail.com") + aboutData.authors += AboutPerson("Àlex Fiestas", R.string.alex_fiestas_task, "afiestas@kde.org") + aboutData.authors += AboutPerson("Daniel Tang", R.string.bug_fixes_and_general_improvements, "danielzgtg.opensource@gmail.com") + aboutData.authors += AboutPerson("Maxim Leshchenko", R.string.maxim_leshchenko_task, "cnmaks90@gmail.com") + aboutData.authors += AboutPerson("Holger Kaelberer", R.string.holger_kaelberer_task, "holger.k@elberer.de") + aboutData.authors += AboutPerson("Saikrishna Arcot", R.string.saikrishna_arcot_task, "saiarcot895@gmail.com") + + return aboutData +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/AutoGridLayout.kt b/src/org/kde/kdeconnect/UserInterface/About/AutoGridLayout.kt new file mode 100644 index 00000000..2b18167d --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/AutoGridLayout.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.content.Context +import android.util.AttributeSet +import androidx.gridlayout.widget.GridLayout +import org.kde.kdeconnect_tp.R +import kotlin.math.max + +/** +* GridLayout that adjusts the number of columns and rows to fill all screen space + */ +class AutoGridLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : GridLayout(context, attrs, defStyleAttr) { + private var defaultColumnCount = 0 + private var columnWidth = 0 + private var changeColumnCountIfTheyHaveOnlyOneElement = false + + init { + var typedArray = context.obtainStyledAttributes(attrs, R.styleable.AutoGridLayout, 0, defStyleAttr) + + try { + columnWidth = typedArray.getDimensionPixelSize(R.styleable.AutoGridLayout_columnWidth, 0) + changeColumnCountIfTheyHaveOnlyOneElement = typedArray.getBoolean(R.styleable.AutoGridLayout_changeColumnCountIfTheyHaveOnlyOneElement, false) + typedArray = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.columnCount), 0, defStyleAttr) + defaultColumnCount = typedArray.getInt(0, 10) + } finally { + typedArray.recycle() + } + + columnCount = 1 + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + val width = MeasureSpec.getSize(widthSpec) + + if (columnWidth > 0 && width > 0) { + val totalSpace = width - paddingRight - paddingLeft + var calculatedColumnCount = max(1, totalSpace / columnWidth) + + if (calculatedColumnCount < childCount && changeColumnCountIfTheyHaveOnlyOneElement) { + calculatedColumnCount = defaultColumnCount + } + + columnCount = calculatedColumnCount + } else { + columnCount = defaultColumnCount + } + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/EasterEggActivity.kt b/src/org/kde/kdeconnect/UserInterface/About/EasterEggActivity.kt new file mode 100644 index 00000000..21067d06 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/EasterEggActivity.kt @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.animation.ValueAnimator +import android.content.pm.ActivityInfo +import android.graphics.Color +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.ActivityEasterEggBinding +import kotlin.math.PI +import kotlin.math.atan2 + +private val KDE_ICON_BACKGROUND_COLOR = Color.rgb(29, 153, 243) +private val KONQI_BACKGROUND_COLOR = Color.rgb(191, 255, 0) + +class EasterEggActivity : AppCompatActivity(), SensorEventListener { + private var binding: ActivityEasterEggBinding? = null + private lateinit var sensorManager: SensorManager + private val animator = ValueAnimator() + private var isAlreadyLongClicked = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityEasterEggBinding.inflate(layoutInflater) + setContentView(binding!!.root) + + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR + + // Make the status bar blue to make the Easter Egg beautiful + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + window.statusBarColor = KDE_ICON_BACKGROUND_COLOR + window.navigationBarColor = KDE_ICON_BACKGROUND_COLOR + setLightSystemWindowsEnabled(false) + } + + sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager + if (hasAccelerometer()) { + animator.addUpdateListener { + binding!!.logo.rotation = animator.animatedValue as Float + } + animator.duration = 300 + } else { + binding!!.angle.visibility = View.GONE + } + + // Make Easter Egg more fun + binding!!.easterEggLayout.setOnLongClickListener { + if (!isAlreadyLongClicked) { + isAlreadyLongClicked = true + + binding!!.easterEggLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.activity_background)) + binding!!.logo.setColorFilter(ContextCompat.getColor(this, R.color.text_color)) + binding!!.angle.setTextColor(ContextCompat.getColor(this, R.color.text_color)) + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + var typedArray = this.theme.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor)) + window.statusBarColor = typedArray.getColor(0, Color.WHITE) + window.navigationBarColor = typedArray.getColor(0, Color.WHITE) + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + typedArray = this.theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar)) + setLightSystemWindowsEnabled(typedArray.getBoolean(0, true)) + } + + typedArray.recycle() + } + } + + val icon = intArrayOf( + R.drawable.ic_action_keyboard_24dp, R.drawable.ic_action_refresh_24dp, + R.drawable.ic_action_image_edit_24dp, R.drawable.ic_arrow_upward_black_24dp, + R.drawable.ic_baseline_attach_money_24, R.drawable.ic_baseline_bug_report_24, + R.drawable.ic_baseline_code_24, R.drawable.ic_baseline_gavel_24, + R.drawable.ic_baseline_info_24, R.drawable.ic_baseline_web_24, + R.drawable.ic_baseline_send_24, R.drawable.ic_baseline_sms_24, + R.drawable.ic_accept_pairing_24dp, R.drawable.ic_share_white, + R.drawable.ic_camera, R.drawable.ic_delete, + R.drawable.ic_device_laptop_32dp, R.drawable.ic_device_phone_32dp, + R.drawable.ic_device_tablet_32dp, R.drawable.ic_device_tv_32dp, + R.drawable.ic_delete, R.drawable.ic_warning, + R.drawable.ic_volume_black, R.drawable.ic_wifi, + R.drawable.ic_add, R.drawable.touchpad_plugin_action_24dp, + R.drawable.konqi, R.drawable.run_command_plugin_icon_24dp, + R.drawable.ic_phonelink_36dp, R.drawable.ic_phonelink_off_36dp, + R.drawable.ic_error_outline_48dp, R.drawable.ic_home_black_24dp, + R.drawable.ic_settings_white_32dp, R.drawable.ic_stop, + R.drawable.ic_rewind_black, R.drawable.ic_play_black, + R.drawable.ic_mic_black, R.drawable.ic_pause_black, + R.drawable.ic_volume_mute_black, R.drawable.ic_arrow_upward_black_24dp, + R.drawable.ic_next_black, R.drawable.ic_previous_black, + R.drawable.ic_presenter_24dp, R.drawable.ic_key, + R.drawable.ic_keyboard_return_black_24dp, R.drawable.ic_keyboard_hide_36dp, + R.drawable.ic_kde_24dp, R.drawable.ic_album_art_placeholder, + R.drawable.ic_arrow_back_black_24dp, R.drawable.share_plugin_action_24dp + ).random() + + if (icon == R.drawable.konqi) { + binding!!.logo.clearColorFilter() + binding!!.easterEggLayout.setBackgroundColor(KONQI_BACKGROUND_COLOR) + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + window.statusBarColor = KONQI_BACKGROUND_COLOR + window.navigationBarColor = KONQI_BACKGROUND_COLOR + } + + isAlreadyLongClicked = false + } + + binding!!.logo.setImageResource(icon) + + true + } + } + + private fun hasAccelerometer(): Boolean = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null + + private fun setLightSystemWindowsEnabled(enabled: Boolean) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + if (enabled) { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + if (enabled) { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + } + } + + override fun onResume() { + super.onResume() + if (hasAccelerometer()) { + sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI) + } + } + + override fun onPause() { + if (hasAccelerometer()) { + sensorManager.unregisterListener(this) + } + super.onPause() + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { } + override fun onSensorChanged(event: SensorEvent) { + if (binding != null) { + val axisX = event.values[0] + val axisY = event.values[1] + + val angle = (atan2(axisX, axisY) / (PI / 180)).toInt() + binding!!.angle.text = angle.toString() + "°" + + animator.setFloatValues(binding!!.logo.rotation, angle.toFloat()) + animator.start() + } + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/LicensesActivity.kt b/src/org/kde/kdeconnect/UserInterface/About/LicensesActivity.kt new file mode 100644 index 00000000..418fc684 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/LicensesActivity.kt @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import org.apache.commons.io.IOUtils +import org.kde.kdeconnect.UserInterface.ThemeUtil +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.ActivityLicensesBinding +import java.nio.charset.Charset + +class LicensesActivity : AppCompatActivity() { + private lateinit var binding: ActivityLicensesBinding + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeUtil.setUserPreferredTheme(this) + super.onCreate(savedInstanceState) + + binding = ActivityLicensesBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbarLayout.toolbar) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar!!.setDisplayShowHomeEnabled(true) + + binding.licensesText.layoutManager = LinearLayoutManager(this) + binding.licensesText.adapter = StringListAdapter(getLicenses().split("\n\n")) + } + + private fun getLicenses(): String = resources.openRawResource(R.raw.license).use { inputStream -> IOUtils.toString(inputStream, Charset.defaultCharset()) } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_licenses, menu) + return super.onCreateOptionsMenu(menu) + } + + private fun smoothScrollToPosition(position: Int) { + val linearSmoothScroller: LinearSmoothScroller = object : LinearSmoothScroller(this) { + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = 2.5F / displayMetrics.densityDpi + } + + linearSmoothScroller.targetPosition = position + binding.licensesText.layoutManager?.startSmoothScroll(linearSmoothScroller) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.menu_rise_up -> { + smoothScrollToPosition(0) + true + } + R.id.menu_rise_down -> { + smoothScrollToPosition(binding.licensesText.adapter!!.itemCount - 1) + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/About/StringListAdapter.kt b/src/org/kde/kdeconnect/UserInterface/About/StringListAdapter.kt new file mode 100644 index 00000000..74b3ac56 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/About/StringListAdapter.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 Maxim Leshchenko + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +package org.kde.kdeconnect.UserInterface.About + +import android.widget.TextView +import android.view.View +import android.view.ViewGroup +import androidx.core.view.setPadding +import androidx.recyclerview.widget.RecyclerView + +class StringListAdapter(private val stringList: List) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val textView = TextView(parent.context) + textView.setPadding(8) + return ViewHolder(textView) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + (holder.itemView as TextView).text = stringList[position] + } + + override fun getItemCount(): Int = stringList.size + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/UserInterface/List/ListAdapter.java b/src/org/kde/kdeconnect/UserInterface/List/ListAdapter.java index e555c95e..4db173b0 100644 --- a/src/org/kde/kdeconnect/UserInterface/List/ListAdapter.java +++ b/src/org/kde/kdeconnect/UserInterface/List/ListAdapter.java @@ -23,10 +23,16 @@ public class ListAdapter extends ArrayAdapter { } private final List items; + private final boolean isEnabled; public ListAdapter(Context context, List items) { + this(context, items, true); + } + + public ListAdapter(Context context, List items, boolean isEnabled) { super(context, 0, (List) items); this.items = items; + this.isEnabled = isEnabled; } @NonNull @@ -35,4 +41,9 @@ public class ListAdapter extends ArrayAdapter { final Item i = items.get(position); return i.inflateView(LayoutInflater.from(parent.getContext())); } + + @Override + public boolean isEnabled(int position) { + return isEnabled; + } } diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index 7ff063cd..374386af 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -22,7 +22,6 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; @@ -35,16 +34,20 @@ import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment; +import org.kde.kdeconnect.UserInterface.About.AboutFragment; +import org.kde.kdeconnect.UserInterface.About.ApplicationAboutDataKt; import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.databinding.ActivityMainBinding; import java.util.Collection; import java.util.HashMap; +import java.util.Objects; public class MainActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private static final int MENU_ENTRY_ADD_DEVICE = 1; //0 means no-selection private static final int MENU_ENTRY_SETTINGS = 2; + private static final int MENU_ENTRY_ABOUT = 3; private static final int MENU_ENTRY_DEVICE_FIRST_ID = 1000; //All subsequent ids are devices in the menu private static final int MENU_ENTRY_DEVICE_UNKNOWN = 9999; //It's still a device, but we don't know which one yet private static final int STORAGE_lOCATION_CONFIGURED = 2020; @@ -129,6 +132,11 @@ public class MainActivity extends AppCompatActivity implements SharedPreferences preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply(); setContentFragment(new SettingsFragment()); break; + case MENU_ENTRY_ABOUT: + mCurrentDevice = null; + preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply(); + setContentFragment(AboutFragment.newInstance(Objects.requireNonNull(ApplicationAboutDataKt.getApplicationAboutData(this)))); + break; default: String deviceId = mMapMenuToDeviceId.get(menuItem); onDeviceSelected(deviceId); @@ -187,6 +195,8 @@ public class MainActivity extends AppCompatActivity implements SharedPreferences } else { if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS) { setContentFragment(new SettingsFragment()); + } else if (mCurrentMenuEntry == MENU_ENTRY_ABOUT) { + setContentFragment(AboutFragment.newInstance(Objects.requireNonNull(ApplicationAboutDataKt.getApplicationAboutData(this)))); } else { setContentFragment(new PairingFragment()); } @@ -282,6 +292,10 @@ public class MainActivity extends AppCompatActivity implements SharedPreferences settingsItem.setIcon(R.drawable.ic_settings_white_32dp); settingsItem.setCheckable(true); + MenuItem aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about); + aboutItem.setIcon(R.drawable.ic_baseline_info_24); + aboutItem.setCheckable(true); + //Ids might have changed if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) { mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice); diff --git a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java index 0f7f0adc..ee853919 100644 --- a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java @@ -1,6 +1,5 @@ package org.kde.kdeconnect.UserInterface; -import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -32,33 +31,6 @@ import java.nio.charset.Charset; public class SettingsFragment extends PreferenceFragmentCompat { - private static class LicenseTextSingleton { - private static volatile String licenseText; - - // Need these separate methods because Context is required to load the string - // Mixing Context and static here is fine because the license - // is a pure function of the project directory (i.e. doesn't depend on language, etc.) - - static synchronized String getOrLoadLicenseText(Context context) { - String licenseText = LicenseTextSingleton.licenseText; - if (licenseText != null) { - return licenseText; - } - try (InputStream is = context.getResources().openRawResource(R.raw.license)) { - licenseText = IOUtils.toString(is, Charset.defaultCharset()); - } catch (IOException ie) { - throw new RuntimeException(ie); - } - return LicenseTextSingleton.licenseText = licenseText; - } - - static synchronized void startLoadingLicenseText(Context context) { - if (licenseText == null) { - new Thread(() -> getOrLoadLicenseText(context)).start(); - } - } - } - private EditTextPreference renameDevice; @Override @@ -187,40 +159,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { moreSettingsText.setSummary(R.string.settings_more_settings_text); screen.addPreference(moreSettingsText); - Preference bug = new Preference(context); - bug.setTitle(R.string.report_bug); - bug.setPersistent(false); - bug.setIcon(R.drawable.ic_baseline_bug_report_24); - bug.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.report_bug_url)))); - screen.addPreference(bug); - - Preference donate = new Preference(context); - donate.setTitle(R.string.donate); - donate.setPersistent(false); - donate.setIcon(R.drawable.ic_baseline_attach_money_24); - donate.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.donate_url)))); - screen.addPreference(donate); - - Preference source = new Preference(context); - source.setTitle(R.string.source_code); - source.setPersistent(false); - source.setIcon(R.drawable.ic_baseline_code_24); - source.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_code_url)))); - screen.addPreference(source); - - Preference licences = new Preference(context); - licences.setTitle(R.string.licenses); - licences.setPersistent(false); - licences.setIcon(R.drawable.ic_baseline_gavel_24); - licences.setOnPreferenceClickListener(preference -> { - new AlertDialog.Builder(context) - .setMessage(LicenseTextSingleton.getOrLoadLicenseText(context)) - .create().show(); - return true; - }); - LicenseTextSingleton.startLoadingLicenseText(context); - screen.addPreference(licences); - setPreferenceScreen(screen);