About page fixed (#3)

* Canon CR3 support
Small UI fixes

* About page fixed
This commit is contained in:
Ilia
2023-06-14 12:44:21 +04:00
committed by GitHub
parent 1f9ccbdc76
commit c9f81cbd4a
636 changed files with 89869 additions and 4 deletions

View File

@ -13,10 +13,10 @@ android {
compileSdkVersion 33
defaultConfig {
applicationId "com.simplemobiletools.gallery.pro"
applicationId "com.gallery.raw"
minSdkVersion 24
targetSdkVersion 33
versionCode 388
versionCode 389
versionName "6.26.7"
setProperty("archivesBaseName", "gallery-$versionCode")
vectorDrawables.useSupportLibrary = true
@ -84,7 +84,7 @@ android {
}
dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:925e7f9bd7'
// implementation 'com.github.SimpleMobileTools:Simple-Commons:925e7f9bd7'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.24'
@ -106,6 +106,7 @@ dependencies {
}
compileOnly 'com.squareup.okhttp3:okhttp:4.9.0'
implementation project(path: ':libraw')
implementation project(path: ':commons')
kapt 'com.github.bumptech.glide:compiler:4.13.2'
kapt 'androidx.room:room-compiler:2.4.3'

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.simplemobiletools.gallery.pro"
package="com.simplemobiletools.gallery.raw"
android:installLocation="auto">
<application

View File

@ -33,6 +33,12 @@ buildscript {
}
}
ext {
groupId = 'com.homesoft.android'
artifactId = 'libraw'
artifactVersion = '2.0.4'
}
allprojects {
repositories {
google()
@ -47,3 +53,5 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}

1
commons/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

53
commons/build.gradle Normal file
View File

@ -0,0 +1,53 @@
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlin-kapt"
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 24
targetSdkVersion 33
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
consumerProguardFiles "proguard-rules.pro"
}
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
}
}
dependencies {
implementation "com.andrognito.patternlockview:patternlockview:1.0.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.documentfile:documentfile:1.0.1"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.exifinterface:exifinterface:1.3.3"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
implementation "com.googlecode.ez-vcard:ez-vcard:0.11.3"
api "joda-time:joda-time:2.11.0"
api "com.github.tibbi:RecyclerView-FastScroller:5a95285b1f"
api "com.github.tibbi:reprint:2cb206415d"
api "androidx.core:core-ktx:1.8.0"
api "androidx.appcompat:appcompat:1.5.0"
api "com.google.android.material:material:1.8.0"
api "com.google.code.gson:gson:2.9.1"
api "com.duolingo.open:rtl-viewpager:2.0.0"
api "com.github.bumptech.glide:glide:4.13.2"
kapt "com.github.bumptech.glide:compiler:4.13.2"
annotationProcessor "com.github.bumptech.glide:compiler:4.13.2"
api "androidx.room:room-runtime:2.4.3"
kapt "androidx.room:room-compiler:2.4.3"
annotationProcessor "androidx.room:room-compiler:2.4.3"
}

26
commons/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,26 @@
-renamesourcefileattribute SourceFile
-keepattributes SourceFile, LineNumberTable
-dontwarn com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
-dontwarn com.bumptech.glide.load.resource.bitmap.Downsampler
-dontwarn com.bumptech.glide.load.resource.bitmap.HardwareConfigState
-dontwarn com.bumptech.glide.manager.RequestManagerRetriever
-keep public class * extends java.lang.Exception
-keep class android.support.v7.widget.SearchView { *; }
-keep class com.simplemobiletools.commons.models.PhoneNumber { *; }
# Joda
-dontwarn org.joda.convert.**
-dontwarn org.joda.time.**
-keep class org.joda.time.** { *; }
-keep interface org.joda.time.** { *; }
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.simplemobiletools.commons">
<queries>
<package android:name="com.simplemobiletools.thankyou.debug" />
<package android:name="com.simplemobiletools.thankyou" />
</queries>
<uses-feature
android:name="android.hardware.faketouch"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-permission
android:name="android.permission.USE_BIOMETRIC"
tools:node="remove" />
<!-- used by Reprint -->
<uses-permission
android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"
tools:node="remove" />
<application android:supportsRtl="true">
<activity
android:name="com.simplemobiletools.commons.activities.ContributorsActivity"
android:configChanges="orientation"
android:exported="false"
android:label="@string/contributors"
android:parentActivityName="com.simplemobiletools.commons.activities.AboutActivity" />
<activity
android:name="com.simplemobiletools.commons.activities.FAQActivity"
android:configChanges="orientation"
android:exported="false"
android:label="@string/frequently_asked_questions"
android:parentActivityName="com.simplemobiletools.commons.activities.AboutActivity" />
<activity
android:name="com.simplemobiletools.commons.activities.LicenseActivity"
android:configChanges="orientation"
android:exported="false"
android:label="@string/third_party_licences"
android:parentActivityName="com.simplemobiletools.commons.activities.AboutActivity" />
<receiver
android:name=".receivers.SharedThemeReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.simplemobiletools.commons.SHARED_THEME_ACTIVATED" />
<action android:name="com.simplemobiletools.commons.SHARED_THEME_UPDATED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -0,0 +1,431 @@
package com.simplemobiletools.commons.activities
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.Intent.*
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import androidx.core.net.toUri
import androidx.core.view.isEmpty
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog
import com.simplemobiletools.commons.dialogs.RateStarsDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FAQItem
import kotlinx.android.synthetic.main.activity_about.*
import kotlinx.android.synthetic.main.item_about.view.*
class AboutActivity : BaseSimpleActivity() {
private var appName = ""
private var primaryColor = 0
private var textColor = 0
private var backgroundColor = 0
private var inflater: LayoutInflater? = null
private var firstVersionClickTS = 0L
private var clicksSinceFirstClick = 0
private val EASTER_EGG_TIME_LIMIT = 3000L
private val EASTER_EGG_REQUIRED_CLICKS = 7
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
primaryColor = getProperPrimaryColor()
textColor = getProperTextColor()
backgroundColor = getProperBackgroundColor()
inflater = LayoutInflater.from(this)
updateMaterialActivityViews(about_coordinator, about_holder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(about_nested_scrollview, about_toolbar)
appName = intent.getStringExtra(APP_NAME) ?: ""
}
override fun onResume() {
super.onResume()
updateTextColors(about_nested_scrollview)
setupToolbar(about_toolbar, NavigationIcon.Arrow)
about_support_layout.removeAllViews()
about_social_layout.removeAllViews()
about_other_layout.removeAllViews()
setupEmail()
setupRateUs()
setupInvite()
setupContributors()
setupDonate()
setupGitHub()
setupReddit()
setupTelegram()
setupMoreApps()
setupWebsite()
setupPrivacyPolicy()
setupLicense()
setupVersion()
}
private fun setupFAQ() {
val faqItems = intent.getSerializableExtra(APP_FAQ) as ArrayList<FAQItem>
if (faqItems.isNotEmpty()) {
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_question_mark_vector, R.string.frequently_asked_questions)
about_support_layout.addView(this)
setOnClickListener {
launchFAQActivity()
}
}
}
}
private fun launchFAQActivity() {
val faqItems = intent.getSerializableExtra(APP_FAQ) as ArrayList<FAQItem>
Intent(applicationContext, FAQActivity::class.java).apply {
putExtra(APP_ICON_IDS, getAppIconIDs())
putExtra(APP_LAUNCHER_NAME, getAppLauncherName())
putExtra(APP_FAQ, faqItems)
startActivity(this)
}
}
private fun setupEmail() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
if (about_support_layout.isEmpty()) {
about_support.beGone()
about_support_divider.beGone()
}
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_mail_vector, R.string.my_email)
about_support_layout.addView(this)
setOnClickListener {
val msg = "${getString(R.string.before_asking_question_read_faq)}\n\n${getString(R.string.make_sure_latest)}"
if (intent.getBooleanExtra(SHOW_FAQ_BEFORE_MAIL, false) && !baseConfig.wasBeforeAskingShown) {
baseConfig.wasBeforeAskingShown = true
ConfirmationAdvancedDialog(this@AboutActivity, msg, 0, R.string.read_faq, R.string.skip) { success ->
if (success) {
launchFAQActivity()
} else {
launchEmailIntent()
}
}
} else {
launchEmailIntent()
}
}
}
}
private fun launchEmailIntent() {
val appVersion = String.format(getString(R.string.app_version, intent.getStringExtra(APP_VERSION_NAME)))
val deviceOS = String.format(getString(R.string.device_os), Build.VERSION.RELEASE)
val newline = "\n"
val separator = "------------------------------"
val body = "$appVersion$newline$deviceOS$newline$separator$newline$newline"
val address = if (packageName.startsWith("com.simplemobiletools")) {
getString(R.string.my_email)
} else {
getString(R.string.my_fake_email)
}
val selectorIntent = Intent(ACTION_SENDTO)
.setData("mailto:$address".toUri())
val emailIntent = Intent(ACTION_SEND).apply {
putExtra(EXTRA_EMAIL, arrayOf(address))
putExtra(EXTRA_SUBJECT, appName)
putExtra(EXTRA_TEXT, body)
selector = selectorIntent
}
try {
startActivity(emailIntent)
} catch (e: ActivityNotFoundException) {
val chooser = createChooser(emailIntent, getString(R.string.send_email))
try {
startActivity(chooser)
} catch (e: Exception) {
toast(R.string.no_email_client_found)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
private fun setupRateUs() {
if (resources.getBoolean(R.bool.hide_google_relations) || resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_star_vector, R.string.rate_us)
setOnClickListener {
if (baseConfig.wasBeforeRateShown) {
launchRateUsPrompt()
} else {
baseConfig.wasBeforeRateShown = true
val msg = "${getString(R.string.before_rate_read_faq)}\n\n${getString(R.string.make_sure_latest)}"
ConfirmationAdvancedDialog(this@AboutActivity, msg, 0, R.string.read_faq, R.string.skip) { success ->
if (success) {
launchFAQActivity()
} else {
launchRateUsPrompt()
}
}
}
}
}
}
private fun launchRateUsPrompt() {
if (baseConfig.wasAppRated) {
redirectToRateUs()
} else {
RateStarsDialog(this@AboutActivity)
}
}
private fun setupInvite() {
if (resources.getBoolean(R.bool.hide_google_relations) || resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_add_person_vector, R.string.invite_friends)
setOnClickListener {
val text = String.format(getString(R.string.share_text), appName, getStoreUrl())
Intent().apply {
action = ACTION_SEND
putExtra(EXTRA_SUBJECT, appName)
putExtra(EXTRA_TEXT, text)
type = "text/plain"
startActivity(createChooser(this, getString(R.string.invite_via)))
}
}
}
}
private fun setupContributors() {
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_face_vector, R.string.contributors)
setOnClickListener {
val intent = Intent(applicationContext, ContributorsActivity::class.java)
startActivity(intent)
}
}
}
private fun setupDonate() {
if (resources.getBoolean(R.bool.show_donate_in_about) && !resources.getBoolean(R.bool.hide_all_external_links)) {
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_dollar_vector, R.string.donate)
setOnClickListener {
launchViewIntent(getString(R.string.donate_url))
}
}
}
}
private fun setupFacebook() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
about_item_icon.setImageResource(R.drawable.ic_facebook_vector)
about_item_label.setText(R.string.facebook)
about_item_label.setTextColor(textColor)
about_social_layout.addView(this)
setOnClickListener {
var link = "https://www.facebook.com/simplemobiletools"
try {
packageManager.getPackageInfo("com.facebook.katana", 0)
link = "fb://page/150270895341774"
} catch (ignored: Exception) {
}
launchViewIntent(link)
}
}
}
private fun setupGitHub() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
about_item_icon.setImageDrawable(resources.getColoredDrawableWithColor(R.drawable.ic_github_vector, backgroundColor.getContrastColor()))
about_item_label.setText(R.string.github)
about_item_label.setTextColor(textColor)
about_social_layout.addView(this)
setOnClickListener {
launchViewIntent("https://github.com/RikardoMexican/GalleryRAW")
}
}
}
private fun setupReddit() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
about_item_icon.setImageResource(R.drawable.ic_reddit_vector)
about_item_label.setText(R.string.reddit)
about_item_label.setTextColor(textColor)
about_social_layout.addView(this)
setOnClickListener {
launchViewIntent("https://www.reddit.com/r/GalleryRAW/")
}
}
}
private fun setupTelegram() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
if (about_social_layout.isEmpty()) {
about_social.beGone()
about_social_divider.beGone()
}
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
about_item_icon.setImageResource(R.drawable.ic_telegram_vector)
about_item_label.setText(R.string.telegram)
about_item_label.setTextColor(textColor)
about_social_layout.addView(this)
setOnClickListener {
launchViewIntent("https://t.me/GalleryRAW")
}
}
}
private fun setupMoreApps() {
if (resources.getBoolean(R.bool.hide_google_relations)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_heart_vector, R.string.more_apps_from_us)
about_other_layout.addView(this)
setOnClickListener {
launchMoreAppsFromUsIntent()
}
}
}
private fun setupWebsite() {
if (!resources.getBoolean(R.bool.show_donate_in_about) || resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_link_vector, R.string.website)
about_other_layout.addView(this)
setOnClickListener {
launchViewIntent("https://gallery-raw.webflow.io/")
}
}
}
private fun setupPrivacyPolicy() {
if (resources.getBoolean(R.bool.hide_all_external_links)) {
return
}
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_unhide_vector, R.string.privacy_policy)
about_other_layout.addView(this)
setOnClickListener {
val appId = baseConfig.appId.removeSuffix(".debug").removeSuffix(".pro").removePrefix("com.simplemobiletools.")
val url = "https://simplemobiletools.com/privacy/$appId.txt"
launchViewIntent(url)
}
}
}
private fun setupLicense() {
inflater?.inflate(R.layout.item_about, null)?.apply {
setupAboutItem(this, R.drawable.ic_article_vector, R.string.third_party_licences)
about_other_layout.addView(this)
setOnClickListener {
Intent(applicationContext, LicenseActivity::class.java).apply {
putExtra(APP_ICON_IDS, getAppIconIDs())
putExtra(APP_LAUNCHER_NAME, getAppLauncherName())
putExtra(APP_LICENSES, intent.getLongExtra(APP_LICENSES, 0))
startActivity(this)
}
}
}
}
private fun setupVersion() {
var version = intent.getStringExtra(APP_VERSION_NAME) ?: ""
if (baseConfig.appId.removeSuffix(".debug").endsWith(".pro")) {
version += " ${getString(R.string.pro)}"
}
inflater?.inflate(R.layout.item_about, null)?.apply {
about_item_icon.setImageDrawable(resources.getColoredDrawableWithColor(R.drawable.ic_info_vector, textColor))
val fullVersion = String.format(getString(R.string.version_placeholder, version))
about_item_label.text = fullVersion
about_item_label.setTextColor(textColor)
about_other_layout.addView(this)
setOnClickListener {
if (firstVersionClickTS == 0L) {
firstVersionClickTS = System.currentTimeMillis()
Handler().postDelayed({
firstVersionClickTS = 0L
clicksSinceFirstClick = 0
}, EASTER_EGG_TIME_LIMIT)
}
clicksSinceFirstClick++
if (clicksSinceFirstClick >= EASTER_EGG_REQUIRED_CLICKS) {
toast(R.string.hello)
firstVersionClickTS = 0L
clicksSinceFirstClick = 0
}
}
}
}
private fun setupAboutItem(view: View, drawableId: Int, textId: Int) {
view.apply {
about_item_icon.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, textColor))
about_item_label.setText(textId)
about_item_label.setTextColor(textColor)
}
}
}

View File

@ -0,0 +1,59 @@
package com.simplemobiletools.commons.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SIDELOADING_TRUE
import com.simplemobiletools.commons.helpers.SIDELOADING_UNCHECKED
abstract class BaseSplashActivity : AppCompatActivity() {
abstract fun initActivity()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (baseConfig.appSideloadingStatus == SIDELOADING_UNCHECKED) {
if (checkAppSideloading()) {
return
}
} else if (baseConfig.appSideloadingStatus == SIDELOADING_TRUE) {
showSideloadingDialog()
return
}
baseConfig.apply {
if (isUsingAutoTheme) {
val isUsingSystemDarkTheme = isUsingSystemDarkTheme()
isUsingSharedTheme = false
textColor = resources.getColor(if (isUsingSystemDarkTheme) R.color.theme_dark_text_color else R.color.theme_light_text_color)
backgroundColor = resources.getColor(if (isUsingSystemDarkTheme) R.color.theme_dark_background_color else R.color.theme_light_background_color)
}
}
if (!baseConfig.isUsingAutoTheme && !baseConfig.isUsingSystemTheme && isThankYouInstalled()) {
getSharedTheme {
if (it != null) {
baseConfig.apply {
wasSharedThemeForced = true
isUsingSharedTheme = true
wasSharedThemeEverActivated = true
textColor = it.textColor
backgroundColor = it.backgroundColor
primaryColor = it.primaryColor
accentColor = it.accentColor
}
if (baseConfig.appIconColor != it.appIconColor) {
baseConfig.appIconColor = it.appIconColor
checkAppIconColor()
}
}
initActivity()
}
} else {
initActivity()
}
}
}

View File

@ -0,0 +1,119 @@
package com.simplemobiletools.commons.activities
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.APP_ICON_IDS
import com.simplemobiletools.commons.helpers.APP_LAUNCHER_NAME
import com.simplemobiletools.commons.helpers.NavigationIcon
import com.simplemobiletools.commons.models.LanguageContributor
import kotlinx.android.synthetic.main.activity_contributors.*
import kotlinx.android.synthetic.main.item_language_contributor.view.*
class ContributorsActivity : BaseSimpleActivity() {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contributors)
updateTextColors(contributors_holder)
updateMaterialActivityViews(contributors_coordinator, contributors_holder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(contributors_nested_scrollview, contributors_toolbar)
val primaryColor = getProperPrimaryColor()
contributors_development_label.setTextColor(primaryColor)
contributors_translation_label.setTextColor(primaryColor)
val inflater = LayoutInflater.from(this)
val languages = arrayListOf<LanguageContributor>()
languages.addAll(
arrayListOf(
LanguageContributor(R.drawable.ic_flag_arabic_vector, R.string.translation_arabic, R.string.translators_arabic),
LanguageContributor(R.drawable.ic_flag_azerbaijani_vector, R.string.translation_azerbaijani, R.string.translators_azerbaijani),
LanguageContributor(R.drawable.ic_flag_bengali_vector, R.string.translation_bengali, R.string.translators_bengali),
LanguageContributor(R.drawable.ic_flag_catalan_vector, R.string.translation_catalan, R.string.translators_catalan),
LanguageContributor(R.drawable.ic_flag_czech_vector, R.string.translation_czech, R.string.translators_czech),
LanguageContributor(R.drawable.ic_flag_welsh_vector, R.string.translation_welsh, R.string.translators_welsh),
LanguageContributor(R.drawable.ic_flag_danish_vector, R.string.translation_danish, R.string.translators_danish),
LanguageContributor(R.drawable.ic_flag_german_vector, R.string.translation_german, R.string.translators_german),
LanguageContributor(R.drawable.ic_flag_greek_vector, R.string.translation_greek, R.string.translators_greek),
LanguageContributor(R.drawable.ic_flag_spanish_vector, R.string.translation_spanish, R.string.translators_spanish),
LanguageContributor(R.drawable.ic_flag_basque_vector, R.string.translation_basque, R.string.translators_basque),
LanguageContributor(R.drawable.ic_flag_persian_vector, R.string.translation_persian, R.string.translators_persian),
LanguageContributor(R.drawable.ic_flag_finnish_vector, R.string.translation_finnish, R.string.translators_finnish),
LanguageContributor(R.drawable.ic_flag_french_vector, R.string.translation_french, R.string.translators_french),
LanguageContributor(R.drawable.ic_flag_galician_vector, R.string.translation_galician, R.string.translators_galician),
LanguageContributor(R.drawable.ic_flag_hindi_vector, R.string.translation_hindi, R.string.translators_hindi),
LanguageContributor(R.drawable.ic_flag_croatian_vector, R.string.translation_croatian, R.string.translators_croatian),
LanguageContributor(R.drawable.ic_flag_hungarian_vector, R.string.translation_hungarian, R.string.translators_hungarian),
LanguageContributor(R.drawable.ic_flag_indonesian_vector, R.string.translation_indonesian, R.string.translators_indonesian),
LanguageContributor(R.drawable.ic_flag_italian_vector, R.string.translation_italian, R.string.translators_italian),
LanguageContributor(R.drawable.ic_flag_hebrew_vector, R.string.translation_hebrew, R.string.translators_hebrew),
LanguageContributor(R.drawable.ic_flag_japanese_vector, R.string.translation_japanese, R.string.translators_japanese),
LanguageContributor(R.drawable.ic_flag_korean_vector, R.string.translation_korean, R.string.translators_korean),
LanguageContributor(R.drawable.ic_flag_lithuanian_vector, R.string.translation_lithuanian, R.string.translators_lithuanian),
LanguageContributor(R.drawable.ic_flag_nepali_vector, R.string.translation_nepali, R.string.translators_nepali),
LanguageContributor(R.drawable.ic_flag_norwegian_vector, R.string.translation_norwegian, R.string.translators_norwegian),
LanguageContributor(R.drawable.ic_flag_dutch_vector, R.string.translation_dutch, R.string.translators_dutch),
LanguageContributor(R.drawable.ic_flag_polish_vector, R.string.translation_polish, R.string.translators_polish),
LanguageContributor(R.drawable.ic_flag_portuguese_vector, R.string.translation_portuguese, R.string.translators_portuguese),
LanguageContributor(R.drawable.ic_flag_romanian_vector, R.string.translation_romanian, R.string.translators_romanian),
LanguageContributor(R.drawable.ic_flag_russian_vector, R.string.translation_russian, R.string.translators_russian),
LanguageContributor(R.drawable.ic_flag_slovak_vector, R.string.translation_slovak, R.string.translators_slovak),
LanguageContributor(R.drawable.ic_flag_slovenian_vector, R.string.translation_slovenian, R.string.translators_slovenian),
LanguageContributor(R.drawable.ic_flag_swedish_vector, R.string.translation_swedish, R.string.translators_swedish),
LanguageContributor(R.drawable.ic_flag_tamil_vector, R.string.translation_tamil, R.string.translators_tamil),
LanguageContributor(R.drawable.ic_flag_turkish_vector, R.string.translation_turkish, R.string.translators_turkish),
LanguageContributor(R.drawable.ic_flag_ukrainian_vector, R.string.translation_ukrainian, R.string.translators_ukrainian),
LanguageContributor(R.drawable.ic_flag_chinese_hk_vector, R.string.translation_chinese_hk, R.string.translators_chinese_hk),
LanguageContributor(R.drawable.ic_flag_chinese_cn_vector, R.string.translation_chinese_cn, R.string.translators_chinese_cn),
LanguageContributor(R.drawable.ic_flag_chinese_tw_vector, R.string.translation_chinese_tw, R.string.translators_chinese_tw)
)
)
val textColor = getProperTextColor()
languages.forEach { language ->
inflater.inflate(R.layout.item_language_contributor, null).apply {
language_icon.setImageDrawable(getDrawable(language.iconId))
language_label.apply {
text = getString(language.labelId)
setTextColor(textColor)
}
language_contributors.apply {
text = getString(language.contributorsId)
setTextColor(textColor)
}
contributors_languages_holder.addView(this)
}
}
contributors_label.apply {
setTextColor(textColor)
text = Html.fromHtml(getString(R.string.contributors_label))
setLinkTextColor(primaryColor)
movementMethod = LinkMovementMethod.getInstance()
removeUnderlines()
}
contributors_development_icon.applyColorFilter(textColor)
contributors_footer_icon.applyColorFilter(textColor)
if (resources.getBoolean(R.bool.hide_all_external_links)) {
contributors_footer_layout.beGone()
}
}
override fun onResume() {
super.onResume()
setupToolbar(contributors_toolbar, NavigationIcon.Arrow)
}
}

View File

@ -0,0 +1,664 @@
package com.simplemobiletools.commons.activities
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.os.Bundle
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.MyTheme
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.models.SharedTheme
import com.simplemobiletools.commons.views.MyTextView
import kotlinx.android.synthetic.main.activity_customization.*
class CustomizationActivity : BaseSimpleActivity() {
private val THEME_LIGHT = 0
private val THEME_DARK = 1
private val THEME_SOLARIZED = 2
private val THEME_DARK_RED = 3
private val THEME_BLACK_WHITE = 4
private val THEME_CUSTOM = 5
private val THEME_SHARED = 6
private val THEME_WHITE = 7
private val THEME_AUTO = 8
private val THEME_SYSTEM = 9 // Material You
private var curTextColor = 0
private var curBackgroundColor = 0
private var curPrimaryColor = 0
private var curAccentColor = 0
private var curAppIconColor = 0
private var curSelectedThemeId = 0
private var originalAppIconColor = 0
private var lastSavePromptTS = 0L
private var hasUnsavedChanges = false
private var isThankYou = false // show "Apply colors to all Simple apps" in Simple Thank You itself even with "Hide Google relations" enabled
private var predefinedThemes = LinkedHashMap<Int, MyTheme>()
private var curPrimaryLineColorPicker: LineColorPickerDialog? = null
private var storedSharedTheme: SharedTheme? = null
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_customization)
setupOptionsMenu()
refreshMenuItems()
updateMaterialActivityViews(customization_coordinator, customization_holder, useTransparentNavigation = true, useTopSearchMenu = false)
isThankYou = packageName.removeSuffix(".debug") == "com.simplemobiletools.thankyou"
initColorVariables()
if (isThankYouInstalled()) {
val cursorLoader = getMyContentProviderCursorLoader()
ensureBackgroundThread {
try {
storedSharedTheme = getSharedThemeSync(cursorLoader)
if (storedSharedTheme == null) {
baseConfig.isUsingSharedTheme = false
} else {
baseConfig.wasSharedThemeEverActivated = true
}
runOnUiThread {
setupThemes()
val hideGoogleRelations = resources.getBoolean(R.bool.hide_google_relations) && !isThankYou
apply_to_all_holder.beVisibleIf(
storedSharedTheme == null && curSelectedThemeId != THEME_AUTO && curSelectedThemeId != THEME_SYSTEM && !hideGoogleRelations
)
}
} catch (e: Exception) {
toast(R.string.update_thank_you)
finish()
}
}
} else {
setupThemes()
baseConfig.isUsingSharedTheme = false
}
val textColor = if (baseConfig.isUsingSystemTheme) {
getProperTextColor()
} else {
baseConfig.textColor
}
updateLabelColors(textColor)
originalAppIconColor = baseConfig.appIconColor
if (resources.getBoolean(R.bool.hide_google_relations) && !isThankYou) {
apply_to_all_holder.beGone()
}
}
override fun onResume() {
super.onResume()
setTheme(getThemeId(getCurrentPrimaryColor()))
if (!baseConfig.isUsingSystemTheme) {
updateBackgroundColor(getCurrentBackgroundColor())
updateActionbarColor(getCurrentStatusBarColor())
}
curPrimaryLineColorPicker?.getSpecificColor()?.apply {
updateActionbarColor(this)
setTheme(getThemeId(this))
}
setupToolbar(customization_toolbar, NavigationIcon.Cross, getColoredMaterialStatusBarColor())
}
private fun refreshMenuItems() {
customization_toolbar.menu.findItem(R.id.save).isVisible = hasUnsavedChanges
}
private fun setupOptionsMenu() {
customization_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.save -> {
saveChanges(true)
true
}
else -> false
}
}
}
override fun onBackPressed() {
if (hasUnsavedChanges && System.currentTimeMillis() - lastSavePromptTS > SAVE_DISCARD_PROMPT_INTERVAL) {
promptSaveDiscard()
} else {
super.onBackPressed()
}
}
private fun setupThemes() {
predefinedThemes.apply {
if (isSPlus()) {
put(THEME_SYSTEM, getSystemThemeColors())
}
put(THEME_AUTO, getAutoThemeColors())
put(
THEME_LIGHT,
MyTheme(
getString(R.string.light_theme),
R.color.theme_light_text_color,
R.color.theme_light_background_color,
R.color.color_primary,
R.color.color_primary
)
)
put(
THEME_DARK,
MyTheme(
getString(R.string.dark_theme),
R.color.theme_dark_text_color,
R.color.theme_dark_background_color,
R.color.color_primary,
R.color.color_primary
)
)
put(
THEME_DARK_RED,
MyTheme(
getString(R.string.dark_red),
R.color.theme_dark_text_color,
R.color.theme_dark_background_color,
R.color.theme_dark_red_primary_color,
R.color.md_red_700
)
)
put(THEME_WHITE, MyTheme(getString(R.string.white), R.color.dark_grey, android.R.color.white, android.R.color.white, R.color.color_primary))
put(
THEME_BLACK_WHITE,
MyTheme(getString(R.string.black_white), android.R.color.white, android.R.color.black, android.R.color.black, R.color.md_grey_black)
)
put(THEME_CUSTOM, MyTheme(getString(R.string.custom), 0, 0, 0, 0))
if (storedSharedTheme != null) {
put(THEME_SHARED, MyTheme(getString(R.string.shared), 0, 0, 0, 0))
}
}
setupThemePicker()
setupColorsPickers()
}
private fun setupThemePicker() {
curSelectedThemeId = getCurrentThemeId()
customization_theme.text = getThemeText()
updateAutoThemeFields()
handleAccentColorLayout()
customization_theme_holder.setOnClickListener {
if (baseConfig.wasAppIconCustomizationWarningShown) {
themePickerClicked()
} else {
ConfirmationDialog(this, "", R.string.app_icon_color_warning, R.string.ok, 0) {
baseConfig.wasAppIconCustomizationWarningShown = true
themePickerClicked()
}
}
}
if (customization_theme.value == getMaterialYouString()) {
apply_to_all_holder.beGone()
}
}
private fun themePickerClicked() {
val items = arrayListOf<RadioItem>()
for ((key, value) in predefinedThemes) {
items.add(RadioItem(key, value.label))
}
RadioGroupDialog(this@CustomizationActivity, items, curSelectedThemeId) {
if (it == THEME_SHARED && !isThankYouInstalled()) {
PurchaseThankYouDialog(this)
return@RadioGroupDialog
}
updateColorTheme(it as Int, true)
if (it != THEME_CUSTOM && it != THEME_SHARED && it != THEME_AUTO && it != THEME_SYSTEM && !baseConfig.wasCustomThemeSwitchDescriptionShown) {
baseConfig.wasCustomThemeSwitchDescriptionShown = true
toast(R.string.changing_color_description)
}
val hideGoogleRelations = resources.getBoolean(R.bool.hide_google_relations) && !isThankYou
apply_to_all_holder.beVisibleIf(
curSelectedThemeId != THEME_AUTO && curSelectedThemeId != THEME_SYSTEM && curSelectedThemeId != THEME_SHARED && !hideGoogleRelations
)
updateMenuItemColors(customization_toolbar.menu, getCurrentStatusBarColor())
setupToolbar(customization_toolbar, NavigationIcon.Cross, getCurrentStatusBarColor())
}
}
private fun updateColorTheme(themeId: Int, useStored: Boolean = false) {
curSelectedThemeId = themeId
customization_theme.text = getThemeText()
resources.apply {
if (curSelectedThemeId == THEME_CUSTOM) {
if (useStored) {
curTextColor = baseConfig.customTextColor
curBackgroundColor = baseConfig.customBackgroundColor
curPrimaryColor = baseConfig.customPrimaryColor
curAccentColor = baseConfig.customAccentColor
curAppIconColor = baseConfig.customAppIconColor
setTheme(getThemeId(curPrimaryColor))
updateMenuItemColors(customization_toolbar.menu, curPrimaryColor)
setupToolbar(customization_toolbar, NavigationIcon.Cross, curPrimaryColor)
setupColorsPickers()
} else {
baseConfig.customPrimaryColor = curPrimaryColor
baseConfig.customAccentColor = curAccentColor
baseConfig.customBackgroundColor = curBackgroundColor
baseConfig.customTextColor = curTextColor
baseConfig.customAppIconColor = curAppIconColor
}
} else if (curSelectedThemeId == THEME_SHARED) {
if (useStored) {
storedSharedTheme?.apply {
curTextColor = textColor
curBackgroundColor = backgroundColor
curPrimaryColor = primaryColor
curAccentColor = accentColor
curAppIconColor = appIconColor
}
setTheme(getThemeId(curPrimaryColor))
setupColorsPickers()
updateMenuItemColors(customization_toolbar.menu, curPrimaryColor)
setupToolbar(customization_toolbar, NavigationIcon.Cross, curPrimaryColor)
}
} else {
val theme = predefinedThemes[curSelectedThemeId]!!
curTextColor = getColor(theme.textColorId)
curBackgroundColor = getColor(theme.backgroundColorId)
if (curSelectedThemeId != THEME_AUTO && curSelectedThemeId != THEME_SYSTEM) {
curPrimaryColor = getColor(theme.primaryColorId)
curAccentColor = getColor(R.color.color_primary)
curAppIconColor = getColor(theme.appIconColorId)
}
setTheme(getThemeId(getCurrentPrimaryColor()))
colorChanged()
updateMenuItemColors(customization_toolbar.menu, getCurrentStatusBarColor())
setupToolbar(customization_toolbar, NavigationIcon.Cross, getCurrentStatusBarColor())
}
}
hasUnsavedChanges = true
refreshMenuItems()
updateLabelColors(getCurrentTextColor())
updateBackgroundColor(getCurrentBackgroundColor())
updateActionbarColor(getCurrentStatusBarColor())
updateAutoThemeFields()
updateApplyToAllColors(getCurrentPrimaryColor())
handleAccentColorLayout()
}
private fun getAutoThemeColors(): MyTheme {
val isUsingSystemDarkTheme = isUsingSystemDarkTheme()
val textColor = if (isUsingSystemDarkTheme) R.color.theme_dark_text_color else R.color.theme_light_text_color
val backgroundColor = if (isUsingSystemDarkTheme) R.color.theme_dark_background_color else R.color.theme_light_background_color
return MyTheme(getString(R.string.auto_light_dark_theme), textColor, backgroundColor, R.color.color_primary, R.color.color_primary)
}
// doesn't really matter what colors we use here, everything will be taken from the system. Use the default dark theme values here.
private fun getSystemThemeColors(): MyTheme {
return MyTheme(
getMaterialYouString(),
R.color.theme_dark_text_color,
R.color.theme_dark_background_color,
R.color.color_primary,
R.color.color_primary
)
}
private fun getCurrentThemeId(): Int {
if (baseConfig.isUsingSharedTheme) {
return THEME_SHARED
} else if ((baseConfig.isUsingSystemTheme && !hasUnsavedChanges) || curSelectedThemeId == THEME_SYSTEM) {
return THEME_SYSTEM
} else if (baseConfig.isUsingAutoTheme || curSelectedThemeId == THEME_AUTO) {
return THEME_AUTO
}
var themeId = THEME_CUSTOM
resources.apply {
for ((key, value) in predefinedThemes.filter { it.key != THEME_CUSTOM && it.key != THEME_SHARED && it.key != THEME_AUTO && it.key != THEME_SYSTEM }) {
if (curTextColor == getColor(value.textColorId) &&
curBackgroundColor == getColor(value.backgroundColorId) &&
curPrimaryColor == getColor(value.primaryColorId) &&
curAppIconColor == getColor(value.appIconColorId)
) {
themeId = key
}
}
}
return themeId
}
private fun getThemeText(): String {
var label = getString(R.string.custom)
for ((key, value) in predefinedThemes) {
if (key == curSelectedThemeId) {
label = value.label
}
}
return label
}
private fun updateAutoThemeFields() {
arrayOf(customization_text_color_holder, customization_background_color_holder).forEach {
it.beVisibleIf(curSelectedThemeId != THEME_AUTO && curSelectedThemeId != THEME_SYSTEM)
}
customization_primary_color_holder.beVisibleIf(curSelectedThemeId != THEME_SYSTEM)
}
private fun promptSaveDiscard() {
lastSavePromptTS = System.currentTimeMillis()
ConfirmationAdvancedDialog(this, "", R.string.save_before_closing, R.string.save, R.string.discard) {
if (it) {
saveChanges(true)
} else {
resetColors()
finish()
}
}
}
private fun saveChanges(finishAfterSave: Boolean) {
val didAppIconColorChange = curAppIconColor != originalAppIconColor
baseConfig.apply {
textColor = curTextColor
backgroundColor = curBackgroundColor
primaryColor = curPrimaryColor
accentColor = curAccentColor
appIconColor = curAppIconColor
}
if (didAppIconColorChange) {
checkAppIconColor()
}
if (curSelectedThemeId == THEME_SHARED) {
val newSharedTheme = SharedTheme(curTextColor, curBackgroundColor, curPrimaryColor, curAppIconColor, 0, curAccentColor)
updateSharedTheme(newSharedTheme)
Intent().apply {
action = MyContentProvider.SHARED_THEME_UPDATED
sendBroadcast(this)
}
}
baseConfig.isUsingSharedTheme = curSelectedThemeId == THEME_SHARED
baseConfig.shouldUseSharedTheme = curSelectedThemeId == THEME_SHARED
baseConfig.isUsingAutoTheme = curSelectedThemeId == THEME_AUTO
baseConfig.isUsingSystemTheme = curSelectedThemeId == THEME_SYSTEM
hasUnsavedChanges = false
if (finishAfterSave) {
finish()
} else {
refreshMenuItems()
}
}
private fun resetColors() {
hasUnsavedChanges = false
initColorVariables()
setupColorsPickers()
updateBackgroundColor()
updateActionbarColor()
refreshMenuItems()
updateLabelColors(getCurrentTextColor())
}
private fun initColorVariables() {
curTextColor = baseConfig.textColor
curBackgroundColor = baseConfig.backgroundColor
curPrimaryColor = baseConfig.primaryColor
curAccentColor = baseConfig.accentColor
curAppIconColor = baseConfig.appIconColor
}
private fun setupColorsPickers() {
val textColor = getCurrentTextColor()
val backgroundColor = getCurrentBackgroundColor()
val primaryColor = getCurrentPrimaryColor()
customization_text_color.setFillWithStroke(textColor, backgroundColor)
customization_primary_color.setFillWithStroke(primaryColor, backgroundColor)
customization_accent_color.setFillWithStroke(curAccentColor, backgroundColor)
customization_background_color.setFillWithStroke(backgroundColor, backgroundColor)
customization_app_icon_color.setFillWithStroke(curAppIconColor, backgroundColor)
apply_to_all.setTextColor(primaryColor.getContrastColor())
customization_text_color_holder.setOnClickListener { pickTextColor() }
customization_background_color_holder.setOnClickListener { pickBackgroundColor() }
customization_primary_color_holder.setOnClickListener { pickPrimaryColor() }
customization_accent_color_holder.setOnClickListener { pickAccentColor() }
handleAccentColorLayout()
apply_to_all.setOnClickListener {
applyToAll()
}
customization_app_icon_color_holder.setOnClickListener {
if (baseConfig.wasAppIconCustomizationWarningShown) {
pickAppIconColor()
} else {
ConfirmationDialog(this, "", R.string.app_icon_color_warning, R.string.ok, 0) {
baseConfig.wasAppIconCustomizationWarningShown = true
pickAppIconColor()
}
}
}
}
private fun hasColorChanged(old: Int, new: Int) = Math.abs(old - new) > 1
private fun colorChanged() {
hasUnsavedChanges = true
setupColorsPickers()
refreshMenuItems()
}
private fun setCurrentTextColor(color: Int) {
curTextColor = color
updateLabelColors(color)
}
private fun setCurrentBackgroundColor(color: Int) {
curBackgroundColor = color
updateBackgroundColor(color)
}
private fun setCurrentPrimaryColor(color: Int) {
curPrimaryColor = color
updateActionbarColor(color)
updateApplyToAllColors(color)
}
private fun updateApplyToAllColors(newColor: Int) {
if (newColor == baseConfig.primaryColor && !baseConfig.isUsingSystemTheme) {
apply_to_all.setBackgroundResource(R.drawable.button_background_rounded)
} else {
val applyBackground = resources.getDrawable(R.drawable.button_background_rounded, theme) as RippleDrawable
(applyBackground as LayerDrawable).findDrawableByLayerId(R.id.button_background_holder).applyColorFilter(newColor)
apply_to_all.background = applyBackground
}
}
private fun handleAccentColorLayout() {
customization_accent_color_holder.beVisibleIf(curSelectedThemeId == THEME_WHITE || isCurrentWhiteTheme() || curSelectedThemeId == THEME_BLACK_WHITE || isCurrentBlackAndWhiteTheme())
customization_accent_color_label.text = getString(
if (curSelectedThemeId == THEME_WHITE || isCurrentWhiteTheme()) {
R.string.accent_color_white
} else {
R.string.accent_color_black_and_white
}
)
}
private fun isCurrentWhiteTheme() = curTextColor == DARK_GREY && curPrimaryColor == Color.WHITE && curBackgroundColor == Color.WHITE
private fun isCurrentBlackAndWhiteTheme() = curTextColor == Color.WHITE && curPrimaryColor == Color.BLACK && curBackgroundColor == Color.BLACK
private fun pickTextColor() {
ColorPickerDialog(this, curTextColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
if (hasColorChanged(curTextColor, color)) {
setCurrentTextColor(color)
colorChanged()
updateColorTheme(getUpdatedTheme())
}
}
}
}
private fun pickBackgroundColor() {
ColorPickerDialog(this, curBackgroundColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
if (hasColorChanged(curBackgroundColor, color)) {
setCurrentBackgroundColor(color)
colorChanged()
updateColorTheme(getUpdatedTheme())
}
}
}
}
private fun pickPrimaryColor() {
if (!packageName.startsWith("com.simplemobiletools.", true) && baseConfig.appRunCount > 50) {
finish()
return
}
curPrimaryLineColorPicker = LineColorPickerDialog(this, curPrimaryColor, true, toolbar = customization_toolbar) { wasPositivePressed, color ->
curPrimaryLineColorPicker = null
if (wasPositivePressed) {
if (hasColorChanged(curPrimaryColor, color)) {
setCurrentPrimaryColor(color)
colorChanged()
updateColorTheme(getUpdatedTheme())
setTheme(getThemeId(color))
}
updateMenuItemColors(customization_toolbar.menu, color)
setupToolbar(customization_toolbar, NavigationIcon.Cross, color)
} else {
updateActionbarColor(curPrimaryColor)
setTheme(getThemeId(curPrimaryColor))
updateMenuItemColors(customization_toolbar.menu, curPrimaryColor)
setupToolbar(customization_toolbar, NavigationIcon.Cross, curPrimaryColor)
updateTopBarColors(customization_toolbar, curPrimaryColor)
}
}
}
private fun pickAccentColor() {
ColorPickerDialog(this, curAccentColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
if (hasColorChanged(curAccentColor, color)) {
curAccentColor = color
colorChanged()
if (isCurrentWhiteTheme() || isCurrentBlackAndWhiteTheme()) {
updateActionbarColor(getCurrentStatusBarColor())
}
}
}
}
}
private fun pickAppIconColor() {
LineColorPickerDialog(this, curAppIconColor, false, R.array.md_app_icon_colors, getAppIconIDs()) { wasPositivePressed, color ->
if (wasPositivePressed) {
if (hasColorChanged(curAppIconColor, color)) {
curAppIconColor = color
colorChanged()
updateColorTheme(getUpdatedTheme())
}
}
}
}
private fun getUpdatedTheme() = if (curSelectedThemeId == THEME_SHARED) THEME_SHARED else getCurrentThemeId()
private fun applyToAll() {
if (isThankYouInstalled()) {
ConfirmationDialog(this, "", R.string.share_colors_success, R.string.ok, 0) {
Intent().apply {
action = MyContentProvider.SHARED_THEME_ACTIVATED
sendBroadcast(this)
}
if (!predefinedThemes.containsKey(THEME_SHARED)) {
predefinedThemes[THEME_SHARED] = MyTheme(getString(R.string.shared), 0, 0, 0, 0)
}
baseConfig.wasSharedThemeEverActivated = true
apply_to_all_holder.beGone()
updateColorTheme(THEME_SHARED)
saveChanges(false)
}
} else {
PurchaseThankYouDialog(this)
}
}
private fun updateLabelColors(textColor: Int) {
arrayListOf<MyTextView>(
customization_theme_label,
customization_theme,
customization_text_color_label,
customization_background_color_label,
customization_primary_color_label,
customization_accent_color_label,
customization_app_icon_color_label
).forEach {
it.setTextColor(textColor)
}
val primaryColor = getCurrentPrimaryColor()
apply_to_all.setTextColor(primaryColor.getContrastColor())
updateApplyToAllColors(primaryColor)
}
private fun getCurrentTextColor() = if (customization_theme.value == getMaterialYouString()) {
resources.getColor(R.color.you_neutral_text_color)
} else {
curTextColor
}
private fun getCurrentBackgroundColor() = if (customization_theme.value == getMaterialYouString()) {
resources.getColor(R.color.you_background_color)
} else {
curBackgroundColor
}
private fun getCurrentPrimaryColor() = if (customization_theme.value == getMaterialYouString()) {
resources.getColor(R.color.you_primary_color)
} else {
curPrimaryColor
}
private fun getCurrentStatusBarColor() = if (customization_theme.value == getMaterialYouString()) {
resources.getColor(R.color.you_status_bar_color)
} else {
curPrimaryColor
}
private fun getMaterialYouString() = "${getString(R.string.system_default)} (${getString(R.string.material_you)})"
}

View File

@ -0,0 +1,66 @@
package com.simplemobiletools.commons.activities
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getProperBackgroundColor
import com.simplemobiletools.commons.extensions.getProperPrimaryColor
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.extensions.removeUnderlines
import com.simplemobiletools.commons.helpers.APP_FAQ
import com.simplemobiletools.commons.helpers.APP_ICON_IDS
import com.simplemobiletools.commons.helpers.APP_LAUNCHER_NAME
import com.simplemobiletools.commons.helpers.NavigationIcon
import com.simplemobiletools.commons.models.FAQItem
import kotlinx.android.synthetic.main.activity_faq.*
import kotlinx.android.synthetic.main.item_faq.view.*
class FAQActivity : BaseSimpleActivity() {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_faq)
updateMaterialActivityViews(faq_coordinator, faq_holder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(faq_nested_scrollview, faq_toolbar)
val textColor = getProperTextColor()
val backgroundColor = getProperBackgroundColor()
val primaryColor = getProperPrimaryColor()
val inflater = LayoutInflater.from(this)
val faqItems = intent.getSerializableExtra(APP_FAQ) as ArrayList<FAQItem>
faqItems.forEach {
val faqItem = it
inflater.inflate(R.layout.item_faq, null).apply {
faq_card.setCardBackgroundColor(backgroundColor)
faq_title.apply {
text = if (faqItem.title is Int) getString(faqItem.title) else faqItem.title as String
setTextColor(primaryColor)
}
faq_text.apply {
text = if (faqItem.text is Int) Html.fromHtml(getString(faqItem.text)) else faqItem.text as String
setTextColor(textColor)
setLinkTextColor(primaryColor)
movementMethod = LinkMovementMethod.getInstance()
removeUnderlines()
}
faq_holder.addView(this)
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(faq_toolbar, NavigationIcon.Arrow)
}
}

View File

@ -0,0 +1,94 @@
package com.simplemobiletools.commons.activities
import android.os.Bundle
import android.view.LayoutInflater
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.License
import kotlinx.android.synthetic.main.activity_license.*
import kotlinx.android.synthetic.main.item_license.view.*
class LicenseActivity : BaseSimpleActivity() {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_license)
updateTextColors(licenses_holder)
updateMaterialActivityViews(licenses_coordinator, licenses_holder, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(licenses_nested_scrollview, licenses_toolbar)
val textColor = getProperTextColor()
val backgroundColor = getProperBackgroundColor()
val primaryColor = getProperPrimaryColor()
val inflater = LayoutInflater.from(this)
val licenses = initLicenses()
val licenseMask = intent.getLongExtra(APP_LICENSES, 0) or LICENSE_KOTLIN
licenses.filter { licenseMask and it.id != 0L }.forEach {
val license = it
inflater.inflate(R.layout.item_license, null).apply {
license_card.setCardBackgroundColor(backgroundColor)
license_title.apply {
text = getString(license.titleId)
setTextColor(primaryColor)
setOnClickListener {
launchViewIntent(license.urlId)
}
}
license_text.apply {
text = getString(license.textId)
setTextColor(textColor)
}
licenses_holder.addView(this)
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(licenses_toolbar, NavigationIcon.Arrow)
}
private fun initLicenses() = arrayOf(
License(LICENSE_KOTLIN, R.string.kotlin_title, R.string.kotlin_text, R.string.kotlin_url),
License(LICENSE_SUBSAMPLING, R.string.subsampling_title, R.string.subsampling_text, R.string.subsampling_url),
License(LICENSE_GLIDE, R.string.glide_title, R.string.glide_text, R.string.glide_url),
License(LICENSE_CROPPER, R.string.cropper_title, R.string.cropper_text, R.string.cropper_url),
License(LICENSE_RTL, R.string.rtl_viewpager_title, R.string.rtl_viewpager_text, R.string.rtl_viewpager_url),
License(LICENSE_JODA, R.string.joda_title, R.string.joda_text, R.string.joda_url),
License(LICENSE_STETHO, R.string.stetho_title, R.string.stetho_text, R.string.stetho_url),
License(LICENSE_OTTO, R.string.otto_title, R.string.otto_text, R.string.otto_url),
License(LICENSE_PHOTOVIEW, R.string.photoview_title, R.string.photoview_text, R.string.photoview_url),
License(LICENSE_PICASSO, R.string.picasso_title, R.string.picasso_text, R.string.picasso_url),
License(LICENSE_PATTERN, R.string.pattern_title, R.string.pattern_text, R.string.pattern_url),
License(LICENSE_REPRINT, R.string.reprint_title, R.string.reprint_text, R.string.reprint_url),
License(LICENSE_GIF_DRAWABLE, R.string.gif_drawable_title, R.string.gif_drawable_text, R.string.gif_drawable_url),
License(LICENSE_AUTOFITTEXTVIEW, R.string.autofittextview_title, R.string.autofittextview_text, R.string.autofittextview_url),
License(LICENSE_ROBOLECTRIC, R.string.robolectric_title, R.string.robolectric_text, R.string.robolectric_url),
License(LICENSE_ESPRESSO, R.string.espresso_title, R.string.espresso_text, R.string.espresso_url),
License(LICENSE_GSON, R.string.gson_title, R.string.gson_text, R.string.gson_url),
License(LICENSE_LEAK_CANARY, R.string.leak_canary_title, R.string.leakcanary_text, R.string.leakcanary_url),
License(LICENSE_NUMBER_PICKER, R.string.number_picker_title, R.string.number_picker_text, R.string.number_picker_url),
License(LICENSE_EXOPLAYER, R.string.exoplayer_title, R.string.exoplayer_text, R.string.exoplayer_url),
License(LICENSE_PANORAMA_VIEW, R.string.panorama_view_title, R.string.panorama_view_text, R.string.panorama_view_url),
License(LICENSE_SANSELAN, R.string.sanselan_title, R.string.sanselan_text, R.string.sanselan_url),
License(LICENSE_FILTERS, R.string.filters_title, R.string.filters_text, R.string.filters_url),
License(LICENSE_GESTURE_VIEWS, R.string.gesture_views_title, R.string.gesture_views_text, R.string.gesture_views_url),
License(LICENSE_INDICATOR_FAST_SCROLL, R.string.indicator_fast_scroll_title, R.string.indicator_fast_scroll_text, R.string.indicator_fast_scroll_url),
License(LICENSE_EVENT_BUS, R.string.event_bus_title, R.string.event_bus_text, R.string.event_bus_url),
License(LICENSE_AUDIO_RECORD_VIEW, R.string.audio_record_view_title, R.string.audio_record_view_text, R.string.audio_record_view_url),
License(LICENSE_SMS_MMS, R.string.sms_mms_title, R.string.sms_mms_text, R.string.sms_mms_url),
License(LICENSE_APNG, R.string.apng_title, R.string.apng_text, R.string.apng_url),
License(LICENSE_PDF_VIEW_PAGER, R.string.pdf_view_pager_title, R.string.pdf_view_pager_text, R.string.pdf_view_pager_url),
License(LICENSE_M3U_PARSER, R.string.m3u_parser_title, R.string.m3u_parser_text, R.string.m3u_parser_url),
License(LICENSE_ANDROID_LAME, R.string.android_lame_title, R.string.android_lame_text, R.string.android_lame_url)
)
}

View File

@ -0,0 +1,269 @@
package com.simplemobiletools.commons.activities
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.adapters.ManageBlockedNumbersAdapter
import com.simplemobiletools.commons.dialogs.AddBlockedNumberDialog
import com.simplemobiletools.commons.dialogs.ExportBlockedNumbersDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.helpers.BlockedNumbersExporter.ExportResult
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.models.BlockedNumber
import kotlinx.android.synthetic.main.activity_manage_blocked_numbers.*
import java.io.FileOutputStream
import java.io.OutputStream
class ManageBlockedNumbersActivity : BaseSimpleActivity(), RefreshRecyclerViewListener {
private val PICK_IMPORT_SOURCE_INTENT = 11
private val PICK_EXPORT_FILE_INTENT = 21
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_manage_blocked_numbers)
updateBlockedNumbers()
setupOptionsMenu()
updateMaterialActivityViews(block_numbers_coordinator, manage_blocked_numbers_list, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(manage_blocked_numbers_list, block_numbers_toolbar)
updateTextColors(manage_blocked_numbers_wrapper)
updatePlaceholderTexts()
val blockTitleRes = if (baseConfig.appId.startsWith("com.simplemobiletools.dialer")) R.string.block_unknown_calls else R.string.block_unknown_messages
block_unknown.apply {
setText(blockTitleRes)
isChecked = baseConfig.blockUnknownNumbers
if (isChecked) {
maybeSetDefaultCallerIdApp()
}
}
block_unknown_holder.setOnClickListener {
block_unknown.toggle()
baseConfig.blockUnknownNumbers = block_unknown.isChecked
if (block_unknown.isChecked) {
maybeSetDefaultCallerIdApp()
}
}
manage_blocked_numbers_placeholder_2.apply {
underlineText()
setTextColor(getProperPrimaryColor())
setOnClickListener {
if (isDefaultDialer()) {
addOrEditBlockedNumber()
} else {
launchSetDefaultDialerIntent()
}
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(block_numbers_toolbar, NavigationIcon.Arrow)
}
private fun setupOptionsMenu() {
block_numbers_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.add_blocked_number -> {
addOrEditBlockedNumber()
true
}
R.id.import_blocked_numbers -> {
tryImportBlockedNumbers()
true
}
R.id.export_blocked_numbers -> {
tryExportBlockedNumbers()
true
}
else -> false
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER && isDefaultDialer()) {
updatePlaceholderTexts()
updateBlockedNumbers()
} else if (requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
tryImportBlockedNumbersFromFile(resultData.data!!)
} else if (requestCode == PICK_EXPORT_FILE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val outputStream = contentResolver.openOutputStream(resultData.data!!)
exportBlockedNumbersTo(outputStream)
} else if (requestCode == REQUEST_CODE_SET_DEFAULT_CALLER_ID && resultCode != Activity.RESULT_OK) {
toast(R.string.must_make_default_caller_id_app, length = Toast.LENGTH_LONG)
baseConfig.blockUnknownNumbers = false
block_unknown.isChecked = false
}
}
override fun refreshItems() {
updateBlockedNumbers()
}
private fun updatePlaceholderTexts() {
manage_blocked_numbers_placeholder.text = getString(if (isDefaultDialer()) R.string.not_blocking_anyone else R.string.must_make_default_dialer)
manage_blocked_numbers_placeholder_2.text = getString(if (isDefaultDialer()) R.string.add_a_blocked_number else R.string.set_as_default)
}
private fun updateBlockedNumbers() {
ensureBackgroundThread {
val blockedNumbers = getBlockedNumbers()
runOnUiThread {
ManageBlockedNumbersAdapter(this, blockedNumbers, this, manage_blocked_numbers_list) {
addOrEditBlockedNumber(it as BlockedNumber)
}.apply {
manage_blocked_numbers_list.adapter = this
}
manage_blocked_numbers_placeholder.beVisibleIf(blockedNumbers.isEmpty())
manage_blocked_numbers_placeholder_2.beVisibleIf(blockedNumbers.isEmpty())
if (blockedNumbers.any { it.number.isBlockedNumberPattern() }) {
maybeSetDefaultCallerIdApp()
}
}
}
}
private fun addOrEditBlockedNumber(currentNumber: BlockedNumber? = null) {
AddBlockedNumberDialog(this, currentNumber) {
updateBlockedNumbers()
}
}
private fun tryImportBlockedNumbers() {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
try {
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
} catch (e: ActivityNotFoundException) {
toast(R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
}
}
} else {
handlePermission(PERMISSION_READ_STORAGE) {
if (it) {
pickFileToImportBlockedNumbers()
}
}
}
}
private fun tryImportBlockedNumbersFromFile(uri: Uri) {
when (uri.scheme) {
"file" -> importBlockedNumbers(uri.path!!)
"content" -> {
val tempFile = getTempFile("blocked", "blocked_numbers.txt")
if (tempFile == null) {
toast(R.string.unknown_error_occurred)
return
}
try {
val inputStream = contentResolver.openInputStream(uri)
val out = FileOutputStream(tempFile)
inputStream!!.copyTo(out)
importBlockedNumbers(tempFile.absolutePath)
} catch (e: Exception) {
showErrorToast(e)
}
}
else -> toast(R.string.invalid_file_format)
}
}
private fun pickFileToImportBlockedNumbers() {
FilePickerDialog(this) {
importBlockedNumbers(it)
}
}
private fun importBlockedNumbers(path: String) {
ensureBackgroundThread {
val result = BlockedNumbersImporter(this).importBlockedNumbers(path)
toast(
when (result) {
BlockedNumbersImporter.ImportResult.IMPORT_OK -> R.string.importing_successful
BlockedNumbersImporter.ImportResult.IMPORT_FAIL -> R.string.no_items_found
}
)
updateBlockedNumbers()
}
}
private fun tryExportBlockedNumbers() {
if (isQPlus()) {
ExportBlockedNumbersDialog(this, baseConfig.lastBlockedNumbersExportPath, true) { file ->
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE)
try {
startActivityForResult(this, PICK_EXPORT_FILE_INTENT)
} catch (e: ActivityNotFoundException) {
toast(R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
}
}
}
} else {
handlePermission(PERMISSION_WRITE_STORAGE) {
if (it) {
ExportBlockedNumbersDialog(this, baseConfig.lastBlockedNumbersExportPath, false) { file ->
getFileOutputStream(file.toFileDirItem(this), true) { out ->
exportBlockedNumbersTo(out)
}
}
}
}
}
}
private fun exportBlockedNumbersTo(outputStream: OutputStream?) {
ensureBackgroundThread {
val blockedNumbers = getBlockedNumbers()
if (blockedNumbers.isEmpty()) {
toast(R.string.no_entries_for_exporting)
} else {
BlockedNumbersExporter().exportBlockedNumbers(blockedNumbers, outputStream) {
toast(
when (it) {
ExportResult.EXPORT_OK -> R.string.exporting_successful
ExportResult.EXPORT_FAIL -> R.string.exporting_failed
}
)
}
}
}
}
private fun maybeSetDefaultCallerIdApp() {
if (isQPlus() && baseConfig.appId.startsWith("com.simplemobiletools.dialer")) {
setDefaultCallerIdApp()
}
}
}

View File

@ -0,0 +1,61 @@
package com.simplemobiletools.commons.adapters
import android.util.TypedValue
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.getTextSize
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlinx.android.synthetic.main.filepicker_favorite.view.*
class FilepickerFavoritesAdapter(
activity: BaseSimpleActivity, val paths: List<String>, recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
private var fontSize = 0f
init {
fontSize = activity.getTextSize()
}
override fun getActionMenuId() = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.filepicker_favorite, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val path = paths[position]
holder.bindView(path, true, false) { itemView, adapterPosition ->
setupView(itemView, path)
}
bindViewHolder(holder)
}
override fun getItemCount() = paths.size
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {}
override fun getSelectableItemCount() = paths.size
override fun getIsItemSelectable(position: Int) = false
override fun getItemKeyPosition(key: Int) = paths.indexOfFirst { it.hashCode() == key }
override fun getItemSelectionKey(position: Int) = paths[position].hashCode()
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
private fun setupView(view: View, path: String) {
view.apply {
filepicker_favorite_label.text = path
filepicker_favorite_label.setTextColor(textColor)
filepicker_favorite_label.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
}
}
}

View File

@ -0,0 +1,152 @@
package com.simplemobiletools.commons.adapters
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
import com.bumptech.glide.request.RequestOptions
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.getFilePlaceholderDrawables
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlinx.android.synthetic.main.item_filepicker_list.view.*
import java.util.*
class FilepickerItemsAdapter(
activity: BaseSimpleActivity, val fileDirItems: List<FileDirItem>, recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate {
private lateinit var fileDrawable: Drawable
private lateinit var folderDrawable: Drawable
private var fileDrawables = HashMap<String, Drawable>()
private val hasOTGConnected = activity.hasOTGConnected()
private var fontSize = 0f
private val cornerRadius = resources.getDimension(R.dimen.rounded_corner_radius_small).toInt()
private val dateFormat = activity.baseConfig.dateFormat
private val timeFormat = activity.getTimeFormat()
init {
initDrawables()
fontSize = activity.getTextSize()
}
override fun getActionMenuId() = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_filepicker_list, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fileDirItem = fileDirItems[position]
holder.bindView(fileDirItem, true, false) { itemView, adapterPosition ->
setupView(itemView, fileDirItem)
}
bindViewHolder(holder)
}
override fun getItemCount() = fileDirItems.size
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {}
override fun getSelectableItemCount() = fileDirItems.size
override fun getIsItemSelectable(position: Int) = false
override fun getItemKeyPosition(key: Int) = fileDirItems.indexOfFirst { it.path.hashCode() == key }
override fun getItemSelectionKey(position: Int) = fileDirItems[position].path.hashCode()
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed && !activity.isFinishing) {
Glide.with(activity).clear(holder.itemView.list_item_icon!!)
}
}
private fun setupView(view: View, fileDirItem: FileDirItem) {
view.apply {
list_item_name.text = fileDirItem.name
list_item_name.setTextColor(textColor)
list_item_name.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
list_item_details.setTextColor(textColor)
list_item_details.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
if (fileDirItem.isDirectory) {
list_item_icon.setImageDrawable(folderDrawable)
list_item_details.text = getChildrenCnt(fileDirItem)
} else {
list_item_details.text = fileDirItem.size.formatSize()
val path = fileDirItem.path
val placeholder = fileDrawables.getOrElse(fileDirItem.name.substringAfterLast(".").toLowerCase(Locale.getDefault()), { fileDrawable })
val options = RequestOptions()
.signature(fileDirItem.getKey())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.error(placeholder)
var itemToLoad = if (fileDirItem.name.endsWith(".apk", true)) {
val packageInfo = context.packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
if (packageInfo != null) {
val appInfo = packageInfo.applicationInfo
appInfo.sourceDir = path
appInfo.publicSourceDir = path
appInfo.loadIcon(context.packageManager)
} else {
path
}
} else {
path
}
if (!activity.isDestroyed && !activity.isFinishing) {
if (activity.isRestrictedSAFOnlyRoot(path)) {
itemToLoad = activity.getAndroidSAFUri(path)
} else if (hasOTGConnected && itemToLoad is String && activity.isPathOnOTG(itemToLoad)) {
itemToLoad = itemToLoad.getOTGPublicPath(activity)
}
if (itemToLoad.toString().isGif()) {
Glide.with(activity).asBitmap().load(itemToLoad).apply(options).into(list_item_icon)
} else {
Glide.with(activity)
.load(itemToLoad)
.transition(withCrossFade())
.apply(options)
.transform(CenterCrop(), RoundedCorners(cornerRadius))
.into(list_item_icon)
}
}
}
}
}
private fun getChildrenCnt(item: FileDirItem): String {
val children = item.children
return activity.resources.getQuantityString(R.plurals.items, children, children)
}
private fun initDrawables() {
folderDrawable = resources.getColoredDrawableWithColor(R.drawable.ic_folder_vector, textColor)
folderDrawable.alpha = 180
fileDrawable = resources.getDrawable(R.drawable.ic_file_generic)
fileDrawables = getFilePlaceholderDrawables(activity)
}
override fun onChange(position: Int) = fileDirItems.getOrNull(position)?.getBubbleText(activity, dateFormat, timeFormat) ?: ""
}

View File

@ -0,0 +1,142 @@
package com.simplemobiletools.commons.adapters
import android.view.*
import android.widget.PopupMenu
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.copyToClipboard
import com.simplemobiletools.commons.extensions.deleteBlockedNumber
import com.simplemobiletools.commons.extensions.getPopupMenuTheme
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.models.BlockedNumber
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlinx.android.synthetic.main.item_manage_blocked_number.view.*
class ManageBlockedNumbersAdapter(
activity: BaseSimpleActivity, var blockedNumbers: ArrayList<BlockedNumber>, val listener: RefreshRecyclerViewListener?,
recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
init {
setupDragListener(true)
}
override fun getActionMenuId() = R.menu.cab_blocked_numbers
override fun prepareActionMode(menu: Menu) {
menu.apply {
findItem(R.id.cab_copy_number).isVisible = isOneItemSelected()
}
}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_copy_number -> copyNumberToClipboard()
R.id.cab_delete -> deleteSelection()
}
}
override fun getSelectableItemCount() = blockedNumbers.size
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = blockedNumbers.getOrNull(position)?.id?.toInt()
override fun getItemKeyPosition(key: Int) = blockedNumbers.indexOfFirst { it.id.toInt() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_blocked_number, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val blockedNumber = blockedNumbers[position]
holder.bindView(blockedNumber, true, true) { itemView, _ ->
setupView(itemView, blockedNumber)
}
bindViewHolder(holder)
}
override fun getItemCount() = blockedNumbers.size
private fun getSelectedItems() = blockedNumbers.filter { selectedKeys.contains(it.id.toInt()) } as ArrayList<BlockedNumber>
private fun setupView(view: View, blockedNumber: BlockedNumber) {
view.apply {
manage_blocked_number_holder?.isSelected = selectedKeys.contains(blockedNumber.id.toInt())
manage_blocked_number_title.apply {
text = blockedNumber.number
setTextColor(textColor)
}
overflow_menu_icon.drawable.apply {
mutate()
setTint(activity.getProperTextColor())
}
overflow_menu_icon.setOnClickListener {
showPopupMenu(overflow_menu_anchor, blockedNumber)
}
}
}
private fun showPopupMenu(view: View, blockedNumber: BlockedNumber) {
finishActMode()
val theme = activity.getPopupMenuTheme()
val contextTheme = ContextThemeWrapper(activity, theme)
PopupMenu(contextTheme, view, Gravity.END).apply {
inflate(getActionMenuId())
setOnMenuItemClickListener { item ->
val blockedNumberId = blockedNumber.id.toInt()
when (item.itemId) {
R.id.cab_copy_number -> {
executeItemMenuOperation(blockedNumberId) {
copyNumberToClipboard()
}
}
R.id.cab_delete -> {
executeItemMenuOperation(blockedNumberId) {
deleteSelection()
}
}
}
true
}
show()
}
}
private fun executeItemMenuOperation(blockedNumberId: Int, callback: () -> Unit) {
selectedKeys.add(blockedNumberId)
callback()
selectedKeys.remove(blockedNumberId)
}
private fun copyNumberToClipboard() {
val selectedNumber = getSelectedItems().firstOrNull() ?: return
activity.copyToClipboard(selectedNumber.number)
finishActMode()
}
private fun deleteSelection() {
val deleteBlockedNumbers = ArrayList<BlockedNumber>(selectedKeys.size)
val positions = getSelectedItemPositions()
getSelectedItems().forEach {
deleteBlockedNumbers.add(it)
activity.deleteBlockedNumber(it.number)
}
blockedNumbers.removeAll(deleteBlockedNumbers)
removeSelectedItems(positions)
if (blockedNumbers.isEmpty()) {
listener?.refreshItems()
}
}
}

View File

@ -0,0 +1,23 @@
package com.simplemobiletools.commons.adapters
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
class MyArrayAdapter<T>(context: Context, res: Int, items: Array<T>, val textColor: Int, val backgroundColor: Int, val padding: Int) :
ArrayAdapter<T>(context, res, items) {
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
view.findViewById<TextView>(android.R.id.text1).apply {
setTextColor(textColor)
setPadding(padding, padding, padding, padding)
background = ColorDrawable(backgroundColor)
}
return view
}
}

View File

@ -0,0 +1,348 @@
package com.simplemobiletools.commons.adapters
import android.graphics.Color
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.interfaces.MyActionModeCallback
import com.simplemobiletools.commons.views.MyRecyclerView
abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyclerView: MyRecyclerView, val itemClick: (Any) -> Unit) :
RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder>() {
protected val baseConfig = activity.baseConfig
protected val resources = activity.resources!!
protected val layoutInflater = activity.layoutInflater
protected var textColor = activity.getProperTextColor()
protected var backgroundColor = activity.getProperBackgroundColor()
protected var properPrimaryColor = activity.getProperPrimaryColor()
protected var contrastColor = properPrimaryColor.getContrastColor()
protected var actModeCallback: MyActionModeCallback
protected var selectedKeys = LinkedHashSet<Int>()
protected var positionOffset = 0
protected var actMode: ActionMode? = null
private var actBarTextView: TextView? = null
private var lastLongPressedItem = -1
abstract fun getActionMenuId(): Int
abstract fun prepareActionMode(menu: Menu)
abstract fun actionItemPressed(id: Int)
abstract fun getSelectableItemCount(): Int
abstract fun getIsItemSelectable(position: Int): Boolean
abstract fun getItemSelectionKey(position: Int): Int?
abstract fun getItemKeyPosition(key: Int): Int
abstract fun onActionModeCreated()
abstract fun onActionModeDestroyed()
protected fun isOneItemSelected() = selectedKeys.size == 1
init {
actModeCallback = object : MyActionModeCallback() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
actionItemPressed(item.itemId)
return true
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu?): Boolean {
if (getActionMenuId() == 0) {
return true
}
selectedKeys.clear()
isSelectable = true
actMode = actionMode
actBarTextView = layoutInflater.inflate(R.layout.actionbar_title, null) as TextView
actBarTextView!!.layoutParams = ActionBar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
actMode!!.customView = actBarTextView
actBarTextView!!.setOnClickListener {
if (getSelectableItemCount() == selectedKeys.size) {
finishActMode()
} else {
selectAll()
}
}
activity.menuInflater.inflate(getActionMenuId(), menu)
val bgColor = if (baseConfig.isUsingSystemTheme) {
resources.getColor(R.color.you_contextual_status_bar_color, activity.theme)
} else {
Color.BLACK
}
actBarTextView!!.setTextColor(bgColor.getContrastColor())
activity.updateMenuItemColors(menu, baseColor = bgColor)
onActionModeCreated()
if (baseConfig.isUsingSystemTheme) {
actBarTextView?.onGlobalLayout {
val backArrow = activity.findViewById<ImageView>(R.id.action_mode_close_button)
backArrow?.applyColorFilter(bgColor.getContrastColor())
}
}
return true
}
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
prepareActionMode(menu)
return true
}
override fun onDestroyActionMode(actionMode: ActionMode) {
isSelectable = false
(selectedKeys.clone() as HashSet<Int>).forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
toggleItemSelection(false, position, false)
}
}
updateTitle()
selectedKeys.clear()
actBarTextView?.text = ""
actMode = null
lastLongPressedItem = -1
onActionModeDestroyed()
}
}
}
protected fun toggleItemSelection(select: Boolean, pos: Int, updateTitle: Boolean = true) {
if (select && !getIsItemSelectable(pos)) {
return
}
val itemKey = getItemSelectionKey(pos) ?: return
if ((select && selectedKeys.contains(itemKey)) || (!select && !selectedKeys.contains(itemKey))) {
return
}
if (select) {
selectedKeys.add(itemKey)
} else {
selectedKeys.remove(itemKey)
}
notifyItemChanged(pos + positionOffset)
if (updateTitle) {
updateTitle()
}
if (selectedKeys.isEmpty()) {
finishActMode()
}
}
private fun updateTitle() {
val selectableItemCount = getSelectableItemCount()
val selectedCount = Math.min(selectedKeys.size, selectableItemCount)
val oldTitle = actBarTextView?.text
val newTitle = "$selectedCount / $selectableItemCount"
if (oldTitle != newTitle) {
actBarTextView?.text = newTitle
actMode?.invalidate()
}
}
fun itemLongClicked(position: Int) {
recyclerView.setDragSelectActive(position)
lastLongPressedItem = if (lastLongPressedItem == -1) {
position
} else {
val min = Math.min(lastLongPressedItem, position)
val max = Math.max(lastLongPressedItem, position)
for (i in min..max) {
toggleItemSelection(true, i, false)
}
updateTitle()
position
}
}
protected fun getSelectedItemPositions(sortDescending: Boolean = true): ArrayList<Int> {
val positions = ArrayList<Int>()
val keys = selectedKeys.toList()
keys.forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
positions.add(position)
}
}
if (sortDescending) {
positions.sortDescending()
}
return positions
}
protected fun selectAll() {
val cnt = itemCount - positionOffset
for (i in 0 until cnt) {
toggleItemSelection(true, i, false)
}
lastLongPressedItem = -1
updateTitle()
}
protected fun setupDragListener(enable: Boolean) {
if (enable) {
recyclerView.setupDragListener(object : MyRecyclerView.MyDragListener {
override fun selectItem(position: Int) {
toggleItemSelection(true, position, true)
}
override fun selectRange(initialSelection: Int, lastDraggedIndex: Int, minReached: Int, maxReached: Int) {
selectItemRange(
initialSelection,
Math.max(0, lastDraggedIndex - positionOffset),
Math.max(0, minReached - positionOffset),
maxReached - positionOffset
)
if (minReached != maxReached) {
lastLongPressedItem = -1
}
}
})
} else {
recyclerView.setupDragListener(null)
}
}
protected fun selectItemRange(from: Int, to: Int, min: Int, max: Int) {
if (from == to) {
(min..max).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
return
}
if (to < from) {
for (i in to..from) {
toggleItemSelection(true, i, true)
}
if (min > -1 && min < to) {
(min until to).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
}
if (max > -1) {
for (i in from + 1..max) {
toggleItemSelection(false, i, true)
}
}
} else {
for (i in from..to) {
toggleItemSelection(true, i, true)
}
if (max > -1 && max > to) {
(to + 1..max).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
}
if (min > -1) {
for (i in min until from) {
toggleItemSelection(false, i, true)
}
}
}
}
fun setupZoomListener(zoomListener: MyRecyclerView.MyZoomListener?) {
recyclerView.setupZoomListener(zoomListener)
}
fun addVerticalDividers(add: Boolean) {
if (recyclerView.itemDecorationCount > 0) {
recyclerView.removeItemDecorationAt(0)
}
if (add) {
DividerItemDecoration(activity, DividerItemDecoration.VERTICAL).apply {
setDrawable(resources.getDrawable(R.drawable.divider))
recyclerView.addItemDecoration(this)
}
}
}
fun finishActMode() {
actMode?.finish()
}
fun updateTextColor(textColor: Int) {
this.textColor = textColor
notifyDataSetChanged()
}
fun updatePrimaryColor() {
properPrimaryColor = activity.getProperPrimaryColor()
contrastColor = properPrimaryColor.getContrastColor()
}
fun updateBackgroundColor(backgroundColor: Int) {
this.backgroundColor = backgroundColor
}
protected fun createViewHolder(layoutType: Int, parent: ViewGroup?): ViewHolder {
val view = layoutInflater.inflate(layoutType, parent, false)
return ViewHolder(view)
}
protected fun bindViewHolder(holder: ViewHolder) {
holder.itemView.tag = holder
}
protected fun removeSelectedItems(positions: ArrayList<Int>) {
positions.forEach {
notifyItemRemoved(it)
}
finishActMode()
}
open inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(any: Any, allowSingleClick: Boolean, allowLongClick: Boolean, callback: (itemView: View, adapterPosition: Int) -> Unit): View {
return itemView.apply {
callback(this, adapterPosition)
if (allowSingleClick) {
setOnClickListener { viewClicked(any) }
setOnLongClickListener { if (allowLongClick) viewLongClicked() else viewClicked(any); true }
} else {
setOnClickListener(null)
setOnLongClickListener(null)
}
}
}
fun viewClicked(any: Any) {
if (actModeCallback.isSelectable) {
val currentPosition = adapterPosition - positionOffset
val isSelected = selectedKeys.contains(getItemSelectionKey(currentPosition))
toggleItemSelection(!isSelected, currentPosition, true)
} else {
itemClick.invoke(any)
}
lastLongPressedItem = -1
}
fun viewLongClicked() {
val currentPosition = adapterPosition - positionOffset
if (!actModeCallback.isSelectable) {
activity.startActionMode(actModeCallback)
}
toggleItemSelection(true, currentPosition, true)
itemLongClicked(currentPosition)
}
}
}

View File

@ -0,0 +1,367 @@
package com.simplemobiletools.commons.adapters
import android.graphics.Color
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.interfaces.MyActionModeCallback
import com.simplemobiletools.commons.models.RecyclerSelectionPayload
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlin.math.max
import kotlin.math.min
abstract class MyRecyclerViewListAdapter<T>(
val activity: BaseSimpleActivity,
val recyclerView: MyRecyclerView,
diffUtil: DiffUtil.ItemCallback<T>,
val itemClick: (T) -> Unit,
val onRefresh: () -> Unit = {}
) : ListAdapter<T, MyRecyclerViewListAdapter<T>.ViewHolder>(diffUtil) {
protected val baseConfig = activity.baseConfig
protected val resources = activity.resources!!
protected val layoutInflater = activity.layoutInflater
protected var textColor = activity.getProperTextColor()
protected var backgroundColor = activity.getProperBackgroundColor()
protected var properPrimaryColor = activity.getProperPrimaryColor()
protected var contrastColor = properPrimaryColor.getContrastColor()
protected var actModeCallback: MyActionModeCallback
protected var selectedKeys = LinkedHashSet<Int>()
protected var positionOffset = 0
protected var actMode: ActionMode? = null
private var actBarTextView: TextView? = null
private var lastLongPressedItem = -1
abstract fun getActionMenuId(): Int
abstract fun prepareActionMode(menu: Menu)
abstract fun actionItemPressed(id: Int)
abstract fun getSelectableItemCount(): Int
abstract fun getIsItemSelectable(position: Int): Boolean
abstract fun getItemSelectionKey(position: Int): Int?
abstract fun getItemKeyPosition(key: Int): Int
abstract fun onActionModeCreated()
abstract fun onActionModeDestroyed()
protected fun isOneItemSelected() = selectedKeys.size == 1
init {
actModeCallback = object : MyActionModeCallback() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
actionItemPressed(item.itemId)
return true
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu?): Boolean {
if (getActionMenuId() == 0) {
return true
}
isSelectable = true
actMode = actionMode
actBarTextView = layoutInflater.inflate(R.layout.actionbar_title, null) as TextView
actBarTextView!!.layoutParams = ActionBar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
actMode!!.customView = actBarTextView
actBarTextView!!.setOnClickListener {
if (getSelectableItemCount() == selectedKeys.size) {
finishActMode()
} else {
selectAll()
}
}
activity.menuInflater.inflate(getActionMenuId(), menu)
val bgColor = if (baseConfig.isUsingSystemTheme) {
ResourcesCompat.getColor(resources, R.color.you_contextual_status_bar_color, activity.theme)
} else {
Color.BLACK
}
actBarTextView!!.setTextColor(bgColor.getContrastColor())
activity.updateMenuItemColors(menu, baseColor = bgColor)
onActionModeCreated()
if (baseConfig.isUsingSystemTheme) {
actBarTextView?.onGlobalLayout {
val backArrow = activity.findViewById<ImageView>(R.id.action_mode_close_button)
backArrow?.applyColorFilter(bgColor.getContrastColor())
}
}
return true
}
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
prepareActionMode(menu)
return true
}
override fun onDestroyActionMode(actionMode: ActionMode) {
isSelectable = false
(selectedKeys.clone() as HashSet<Int>).forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
toggleItemSelection(false, position, false)
}
}
updateTitle()
selectedKeys.clear()
actBarTextView?.text = ""
actMode = null
lastLongPressedItem = -1
onActionModeDestroyed()
}
}
}
protected fun toggleItemSelection(select: Boolean, pos: Int, updateTitle: Boolean = true) {
if (select && !getIsItemSelectable(pos)) {
return
}
val itemKey = getItemSelectionKey(pos) ?: return
if ((select && selectedKeys.contains(itemKey)) || (!select && !selectedKeys.contains(itemKey))) {
return
}
if (select) {
selectedKeys.add(itemKey)
} else {
selectedKeys.remove(itemKey)
}
notifyItemChanged(pos + positionOffset, RecyclerSelectionPayload(select))
if (updateTitle) {
updateTitle()
}
if (selectedKeys.isEmpty()) {
finishActMode()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
val any = payloads.firstOrNull()
if (any is RecyclerSelectionPayload) {
holder.itemView.isSelected = any.selected
} else {
onBindViewHolder(holder, position)
}
}
private fun updateTitle() {
val selectableItemCount = getSelectableItemCount()
val selectedCount = min(selectedKeys.size, selectableItemCount)
val oldTitle = actBarTextView?.text
val newTitle = "$selectedCount / $selectableItemCount"
if (oldTitle != newTitle) {
actBarTextView?.text = newTitle
actMode?.invalidate()
}
}
fun itemLongClicked(position: Int) {
recyclerView.setDragSelectActive(position)
lastLongPressedItem = if (lastLongPressedItem == -1) {
position
} else {
val min = min(lastLongPressedItem, position)
val max = max(lastLongPressedItem, position)
for (i in min..max) {
toggleItemSelection(true, i, false)
}
updateTitle()
position
}
}
protected fun getSelectedItemPositions(sortDescending: Boolean = true): ArrayList<Int> {
val positions = ArrayList<Int>()
val keys = selectedKeys.toList()
keys.forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
positions.add(position)
}
}
if (sortDescending) {
positions.sortDescending()
}
return positions
}
protected fun selectAll() {
val cnt = itemCount - positionOffset
for (i in 0 until cnt) {
toggleItemSelection(true, i, false)
}
lastLongPressedItem = -1
updateTitle()
}
protected fun setupDragListener(enable: Boolean) {
if (enable) {
recyclerView.setupDragListener(object : MyRecyclerView.MyDragListener {
override fun selectItem(position: Int) {
toggleItemSelection(true, position, true)
}
override fun selectRange(initialSelection: Int, lastDraggedIndex: Int, minReached: Int, maxReached: Int) {
selectItemRange(
initialSelection,
max(0, lastDraggedIndex - positionOffset),
max(0, minReached - positionOffset),
maxReached - positionOffset
)
if (minReached != maxReached) {
lastLongPressedItem = -1
}
}
})
} else {
recyclerView.setupDragListener(null)
}
}
protected fun selectItemRange(from: Int, to: Int, min: Int, max: Int) {
if (from == to) {
(min..max).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
return
}
if (to < from) {
for (i in to..from) {
toggleItemSelection(true, i, true)
}
if (min > -1 && min < to) {
(min until to).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
}
if (max > -1) {
for (i in from + 1..max) {
toggleItemSelection(false, i, true)
}
}
} else {
for (i in from..to) {
toggleItemSelection(true, i, true)
}
if (max > -1 && max > to) {
(to + 1..max).filter { it != from }.forEach { toggleItemSelection(false, it, true) }
}
if (min > -1) {
for (i in min until from) {
toggleItemSelection(false, i, true)
}
}
}
}
fun setupZoomListener(zoomListener: MyRecyclerView.MyZoomListener?) {
recyclerView.setupZoomListener(zoomListener)
}
fun addVerticalDividers(add: Boolean) {
if (recyclerView.itemDecorationCount > 0) {
recyclerView.removeItemDecorationAt(0)
}
if (add) {
DividerItemDecoration(activity, DividerItemDecoration.VERTICAL).apply {
setDrawable(resources.getDrawable(R.drawable.divider))
recyclerView.addItemDecoration(this)
}
}
}
fun finishActMode() {
actMode?.finish()
}
fun updateTextColor(textColor: Int) {
this.textColor = textColor
onRefresh.invoke()
}
fun updatePrimaryColor() {
properPrimaryColor = activity.getProperPrimaryColor()
contrastColor = properPrimaryColor.getContrastColor()
}
fun updateBackgroundColor(backgroundColor: Int) {
this.backgroundColor = backgroundColor
}
protected fun createViewHolder(layoutType: Int, parent: ViewGroup?): ViewHolder {
val view = layoutInflater.inflate(layoutType, parent, false)
return ViewHolder(view)
}
protected fun bindViewHolder(holder: ViewHolder) {
holder.itemView.tag = holder
}
protected fun removeSelectedItems(positions: ArrayList<Int>) {
positions.forEach {
notifyItemRemoved(it)
}
finishActMode()
}
open inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(item: T, allowSingleClick: Boolean, allowLongClick: Boolean, callback: (itemView: View, adapterPosition: Int) -> Unit): View {
return itemView.apply {
callback(this, adapterPosition)
if (allowSingleClick) {
setOnClickListener { viewClicked(item) }
setOnLongClickListener { if (allowLongClick) viewLongClicked() else viewClicked(item); true }
} else {
setOnClickListener(null)
setOnLongClickListener(null)
}
}
}
fun viewClicked(any: T) {
if (actModeCallback.isSelectable) {
val currentPosition = adapterPosition - positionOffset
val isSelected = selectedKeys.contains(getItemSelectionKey(currentPosition))
toggleItemSelection(!isSelected, currentPosition, true)
} else {
itemClick.invoke(any)
}
lastLongPressedItem = -1
}
fun viewLongClicked() {
val currentPosition = adapterPosition - positionOffset
if (!actModeCallback.isSelectable) {
activity.startActionMode(actModeCallback)
}
toggleItemSelection(true, currentPosition, true)
itemLongClicked(currentPosition)
}
}
}

View File

@ -0,0 +1,57 @@
package com.simplemobiletools.commons.adapters
import android.content.Context
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.biometric.auth.AuthPromptHost
import androidx.viewpager.widget.PagerAdapter
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.helpers.PROTECTION_FINGERPRINT
import com.simplemobiletools.commons.helpers.PROTECTION_PATTERN
import com.simplemobiletools.commons.helpers.PROTECTION_PIN
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.commons.interfaces.HashListener
import com.simplemobiletools.commons.interfaces.SecurityTab
import com.simplemobiletools.commons.views.MyScrollView
class PasswordTypesAdapter(
private val context: Context,
private val requiredHash: String,
private val hashListener: HashListener,
private val scrollView: MyScrollView,
private val biometricPromptHost: AuthPromptHost,
private val showBiometricIdTab: Boolean,
private val showBiometricAuthentication: Boolean
) : PagerAdapter() {
private val tabs = SparseArray<SecurityTab>()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(context).inflate(layoutSelection(position), container, false)
container.addView(view)
tabs.put(position, view as SecurityTab)
(view as SecurityTab).initTab(requiredHash, hashListener, scrollView, biometricPromptHost, showBiometricAuthentication)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, item: Any) {
tabs.remove(position)
container.removeView(item as View)
}
override fun getCount() = if (showBiometricIdTab) 3 else 2
override fun isViewFromObject(view: View, item: Any) = view == item
private fun layoutSelection(position: Int): Int = when (position) {
PROTECTION_PATTERN -> R.layout.tab_pattern
PROTECTION_PIN -> R.layout.tab_pin
PROTECTION_FINGERPRINT -> if (isRPlus()) R.layout.tab_biometric_id else R.layout.tab_fingerprint
else -> throw RuntimeException("Only 3 tabs allowed")
}
fun isTabVisible(position: Int, isVisible: Boolean) {
tabs[position]?.visibilityChanged(isVisible)
}
}

View File

@ -0,0 +1,42 @@
package com.simplemobiletools.commons.adapters
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.interfaces.RenameTab
import java.util.*
class RenameAdapter(val activity: BaseSimpleActivity, val paths: ArrayList<String>) : PagerAdapter() {
private val tabs = SparseArray<RenameTab>()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(activity).inflate(layoutSelection(position), container, false)
container.addView(view)
tabs.put(position, view as RenameTab)
(view as RenameTab).initTab(activity, paths)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, item: Any) {
tabs.remove(position)
container.removeView(item as View)
}
override fun getCount() = 2
override fun isViewFromObject(view: View, item: Any) = view == item
private fun layoutSelection(position: Int): Int = when (position) {
0 -> R.layout.tab_rename_simple
1 -> R.layout.tab_rename_pattern
else -> throw RuntimeException("Only 2 tabs allowed")
}
fun dialogConfirmed(useMediaFileExtension: Boolean, position: Int, callback: (success: Boolean) -> Unit) {
tabs[position].dialogConfirmed(useMediaFileExtension, callback)
}
}

View File

@ -0,0 +1,64 @@
package com.simplemobiletools.commons.adapters
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.SimpleListItem
import kotlinx.android.synthetic.main.item_simple_list.view.*
open class SimpleListItemAdapter(val activity: Activity, val onItemClicked: (SimpleListItem) -> Unit) :
ListAdapter<SimpleListItem, SimpleListItemAdapter.SimpleItemViewHolder>(SimpleListItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleItemViewHolder {
val view = activity.layoutInflater.inflate(R.layout.item_simple_list, parent, false)
return SimpleItemViewHolder(view)
}
override fun onBindViewHolder(holder: SimpleItemViewHolder, position: Int) {
val route = getItem(position)
holder.bindView(route)
}
open inner class SimpleItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindView(item: SimpleListItem) {
setupSimpleListItem(itemView, item, onItemClicked)
}
}
private class SimpleListItemDiffCallback : DiffUtil.ItemCallback<SimpleListItem>() {
override fun areItemsTheSame(oldItem: SimpleListItem, newItem: SimpleListItem): Boolean {
return SimpleListItem.areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(oldItem: SimpleListItem, newItem: SimpleListItem): Boolean {
return SimpleListItem.areContentsTheSame(oldItem, newItem)
}
}
}
fun setupSimpleListItem(view: View, item: SimpleListItem, onItemClicked: (SimpleListItem) -> Unit) {
view.apply {
val color = if (item.selected) {
context.getProperPrimaryColor()
} else {
context.getProperTextColor()
}
bottom_sheet_item_title.setText(item.textRes)
bottom_sheet_item_title.setTextColor(color)
bottom_sheet_item_icon.setImageResourceOrBeGone(item.imageRes)
bottom_sheet_item_icon.applyColorFilter(color)
bottom_sheet_selected_icon.beVisibleIf(item.selected)
bottom_sheet_selected_icon.applyColorFilter(color)
setOnClickListener {
onItemClicked(item)
}
}
}

View File

@ -0,0 +1,363 @@
package com.simplemobiletools.commons.asynctasks
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ContentValues
import android.os.AsyncTask
import android.os.Handler
import android.provider.MediaStore
import androidx.core.app.NotificationCompat
import androidx.core.util.Pair
import androidx.documentfile.provider.DocumentFile
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.CONFLICT_KEEP_BOTH
import com.simplemobiletools.commons.helpers.CONFLICT_SKIP
import com.simplemobiletools.commons.helpers.getConflictResolution
import com.simplemobiletools.commons.helpers.isOreoPlus
import com.simplemobiletools.commons.interfaces.CopyMoveListener
import com.simplemobiletools.commons.models.FileDirItem
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.ref.WeakReference
class CopyMoveTask(
val activity: BaseSimpleActivity, val copyOnly: Boolean, val copyMediaOnly: Boolean, val conflictResolutions: LinkedHashMap<String, Int>,
listener: CopyMoveListener, val copyHidden: Boolean
) : AsyncTask<Pair<ArrayList<FileDirItem>, String>, Void, Boolean>() {
private val INITIAL_PROGRESS_DELAY = 3000L
private val PROGRESS_RECHECK_INTERVAL = 500L
private var mListener: WeakReference<CopyMoveListener>? = null
private var mTransferredFiles = ArrayList<FileDirItem>()
private var mFileDirItemsToDelete = ArrayList<FileDirItem>() // confirm the deletion of files on Android 11 from Downloads and Android at once
private var mDocuments = LinkedHashMap<String, DocumentFile?>()
private var mFiles = ArrayList<FileDirItem>()
private var mFileCountToCopy = 0
private var mDestinationPath = ""
// progress indication
private var mNotificationBuilder: NotificationCompat.Builder
private var mCurrFilename = ""
private var mCurrentProgress = 0L
private var mMaxSize = 0
private var mNotifId = 0
private var mIsTaskOver = false
private var mProgressHandler = Handler()
init {
mListener = WeakReference(listener)
mNotificationBuilder = NotificationCompat.Builder(activity)
}
override fun doInBackground(vararg params: Pair<ArrayList<FileDirItem>, String>): Boolean? {
if (params.isEmpty()) {
return false
}
val pair = params[0]
mFiles = pair.first!!
mDestinationPath = pair.second!!
mFileCountToCopy = mFiles.size
mNotifId = (System.currentTimeMillis() / 1000).toInt()
mMaxSize = 0
for (file in mFiles) {
if (file.size == 0L) {
file.size = file.getProperSize(activity, copyHidden)
}
val newPath = "$mDestinationPath/${file.name}"
val fileExists = activity.getDoesFilePathExist(newPath)
if (getConflictResolution(conflictResolutions, newPath) != CONFLICT_SKIP || !fileExists) {
mMaxSize += (file.size / 1000).toInt()
}
}
mProgressHandler.postDelayed({
initProgressNotification()
updateProgress()
}, INITIAL_PROGRESS_DELAY)
for (file in mFiles) {
try {
val newPath = "$mDestinationPath/${file.name}"
var newFileDirItem = FileDirItem(newPath, newPath.getFilenameFromPath(), file.isDirectory)
if (activity.getDoesFilePathExist(newPath)) {
val resolution = getConflictResolution(conflictResolutions, newPath)
if (resolution == CONFLICT_SKIP) {
mFileCountToCopy--
continue
} else if (resolution == CONFLICT_KEEP_BOTH) {
val newFile = activity.getAlternativeFile(File(newFileDirItem.path))
newFileDirItem = FileDirItem(newFile.path, newFile.name, newFile.isDirectory)
}
}
copy(file, newFileDirItem)
} catch (e: Exception) {
activity.showErrorToast(e)
return false
}
}
return true
}
override fun onPostExecute(success: Boolean) {
if (activity.isFinishing || activity.isDestroyed) {
return
}
deleteProtectedFiles()
mProgressHandler.removeCallbacksAndMessages(null)
activity.notificationManager.cancel(mNotifId)
val listener = mListener?.get() ?: return
if (success) {
listener.copySucceeded(copyOnly, mTransferredFiles.size >= mFileCountToCopy, mDestinationPath, mTransferredFiles.size == 1)
} else {
listener.copyFailed()
}
}
private fun initProgressNotification() {
val channelId = "Copy/Move"
val title = activity.getString(if (copyOnly) R.string.copying else R.string.moving)
if (isOreoPlus()) {
val importance = NotificationManager.IMPORTANCE_LOW
NotificationChannel(channelId, title, importance).apply {
enableLights(false)
enableVibration(false)
activity.notificationManager.createNotificationChannel(this)
}
}
mNotificationBuilder.setContentTitle(title)
.setSmallIcon(R.drawable.ic_copy_vector)
.setChannelId(channelId)
}
private fun updateProgress() {
if (mIsTaskOver) {
activity.notificationManager.cancel(mNotifId)
cancel(true)
return
}
mNotificationBuilder.apply {
setContentText(mCurrFilename)
setProgress(mMaxSize, (mCurrentProgress / 1000).toInt(), false)
activity.notificationManager.notify(mNotifId, build())
}
mProgressHandler.removeCallbacksAndMessages(null)
mProgressHandler.postDelayed({
updateProgress()
if (mCurrentProgress / 1000 >= mMaxSize) {
mIsTaskOver = true
}
}, PROGRESS_RECHECK_INTERVAL)
}
private fun copy(source: FileDirItem, destination: FileDirItem) {
if (source.isDirectory) {
copyDirectory(source, destination.path)
} else {
copyFile(source, destination)
}
}
private fun copyDirectory(source: FileDirItem, destinationPath: String) {
if (!activity.createDirectorySync(destinationPath)) {
val error = String.format(activity.getString(R.string.could_not_create_folder), destinationPath)
activity.showErrorToast(error)
return
}
if (activity.isPathOnOTG(source.path)) {
val children = activity.getDocumentFile(source.path)?.listFiles() ?: return
for (child in children) {
val newPath = "$destinationPath/${child.name}"
if (File(newPath).exists()) {
continue
}
val oldPath = "${source.path}/${child.name}"
val oldFileDirItem = FileDirItem(oldPath, child.name!!, child.isDirectory, 0, child.length())
val newFileDirItem = FileDirItem(newPath, child.name!!, child.isDirectory)
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
} else if (activity.isRestrictedSAFOnlyRoot(source.path)) {
activity.getAndroidSAFFileItems(source.path, true) { files ->
for (child in files) {
val newPath = "$destinationPath/${child.name}"
if (activity.getDoesFilePathExist(newPath)) {
continue
}
val oldPath = "${source.path}/${child.name}"
val oldFileDirItem = FileDirItem(oldPath, child.name, child.isDirectory, 0, child.size)
val newFileDirItem = FileDirItem(newPath, child.name, child.isDirectory)
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
}
} else if (activity.isAccessibleWithSAFSdk30(source.path)) {
val children = activity.getDocumentSdk30(source.path)?.listFiles() ?: return
for (child in children) {
val newPath = "$destinationPath/${child.name}"
if (File(newPath).exists()) {
continue
}
val oldPath = "${source.path}/${child.name}"
val oldFileDirItem = FileDirItem(oldPath, child.name!!, child.isDirectory, 0, child.length())
val newFileDirItem = FileDirItem(newPath, child.name!!, child.isDirectory)
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
} else {
val children = File(source.path).list()
for (child in children) {
val newPath = "$destinationPath/$child"
if (activity.getDoesFilePathExist(newPath)) {
continue
}
val oldFile = File(source.path, child)
val oldFileDirItem = oldFile.toFileDirItem(activity)
val newFileDirItem = FileDirItem(newPath, newPath.getFilenameFromPath(), oldFile.isDirectory)
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
}
}
private fun copyFile(source: FileDirItem, destination: FileDirItem) {
if (copyMediaOnly && !source.path.isMediaFile()) {
mCurrentProgress += source.size
return
}
val directory = destination.getParentPath()
if (!activity.createDirectorySync(directory)) {
val error = String.format(activity.getString(R.string.could_not_create_folder), directory)
activity.showErrorToast(error)
mCurrentProgress += source.size
return
}
mCurrFilename = source.name
var inputStream: InputStream? = null
var out: OutputStream? = null
try {
if (!mDocuments.containsKey(directory) && activity.needsStupidWritePermissions(destination.path)) {
mDocuments[directory] = activity.getDocumentFile(directory)
}
out = activity.getFileOutputStreamSync(destination.path, source.path.getMimeType(), mDocuments[directory])
inputStream = activity.getFileInputStreamSync(source.path)!!
var copiedSize = 0L
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream.read(buffer)
while (bytes >= 0) {
out!!.write(buffer, 0, bytes)
copiedSize += bytes
mCurrentProgress += bytes
bytes = inputStream.read(buffer)
}
out?.flush()
if (source.size == copiedSize && activity.getDoesFilePathExist(destination.path)) {
mTransferredFiles.add(source)
if (copyOnly) {
activity.rescanPath(destination.path) {
if (activity.baseConfig.keepLastModified) {
updateLastModifiedValues(source, destination)
activity.rescanPath(destination.path)
}
}
} else if (activity.baseConfig.keepLastModified) {
updateLastModifiedValues(source, destination)
activity.rescanPath(destination.path)
inputStream.close()
out?.close()
deleteSourceFile(source)
} else {
inputStream.close()
out?.close()
deleteSourceFile(source)
}
}
} catch (e: Exception) {
activity.showErrorToast(e)
} finally {
inputStream?.close()
out?.close()
}
}
private fun updateLastModifiedValues(source: FileDirItem, destination: FileDirItem) {
copyOldLastModified(source.path, destination.path)
val lastModified = File(source.path).lastModified()
if (lastModified != 0L) {
File(destination.path).setLastModified(lastModified)
}
}
private fun deleteSourceFile(source: FileDirItem) {
if (activity.isRestrictedWithSAFSdk30(source.path) && !activity.canManageMedia()) {
mFileDirItemsToDelete.add(source)
} else {
activity.deleteFileBg(source, isDeletingMultipleFiles = false)
activity.deleteFromMediaStore(source.path)
}
}
// if we delete multiple files from Downloads folder on Android 11 or 12 without being a Media Management app, show the confirmation dialog just once
private fun deleteProtectedFiles() {
if (mFileDirItemsToDelete.isNotEmpty()) {
val fileUris = activity.getFileUrisFromFileDirItems(mFileDirItemsToDelete)
activity.deleteSDK30Uris(fileUris) { success ->
if (success) {
mFileDirItemsToDelete.forEach {
activity.deleteFromMediaStore(it.path)
}
}
}
}
}
private fun copyOldLastModified(sourcePath: String, destinationPath: String) {
val projection = arrayOf(
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED
)
val uri = MediaStore.Files.getContentUri("external")
val selection = "${MediaStore.MediaColumns.DATA} = ?"
var selectionArgs = arrayOf(sourcePath)
val cursor = activity.applicationContext.contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
val dateTaken = cursor.getLongValue(MediaStore.Images.Media.DATE_TAKEN)
val dateModified = cursor.getIntValue(MediaStore.Images.Media.DATE_MODIFIED)
val values = ContentValues().apply {
put(MediaStore.Images.Media.DATE_TAKEN, dateTaken)
put(MediaStore.Images.Media.DATE_MODIFIED, dateModified)
}
selectionArgs = arrayOf(destinationPath)
activity.applicationContext.contentResolver.update(uri, values, selection, selectionArgs)
}
}
}
}

View File

@ -0,0 +1,90 @@
package com.simplemobiletools.commons.databases
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.simplemobiletools.commons.helpers.Converters
import com.simplemobiletools.commons.helpers.FIRST_CONTACT_ID
import com.simplemobiletools.commons.helpers.FIRST_GROUP_ID
import com.simplemobiletools.commons.helpers.getEmptyLocalContact
import com.simplemobiletools.commons.interfaces.ContactsDao
import com.simplemobiletools.commons.models.contacts.Group
import com.simplemobiletools.commons.models.contacts.LocalContact
import com.simplemobiletools.commons.interfaces.GroupsDao
import java.util.concurrent.Executors
@Database(entities = [LocalContact::class, Group::class], version = 3)
@TypeConverters(Converters::class)
abstract class ContactsDatabase : RoomDatabase() {
abstract fun ContactsDao(): ContactsDao
abstract fun GroupsDao(): GroupsDao
companion object {
private var db: ContactsDatabase? = null
fun getInstance(context: Context): ContactsDatabase {
if (db == null) {
synchronized(ContactsDatabase::class) {
if (db == null) {
db = Room.databaseBuilder(context.applicationContext, ContactsDatabase::class.java, "local_contacts.db")
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
increaseAutoIncrementIds()
}
})
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.build()
}
}
}
return db!!
}
fun destroyInstance() {
db = null
}
// start autoincrement ID from FIRST_CONTACT_ID/FIRST_GROUP_ID to avoid conflicts
// Room doesn't seem to have a built in way for it, so just create a contact/group and delete it
private fun increaseAutoIncrementIds() {
Executors.newSingleThreadExecutor().execute {
val emptyContact = getEmptyLocalContact()
emptyContact.id = FIRST_CONTACT_ID
db!!.ContactsDao().apply {
insertOrUpdate(emptyContact)
deleteContactId(FIRST_CONTACT_ID)
}
val emptyGroup = Group(FIRST_GROUP_ID, "")
db!!.GroupsDao().apply {
insertOrUpdate(emptyGroup)
deleteGroupId(FIRST_GROUP_ID)
}
}
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("ALTER TABLE contacts ADD COLUMN photo_uri TEXT NOT NULL DEFAULT ''")
}
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("ALTER TABLE contacts ADD COLUMN ringtone TEXT DEFAULT ''")
}
}
}
}
}

View File

@ -0,0 +1,44 @@
package com.simplemobiletools.commons.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.BlockedNumber
import kotlinx.android.synthetic.main.dialog_add_blocked_number.view.*
class AddBlockedNumberDialog(val activity: BaseSimpleActivity, val originalNumber: BlockedNumber? = null, val callback: () -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_add_blocked_number, null).apply {
if (originalNumber != null) {
add_blocked_number_edittext.setText(originalNumber.number)
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
alertDialog.showKeyboard(view.add_blocked_number_edittext)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
var newBlockedNumber = view.add_blocked_number_edittext.value
if (originalNumber != null && newBlockedNumber != originalNumber.number) {
activity.deleteBlockedNumber(originalNumber.number)
}
if (newBlockedNumber.isNotEmpty()) {
// in case the user also added a '.' in the pattern, remove it
if (newBlockedNumber.contains(".*")) {
newBlockedNumber = newBlockedNumber.replace(".*", "*")
}
activity.addBlockedNumber(newBlockedNumber)
}
callback()
alertDialog.dismiss()
}
}
}
}
}

View File

@ -0,0 +1,47 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.getStringsPackageName
import com.simplemobiletools.commons.extensions.launchViewIntent
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_textview.view.*
class AppSideloadedDialog(val activity: Activity, val callback: () -> Unit) {
private var dialog: AlertDialog? = null
private val url = "https://play.google.com/store/apps/details?id=${activity.getStringsPackageName()}"
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_textview, null).apply {
val text = String.format(activity.getString(R.string.sideloaded_app), url)
text_view.text = Html.fromHtml(text)
text_view.movementMethod = LinkMovementMethod.getInstance()
}
activity.getAlertDialogBuilder()
.setNegativeButton(R.string.cancel) { dialog, which -> negativePressed() }
.setPositiveButton(R.string.download, null)
.setOnCancelListener { negativePressed() }
.apply {
activity.setupDialogStuff(view, this, R.string.app_corrupt) { alertDialog ->
dialog = alertDialog
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
downloadApp()
}
}
}
}
private fun downloadApp() {
activity.launchViewIntent(url)
}
private fun negativePressed() {
dialog?.dismiss()
callback()
}
}

View File

@ -0,0 +1,54 @@
package com.simplemobiletools.commons.dialogs
import android.os.Bundle
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.adapters.setupSimpleListItem
import com.simplemobiletools.commons.fragments.BaseBottomSheetDialogFragment
import com.simplemobiletools.commons.models.SimpleListItem
open class BottomSheetChooserDialog : BaseBottomSheetDialogFragment() {
var onItemClick: ((SimpleListItem) -> Unit)? = null
override fun setupContentView(parent: ViewGroup) {
val listItems = arguments?.getParcelableArray(ITEMS) as Array<SimpleListItem>
listItems.forEach { item ->
val view = layoutInflater.inflate(R.layout.item_simple_list, parent, false)
setupSimpleListItem(view, item) {
onItemClick?.invoke(it)
}
parent.addView(view)
}
}
override fun onDestroy() {
super.onDestroy()
onItemClick = null
}
companion object {
private const val TAG = "BottomSheetChooserDialog"
private const val ITEMS = "data"
fun createChooser(
fragmentManager: FragmentManager,
title: Int?,
items: Array<SimpleListItem>,
callback: (SimpleListItem) -> Unit
): BottomSheetChooserDialog {
val extras = Bundle().apply {
if (title != null) {
putInt(BOTTOM_SHEET_TITLE, title)
}
putParcelableArray(ITEMS, items)
}
return BottomSheetChooserDialog().apply {
arguments = extras
onItemClick = callback
show(fragmentManager, TAG)
}
}
}
}

View File

@ -0,0 +1,32 @@
package com.simplemobiletools.commons.dialogs
import android.view.animation.AnimationUtils
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.applyColorFilter
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_call_confirmation.view.*
class CallConfirmationDialog(val activity: BaseSimpleActivity, val callee: String, private val callback: () -> Unit) {
private var view = activity.layoutInflater.inflate(R.layout.dialog_call_confirmation, null)
init {
view.call_confirm_phone.applyColorFilter(activity.getProperTextColor())
activity.getAlertDialogBuilder()
.setNegativeButton(R.string.cancel, null)
.apply {
val title = String.format(activity.getString(R.string.confirm_calling_person), callee)
activity.setupDialogStuff(view, this, titleText = title) { alertDialog ->
view.call_confirm_phone.apply {
startAnimation(AnimationUtils.loadAnimation(activity, R.anim.shake_pulse_animation))
setOnClickListener {
callback.invoke()
alertDialog.dismiss()
}
}
}
}
}
}

View File

@ -0,0 +1,73 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.format.DateFormat
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.R.id.*
import com.simplemobiletools.commons.extensions.baseConfig
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.*
import kotlinx.android.synthetic.main.dialog_change_date_time_format.view.*
import java.util.*
class ChangeDateTimeFormatDialog(val activity: Activity, val callback: () -> Unit) {
private val view = activity.layoutInflater.inflate(R.layout.dialog_change_date_time_format, null)!!
private val sampleTS = 1613422500000 // February 15, 2021
init {
view.apply {
change_date_time_dialog_radio_one.text = formatDateSample(DATE_FORMAT_ONE)
change_date_time_dialog_radio_two.text = formatDateSample(DATE_FORMAT_TWO)
change_date_time_dialog_radio_three.text = formatDateSample(DATE_FORMAT_THREE)
change_date_time_dialog_radio_four.text = formatDateSample(DATE_FORMAT_FOUR)
change_date_time_dialog_radio_five.text = formatDateSample(DATE_FORMAT_FIVE)
change_date_time_dialog_radio_six.text = formatDateSample(DATE_FORMAT_SIX)
change_date_time_dialog_radio_seven.text = formatDateSample(DATE_FORMAT_SEVEN)
change_date_time_dialog_radio_eight.text = formatDateSample(DATE_FORMAT_EIGHT)
change_date_time_dialog_24_hour.isChecked = activity.baseConfig.use24HourFormat
val formatButton = when (activity.baseConfig.dateFormat) {
DATE_FORMAT_ONE -> change_date_time_dialog_radio_one
DATE_FORMAT_TWO -> change_date_time_dialog_radio_two
DATE_FORMAT_THREE -> change_date_time_dialog_radio_three
DATE_FORMAT_FOUR -> change_date_time_dialog_radio_four
DATE_FORMAT_FIVE -> change_date_time_dialog_radio_five
DATE_FORMAT_SIX -> change_date_time_dialog_radio_six
DATE_FORMAT_SEVEN -> change_date_time_dialog_radio_seven
else -> change_date_time_dialog_radio_eight
}
formatButton.isChecked = true
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
activity.baseConfig.dateFormat = when (view.change_date_time_dialog_radio_group.checkedRadioButtonId) {
change_date_time_dialog_radio_one -> DATE_FORMAT_ONE
change_date_time_dialog_radio_two -> DATE_FORMAT_TWO
change_date_time_dialog_radio_three -> DATE_FORMAT_THREE
change_date_time_dialog_radio_four -> DATE_FORMAT_FOUR
change_date_time_dialog_radio_five -> DATE_FORMAT_FIVE
change_date_time_dialog_radio_six -> DATE_FORMAT_SIX
change_date_time_dialog_radio_seven -> DATE_FORMAT_SEVEN
else -> DATE_FORMAT_EIGHT
}
activity.baseConfig.use24HourFormat = view.change_date_time_dialog_24_hour.isChecked
callback()
}
private fun formatDateSample(format: String): String {
val cal = Calendar.getInstance(Locale.ENGLISH)
cal.timeInMillis = sampleTS
return DateFormat.format(format, cal).toString()
}
}

View File

@ -0,0 +1,253 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.graphics.Color
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.views.ColorPickerSquare
import kotlinx.android.synthetic.main.dialog_color_picker.view.*
import java.util.LinkedList
private const val RECENT_COLORS_NUMBER = 5
// forked from https://github.com/yukuku/ambilwarna
class ColorPickerDialog(
val activity: Activity,
color: Int,
val removeDimmedBackground: Boolean = false,
val addDefaultColorButton: Boolean = false,
val currentColorCallback: ((color: Int) -> Unit)? = null,
val callback: (wasPositivePressed: Boolean, color: Int) -> Unit
) {
var viewHue: View
var viewSatVal: ColorPickerSquare
var viewCursor: ImageView
var viewNewColor: ImageView
var viewTarget: ImageView
var newHexField: EditText
var viewContainer: ViewGroup
private val baseConfig = activity.baseConfig
private val currentColorHsv = FloatArray(3)
private val backgroundColor = baseConfig.backgroundColor
private var isHueBeingDragged = false
private var wasDimmedBackgroundRemoved = false
private var dialog: AlertDialog? = null
init {
Color.colorToHSV(color, currentColorHsv)
val view = activity.layoutInflater.inflate(R.layout.dialog_color_picker, null).apply {
if (isQPlus()) {
isForceDarkAllowed = false
}
viewHue = color_picker_hue
viewSatVal = color_picker_square
viewCursor = color_picker_hue_cursor
viewNewColor = color_picker_new_color
viewTarget = color_picker_cursor
viewContainer = color_picker_holder
newHexField = color_picker_new_hex
viewSatVal.setHue(getHue())
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
color_picker_old_color.setFillWithStroke(color, backgroundColor)
val hexCode = getHexCode(color)
color_picker_old_hex.text = "#$hexCode"
color_picker_old_hex.setOnLongClickListener {
activity.copyToClipboard(hexCode)
true
}
newHexField.setText(hexCode)
setupRecentColors()
}
viewHue.setOnTouchListener(OnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
isHueBeingDragged = true
}
if (event.action == MotionEvent.ACTION_MOVE || event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
var y = event.y
if (y < 0f)
y = 0f
if (y > viewHue.measuredHeight) {
y = viewHue.measuredHeight - 0.001f // to avoid jumping the cursor from bottom to top.
}
var hue = 360f - 360f / viewHue.measuredHeight * y
if (hue == 360f)
hue = 0f
currentColorHsv[0] = hue
updateHue()
newHexField.setText(getHexCode(getColor()))
if (event.action == MotionEvent.ACTION_UP) {
isHueBeingDragged = false
}
return@OnTouchListener true
}
false
})
viewSatVal.setOnTouchListener(OnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_MOVE || event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
var x = event.x
var y = event.y
if (x < 0f)
x = 0f
if (x > viewSatVal.measuredWidth)
x = viewSatVal.measuredWidth.toFloat()
if (y < 0f)
y = 0f
if (y > viewSatVal.measuredHeight)
y = viewSatVal.measuredHeight.toFloat()
currentColorHsv[1] = 1f / viewSatVal.measuredWidth * x
currentColorHsv[2] = 1f - 1f / viewSatVal.measuredHeight * y
moveColorPicker()
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
newHexField.setText(getHexCode(getColor()))
return@OnTouchListener true
}
false
})
newHexField.onTextChangeListener {
if (it.length == 6 && !isHueBeingDragged) {
try {
val newColor = Color.parseColor("#$it")
Color.colorToHSV(newColor, currentColorHsv)
updateHue()
moveColorPicker()
} catch (ignored: Exception) {
}
}
}
val textColor = activity.getProperTextColor()
val builder = activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { _, _ -> confirmNewColor() }
.setNegativeButton(R.string.cancel) { _, _ -> dialogDismissed() }
.setOnCancelListener { dialogDismissed() }
.apply {
if (addDefaultColorButton) {
setNeutralButton(R.string.default_color) { _, _ -> confirmDefaultColor() }
}
}
builder.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
view.color_picker_arrow.applyColorFilter(textColor)
view.color_picker_hex_arrow.applyColorFilter(textColor)
viewCursor.applyColorFilter(textColor)
}
}
view.onGlobalLayout {
moveHuePicker()
moveColorPicker()
}
}
private fun View.setupRecentColors() {
val recentColors = baseConfig.colorPickerRecentColors
if (recentColors.isNotEmpty()) {
recent_colors.beVisible()
val squareSize = context.resources.getDimensionPixelSize(R.dimen.colorpicker_hue_width)
recentColors.take(RECENT_COLORS_NUMBER).forEach { recentColor ->
val recentColorView = ImageView(context)
recentColorView.id = View.generateViewId()
recentColorView.layoutParams = ViewGroup.LayoutParams(squareSize, squareSize)
recentColorView.setFillWithStroke(recentColor, backgroundColor)
recentColorView.setOnClickListener { newHexField.setText(getHexCode(recentColor)) }
recent_colors.addView(recentColorView)
recent_colors_flow.addView(recentColorView)
}
}
}
private fun dialogDismissed() {
callback(false, 0)
}
private fun confirmDefaultColor() {
callback(true, 0)
}
private fun confirmNewColor() {
val hexValue = newHexField.value
val newColor = if (hexValue.length == 6) {
Color.parseColor("#$hexValue")
} else {
getColor()
}
addRecentColor(newColor)
callback(true, newColor)
}
private fun addRecentColor(color: Int) {
var recentColors = baseConfig.colorPickerRecentColors
recentColors.remove(color)
if (recentColors.size >= RECENT_COLORS_NUMBER) {
val numberOfColorsToDrop = recentColors.size - RECENT_COLORS_NUMBER + 1
recentColors = LinkedList(recentColors.dropLast(numberOfColorsToDrop))
}
recentColors.addFirst(color)
baseConfig.colorPickerRecentColors = recentColors
}
private fun getHexCode(color: Int) = color.toHex().substring(1)
private fun updateHue() {
viewSatVal.setHue(getHue())
moveHuePicker()
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
if (removeDimmedBackground && !wasDimmedBackgroundRemoved) {
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
wasDimmedBackgroundRemoved = true
}
currentColorCallback?.invoke(getColor())
}
private fun moveHuePicker() {
var y = viewHue.measuredHeight - getHue() * viewHue.measuredHeight / 360f
if (y == viewHue.measuredHeight.toFloat())
y = 0f
viewCursor.x = (viewHue.left - viewCursor.width).toFloat()
viewCursor.y = viewHue.top + y - viewCursor.height / 2
}
private fun moveColorPicker() {
val x = getSat() * viewSatVal.measuredWidth
val y = (1f - getVal()) * viewSatVal.measuredHeight
viewTarget.x = viewSatVal.left + x - viewTarget.width / 2
viewTarget.y = viewSatVal.top + y - viewTarget.height / 2
}
private fun getColor() = Color.HSVToColor(currentColorHsv)
private fun getHue() = currentColorHsv[0]
private fun getSat() = currentColorHsv[1]
private fun getVal() = currentColorHsv[2]
}

View File

@ -0,0 +1,48 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_message.view.*
// similar fo ConfirmationDialog, but has a callback for negative button too
class ConfirmationAdvancedDialog(
activity: Activity, message: String = "", messageId: Int = R.string.proceed_with_deletion, positive: Int = R.string.yes,
negative: Int = R.string.no, val cancelOnTouchOutside: Boolean = true, val callback: (result: Boolean) -> Unit
) {
private var dialog: AlertDialog? = null
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_message, null)
view.message.text = if (message.isEmpty()) activity.resources.getString(messageId) else message
val builder = activity.getAlertDialogBuilder()
.setPositiveButton(positive) { dialog, which -> positivePressed() }
if (negative != 0) {
builder.setNegativeButton(negative) { dialog, which -> negativePressed() }
}
if (!cancelOnTouchOutside) {
builder.setOnCancelListener { negativePressed() }
}
builder.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = cancelOnTouchOutside) { alertDialog ->
dialog = alertDialog
}
}
}
private fun positivePressed() {
dialog?.dismiss()
callback(true)
}
private fun negativePressed() {
dialog?.dismiss()
callback(false)
}
}

View File

@ -0,0 +1,48 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_message.view.*
/**
* A simple dialog without any view, just a messageId, a positive button and optionally a negative button
*
* @param activity has to be activity context to avoid some Theme.AppCompat issues
* @param message the dialogs message, can be any String. If empty, messageId is used
* @param messageId the dialogs messageId ID. Used only if message is empty
* @param positive positive buttons text ID
* @param negative negative buttons text ID (optional)
* @param callback an anonymous function
*/
class ConfirmationDialog(
activity: Activity, message: String = "", messageId: Int = R.string.proceed_with_deletion, positive: Int = R.string.yes,
negative: Int = R.string.no, val cancelOnTouchOutside: Boolean = true, dialogTitle: String = "", val callback: () -> Unit
) {
private var dialog: AlertDialog? = null
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_message, null)
view.message.text = if (message.isEmpty()) activity.resources.getString(messageId) else message
val builder = activity.getAlertDialogBuilder()
.setPositiveButton(positive) { dialog, which -> dialogConfirmed() }
if (negative != 0) {
builder.setNegativeButton(negative, null)
}
builder.apply {
activity.setupDialogStuff(view, this, titleText = dialogTitle, cancelOnTouchOutside = cancelOnTouchOutside) { alertDialog ->
dialog = alertDialog
}
}
}
private fun dialogConfirmed() {
dialog?.dismiss()
callback()
}
}

View File

@ -0,0 +1,84 @@
package com.simplemobiletools.commons.dialogs
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isRPlus
import kotlinx.android.synthetic.main.dialog_create_new_folder.view.*
import java.io.File
class CreateNewFolderDialog(val activity: BaseSimpleActivity, val path: String, val callback: (path: String) -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_create_new_folder, null)
view.folder_path.setText("${activity.humanizePath(path).trimEnd('/')}/")
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.create_new_folder) { alertDialog ->
alertDialog.showKeyboard(view.folder_name)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(View.OnClickListener {
val name = view.folder_name.value
when {
name.isEmpty() -> activity.toast(R.string.empty_name)
name.isAValidFilename() -> {
val file = File(path, name)
if (file.exists()) {
activity.toast(R.string.name_taken)
return@OnClickListener
}
createFolder("$path/$name", alertDialog)
}
else -> activity.toast(R.string.invalid_name)
}
})
}
}
}
private fun createFolder(path: String, alertDialog: AlertDialog) {
try {
when {
activity.isRestrictedSAFOnlyRoot(path) && activity.createAndroidSAFDirectory(path) -> sendSuccess(alertDialog, path)
activity.isAccessibleWithSAFSdk30(path) -> activity.handleSAFDialogSdk30(path) {
if (it && activity.createSAFDirectorySdk30(path)) {
sendSuccess(alertDialog, path)
}
}
activity.needsStupidWritePermissions(path) -> activity.handleSAFDialog(path) {
if (it) {
try {
val documentFile = activity.getDocumentFile(path.getParentPath())
val newDir = documentFile?.createDirectory(path.getFilenameFromPath()) ?: activity.getDocumentFile(path)
if (newDir != null) {
sendSuccess(alertDialog, path)
} else {
activity.toast(R.string.unknown_error_occurred)
}
} catch (e: SecurityException) {
activity.showErrorToast(e)
}
}
}
File(path).mkdirs() -> sendSuccess(alertDialog, path)
isRPlus() && activity.isAStorageRootFolder(path.getParentPath()) -> activity.handleSAFCreateDocumentDialogSdk30(path) {
if (it) {
sendSuccess(alertDialog, path)
}
}
else -> activity.toast(activity.getString(R.string.could_not_create_folder, path.getFilenameFromPath()))
}
} catch (e: Exception) {
activity.showErrorToast(e)
}
}
private fun sendSuccess(alertDialog: AlertDialog, path: String) {
callback(path.trimEnd('/'))
alertDialog.dismiss()
}
}

View File

@ -0,0 +1,81 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.content.DialogInterface
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.DAY_SECONDS
import com.simplemobiletools.commons.helpers.HOUR_SECONDS
import com.simplemobiletools.commons.helpers.MINUTE_SECONDS
import kotlinx.android.synthetic.main.dialog_custom_interval_picker.view.*
class CustomIntervalPickerDialog(val activity: Activity, val selectedSeconds: Int = 0, val showSeconds: Boolean = false, val callback: (minutes: Int) -> Unit) {
private var dialog: AlertDialog? = null
private var view = (activity.layoutInflater.inflate(R.layout.dialog_custom_interval_picker, null) as ViewGroup)
init {
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialogInterface, i -> confirmReminder() }
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
alertDialog.showKeyboard(view.findViewById(R.id.dialog_custom_interval_value))
}
}
view.apply {
dialog_radio_seconds.beVisibleIf(showSeconds)
when {
selectedSeconds == 0 -> dialog_radio_view.check(R.id.dialog_radio_minutes)
selectedSeconds % DAY_SECONDS == 0 -> {
dialog_radio_view.check(R.id.dialog_radio_days)
dialog_custom_interval_value.setText((selectedSeconds / DAY_SECONDS).toString())
}
selectedSeconds % HOUR_SECONDS == 0 -> {
dialog_radio_view.check(R.id.dialog_radio_hours)
dialog_custom_interval_value.setText((selectedSeconds / HOUR_SECONDS).toString())
}
selectedSeconds % MINUTE_SECONDS == 0 -> {
dialog_radio_view.check(R.id.dialog_radio_minutes)
dialog_custom_interval_value.setText((selectedSeconds / MINUTE_SECONDS).toString())
}
else -> {
dialog_radio_view.check(R.id.dialog_radio_seconds)
dialog_custom_interval_value.setText(selectedSeconds.toString())
}
}
dialog_custom_interval_value.setOnKeyListener(object : View.OnKeyListener {
override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
dialog?.getButton(DialogInterface.BUTTON_POSITIVE)?.performClick()
return true
}
return false
}
})
}
}
private fun confirmReminder() {
val value = view.dialog_custom_interval_value.value
val multiplier = getMultiplier(view.dialog_radio_view.checkedRadioButtonId)
val minutes = Integer.valueOf(if (value.isEmpty()) "0" else value)
callback(minutes * multiplier)
activity.hideKeyboard()
dialog?.dismiss()
}
private fun getMultiplier(id: Int) = when (id) {
R.id.dialog_radio_days -> DAY_SECONDS
R.id.dialog_radio_hours -> HOUR_SECONDS
R.id.dialog_radio_minutes -> MINUTE_SECONDS
else -> 1
}
}

View File

@ -0,0 +1,28 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_donate.view.*
class DonateDialog(val activity: Activity) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_donate, null).apply {
dialog_donate_image.applyColorFilter(activity.getProperTextColor())
dialog_donate_text.text = Html.fromHtml(activity.getString(R.string.donate_short))
dialog_donate_text.movementMethod = LinkMovementMethod.getInstance()
dialog_donate_image.setOnClickListener {
activity.launchViewIntent(R.string.thank_you_url)
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.purchase) { dialog, which -> activity.launchViewIntent(R.string.thank_you_url) }
.setNegativeButton(R.string.later, null)
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false)
}
}
}

View File

@ -0,0 +1,68 @@
package com.simplemobiletools.commons.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.BLOCKED_NUMBERS_EXPORT_EXTENSION
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import kotlinx.android.synthetic.main.dialog_export_blocked_numbers.view.*
import java.io.File
class ExportBlockedNumbersDialog(
val activity: BaseSimpleActivity,
val path: String,
val hidePath: Boolean,
callback: (file: File) -> Unit,
) {
private var realPath = if (path.isEmpty()) activity.internalStoragePath else path
private val config = activity.baseConfig
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_export_blocked_numbers, null).apply {
export_blocked_numbers_folder.text = activity.humanizePath(realPath)
export_blocked_numbers_filename.setText("${activity.getString(R.string.blocked_numbers)}_${activity.getCurrentFormattedDateTime()}")
if (hidePath) {
export_blocked_numbers_folder_label.beGone()
export_blocked_numbers_folder.beGone()
} else {
export_blocked_numbers_folder.setOnClickListener {
FilePickerDialog(activity, realPath, false, showFAB = true) {
export_blocked_numbers_folder.text = activity.humanizePath(it)
realPath = it
}
}
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.export_blocked_numbers) { alertDialog ->
alertDialog.showKeyboard(view.export_blocked_numbers_filename)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val filename = view.export_blocked_numbers_filename.value
when {
filename.isEmpty() -> activity.toast(R.string.empty_name)
filename.isAValidFilename() -> {
val file = File(realPath, "$filename$BLOCKED_NUMBERS_EXPORT_EXTENSION")
if (!hidePath && file.exists()) {
activity.toast(R.string.name_taken)
return@setOnClickListener
}
ensureBackgroundThread {
config.lastBlockedNumbersExportPath = file.absolutePath.getParentPath()
callback(file)
alertDialog.dismiss()
}
}
else -> activity.toast(R.string.invalid_name)
}
}
}
}
}
}

View File

@ -0,0 +1,71 @@
package com.simplemobiletools.commons.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_export_settings.view.*
class ExportSettingsDialog(
val activity: BaseSimpleActivity, val defaultFilename: String, val hidePath: Boolean,
callback: (path: String, filename: String) -> Unit
) {
init {
val lastUsedFolder = activity.baseConfig.lastExportedSettingsFolder
var folder = if (lastUsedFolder.isNotEmpty() && activity.getDoesFilePathExist(lastUsedFolder)) {
lastUsedFolder
} else {
activity.internalStoragePath
}
val view = activity.layoutInflater.inflate(R.layout.dialog_export_settings, null).apply {
export_settings_filename.setText(defaultFilename.removeSuffix(".txt"))
if (hidePath) {
export_settings_path_hint.beGone()
} else {
export_settings_path.setText(activity.humanizePath(folder))
export_settings_path.setOnClickListener {
FilePickerDialog(activity, folder, false, showFAB = true) {
export_settings_path.setText(activity.humanizePath(it))
folder = it
}
}
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.export_settings) { alertDialog ->
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
var filename = view.export_settings_filename.value
if (filename.isEmpty()) {
activity.toast(R.string.filename_cannot_be_empty)
return@setOnClickListener
}
filename += ".txt"
val newPath = "${folder.trimEnd('/')}/$filename"
if (!newPath.getFilenameFromPath().isAValidFilename()) {
activity.toast(R.string.filename_invalid_characters)
return@setOnClickListener
}
activity.baseConfig.lastExportedSettingsFolder = folder
if (!hidePath && activity.getDoesFilePathExist(newPath)) {
val title = String.format(activity.getString(R.string.file_already_exists_overwrite), newPath.getFilenameFromPath())
ConfirmationDialog(activity, title) {
callback(newPath, filename)
alertDialog.dismiss()
}
} else {
callback(newPath, filename)
alertDialog.dismiss()
}
}
}
}
}
}

View File

@ -0,0 +1,40 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_feature_locked.view.*
class FeatureLockedDialog(val activity: Activity, val callback: () -> Unit) {
private var dialog: AlertDialog? = null
init {
val view: View = activity.layoutInflater.inflate(R.layout.dialog_feature_locked, null)
view.feature_locked_image.applyColorFilter(activity.getProperTextColor())
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.purchase, null)
.setNegativeButton(R.string.later) { dialog, which -> dismissDialog() }
.setOnDismissListener { dismissDialog() }
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false) { alertDialog ->
dialog = alertDialog
view.feature_locked_description.text = Html.fromHtml(activity.getString(R.string.features_locked))
view.feature_locked_description.movementMethod = LinkMovementMethod.getInstance()
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
activity.launchPurchaseThankYouIntent()
}
}
}
}
fun dismissDialog() {
dialog?.dismiss()
callback()
}
}

View File

@ -0,0 +1,66 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.R.id.conflict_dialog_radio_keep_both
import com.simplemobiletools.commons.R.id.conflict_dialog_radio_merge
import com.simplemobiletools.commons.R.id.conflict_dialog_radio_skip
import com.simplemobiletools.commons.extensions.baseConfig
import com.simplemobiletools.commons.extensions.beVisibleIf
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.helpers.CONFLICT_KEEP_BOTH
import com.simplemobiletools.commons.helpers.CONFLICT_MERGE
import com.simplemobiletools.commons.helpers.CONFLICT_OVERWRITE
import com.simplemobiletools.commons.helpers.CONFLICT_SKIP
import com.simplemobiletools.commons.models.FileDirItem
import kotlinx.android.synthetic.main.dialog_file_conflict.view.*
class FileConflictDialog(
val activity: Activity, val fileDirItem: FileDirItem, val showApplyToAllCheckbox: Boolean,
val callback: (resolution: Int, applyForAll: Boolean) -> Unit
) {
val view = activity.layoutInflater.inflate(R.layout.dialog_file_conflict, null)!!
init {
view.apply {
val stringBase = if (fileDirItem.isDirectory) R.string.folder_already_exists else R.string.file_already_exists
conflict_dialog_title.text = String.format(activity.getString(stringBase), fileDirItem.name)
conflict_dialog_apply_to_all.isChecked = activity.baseConfig.lastConflictApplyToAll
conflict_dialog_apply_to_all.beVisibleIf(showApplyToAllCheckbox)
conflict_dialog_divider.beVisibleIf(showApplyToAllCheckbox)
conflict_dialog_radio_merge.beVisibleIf(fileDirItem.isDirectory)
val resolutionButton = when (activity.baseConfig.lastConflictResolution) {
CONFLICT_OVERWRITE -> conflict_dialog_radio_overwrite
CONFLICT_MERGE -> conflict_dialog_radio_merge
else -> conflict_dialog_radio_skip
}
resolutionButton.isChecked = true
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this)
}
}
private fun dialogConfirmed() {
val resolution = when (view.conflict_dialog_radio_group.checkedRadioButtonId) {
conflict_dialog_radio_skip -> CONFLICT_SKIP
conflict_dialog_radio_merge -> CONFLICT_MERGE
conflict_dialog_radio_keep_both -> CONFLICT_KEEP_BOTH
else -> CONFLICT_OVERWRITE
}
val applyToAll = view.conflict_dialog_apply_to_all.isChecked
activity.baseConfig.apply {
lastConflictApplyToAll = applyToAll
lastConflictResolution = resolution
}
callback(resolution, applyToAll)
}
}

View File

@ -0,0 +1,356 @@
package com.simplemobiletools.commons.dialogs
import android.os.Environment
import android.os.Parcelable
import android.view.KeyEvent
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.documentfile.provider.DocumentFile
import androidx.recyclerview.widget.LinearLayoutManager
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.FilepickerFavoritesAdapter
import com.simplemobiletools.commons.adapters.FilepickerItemsAdapter
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.Breadcrumbs
import kotlinx.android.synthetic.main.dialog_filepicker.view.*
import java.io.File
/**
* The only filepicker constructor with a couple optional parameters
*
* @param activity has to be activity to avoid some Theme.AppCompat issues
* @param currPath initial path of the dialog, defaults to the external storage
* @param pickFile toggle used to determine if we are picking a file or a folder
* @param showHidden toggle for showing hidden items, whose name starts with a dot
* @param showFAB toggle the displaying of a Floating Action Button for creating new folders
* @param callback the callback used for returning the selected file/folder
*/
class FilePickerDialog(
val activity: BaseSimpleActivity,
var currPath: String = Environment.getExternalStorageDirectory().toString(),
val pickFile: Boolean = true,
var showHidden: Boolean = false,
val showFAB: Boolean = false,
val canAddShowHiddenButton: Boolean = false,
val forceShowRoot: Boolean = false,
val showFavoritesButton: Boolean = false,
private val enforceStorageRestrictions: Boolean = true,
val callback: (pickedPath: String) -> Unit
) : Breadcrumbs.BreadcrumbsListener {
private var mFirstUpdate = true
private var mPrevPath = ""
private var mScrollStates = HashMap<String, Parcelable>()
private var mDialog: AlertDialog? = null
private var mDialogView = activity.layoutInflater.inflate(R.layout.dialog_filepicker, null)
init {
if (!activity.getDoesFilePathExist(currPath)) {
currPath = activity.internalStoragePath
}
if (!activity.getIsPathDirectory(currPath)) {
currPath = currPath.getParentPath()
}
// do not allow copying files in the recycle bin manually
if (currPath.startsWith(activity.filesDir.absolutePath)) {
currPath = activity.internalStoragePath
}
mDialogView.filepicker_breadcrumbs.apply {
listener = this@FilePickerDialog
updateFontSize(activity.getTextSize(), false)
isShownInDialog = true
}
tryUpdateItems()
setupFavorites()
val builder = activity.getAlertDialogBuilder()
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener { dialogInterface, i, keyEvent ->
if (keyEvent.action == KeyEvent.ACTION_UP && i == KeyEvent.KEYCODE_BACK) {
val breadcrumbs = mDialogView.filepicker_breadcrumbs
if (breadcrumbs.getItemCount() > 1) {
breadcrumbs.removeBreadcrumb()
currPath = breadcrumbs.getLastItem().path.trimEnd('/')
tryUpdateItems()
} else {
mDialog?.dismiss()
}
}
true
}
if (!pickFile) {
builder.setPositiveButton(R.string.ok, null)
}
if (showFAB) {
mDialogView.filepicker_fab.apply {
beVisible()
setOnClickListener { createNewFolder() }
}
}
val secondaryFabBottomMargin = activity.resources.getDimension(if (showFAB) R.dimen.secondary_fab_bottom_margin else R.dimen.activity_margin).toInt()
mDialogView.filepicker_fabs_holder.apply {
(layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = secondaryFabBottomMargin
}
mDialogView.filepicker_placeholder.setTextColor(activity.getProperTextColor())
mDialogView.filepicker_fastscroller.updateColors(activity.getProperPrimaryColor())
mDialogView.filepicker_fab_show_hidden.apply {
beVisibleIf(!showHidden && canAddShowHiddenButton)
setOnClickListener {
activity.handleHiddenFolderPasswordProtection {
beGone()
showHidden = true
tryUpdateItems()
}
}
}
mDialogView.filepicker_favorites_label.text = "${activity.getString(R.string.favorites)}:"
mDialogView.filepicker_fab_show_favorites.apply {
beVisibleIf(showFavoritesButton && context.baseConfig.favorites.isNotEmpty())
setOnClickListener {
if (mDialogView.filepicker_favorites_holder.isVisible()) {
hideFavorites()
} else {
showFavorites()
}
}
}
builder.apply {
activity.setupDialogStuff(mDialogView, this, getTitle()) { alertDialog ->
mDialog = alertDialog
}
}
if (!pickFile) {
mDialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
verifyPath()
}
}
}
private fun getTitle() = if (pickFile) R.string.select_file else R.string.select_folder
private fun createNewFolder() {
CreateNewFolderDialog(activity, currPath) {
callback(it)
mDialog?.dismiss()
}
}
private fun tryUpdateItems() {
ensureBackgroundThread {
getItems(currPath) {
activity.runOnUiThread {
mDialogView.filepicker_placeholder.beGone()
updateItems(it as ArrayList<FileDirItem>)
}
}
}
}
private fun updateItems(items: ArrayList<FileDirItem>) {
if (!containsDirectory(items) && !mFirstUpdate && !pickFile && !showFAB) {
verifyPath()
return
}
val sortedItems = items.sortedWith(compareBy({ !it.isDirectory }, { it.name.toLowerCase() }))
val adapter = FilepickerItemsAdapter(activity, sortedItems, mDialogView.filepicker_list) {
if ((it as FileDirItem).isDirectory) {
activity.handleLockedFolderOpening(it.path) { success ->
if (success) {
currPath = it.path
tryUpdateItems()
}
}
} else if (pickFile) {
currPath = it.path
verifyPath()
}
}
val layoutManager = mDialogView.filepicker_list.layoutManager as LinearLayoutManager
mScrollStates[mPrevPath.trimEnd('/')] = layoutManager.onSaveInstanceState()!!
mDialogView.apply {
filepicker_list.adapter = adapter
filepicker_breadcrumbs.setBreadcrumb(currPath)
if (context.areSystemAnimationsEnabled) {
filepicker_list.scheduleLayoutAnimation()
}
layoutManager.onRestoreInstanceState(mScrollStates[currPath.trimEnd('/')])
}
mFirstUpdate = false
mPrevPath = currPath
}
private fun verifyPath() {
when {
activity.isRestrictedSAFOnlyRoot(currPath) -> {
val document = activity.getSomeAndroidSAFDocument(currPath) ?: return
sendSuccessForDocumentFile(document)
}
activity.isPathOnOTG(currPath) -> {
val fileDocument = activity.getSomeDocumentFile(currPath) ?: return
sendSuccessForDocumentFile(fileDocument)
}
activity.isAccessibleWithSAFSdk30(currPath) -> {
if (enforceStorageRestrictions) {
activity.handleSAFDialogSdk30(currPath) {
if (it) {
val document = activity.getSomeDocumentSdk30(currPath)
sendSuccessForDocumentFile(document ?: return@handleSAFDialogSdk30)
}
}
} else {
sendSuccessForDirectFile()
}
}
activity.isRestrictedWithSAFSdk30(currPath) -> {
if (enforceStorageRestrictions) {
if (activity.isInDownloadDir(currPath)) {
sendSuccessForDirectFile()
} else {
activity.toast(R.string.system_folder_restriction, Toast.LENGTH_LONG)
}
} else {
sendSuccessForDirectFile()
}
}
else -> {
sendSuccessForDirectFile()
}
}
}
private fun sendSuccessForDocumentFile(document: DocumentFile) {
if ((pickFile && document.isFile) || (!pickFile && document.isDirectory)) {
sendSuccess()
}
}
private fun sendSuccessForDirectFile() {
val file = File(currPath)
if ((pickFile && file.isFile) || (!pickFile && file.isDirectory)) {
sendSuccess()
}
}
private fun sendSuccess() {
currPath = if (currPath.length == 1) {
currPath
} else {
currPath.trimEnd('/')
}
callback(currPath)
mDialog?.dismiss()
}
private fun getItems(path: String, callback: (List<FileDirItem>) -> Unit) {
when {
activity.isRestrictedSAFOnlyRoot(path) -> {
activity.handleAndroidSAFDialog(path) {
activity.getAndroidSAFFileItems(path, showHidden) {
callback(it)
}
}
}
activity.isPathOnOTG(path) -> activity.getOTGItems(path, showHidden, false, callback)
else -> {
val lastModifieds = activity.getFolderLastModifieds(path)
getRegularItems(path, lastModifieds, callback)
}
}
}
private fun getRegularItems(path: String, lastModifieds: HashMap<String, Long>, callback: (List<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>()
val files = File(path).listFiles()?.filterNotNull()
if (files == null) {
callback(items)
return
}
for (file in files) {
if (!showHidden && file.name.startsWith('.')) {
continue
}
val curPath = file.absolutePath
val curName = curPath.getFilenameFromPath()
val size = file.length()
var lastModified = lastModifieds.remove(curPath)
val isDirectory = if (lastModified != null) false else file.isDirectory
if (lastModified == null) {
lastModified = 0 // we don't actually need the real lastModified that badly, do not check file.lastModified()
}
val children = if (isDirectory) file.getDirectChildrenCount(activity, showHidden) else 0
items.add(FileDirItem(curPath, curName, isDirectory, children, size, lastModified))
}
callback(items)
}
private fun containsDirectory(items: List<FileDirItem>) = items.any { it.isDirectory }
private fun setupFavorites() {
FilepickerFavoritesAdapter(activity, activity.baseConfig.favorites.toMutableList(), mDialogView.filepicker_favorites_list) {
currPath = it as String
verifyPath()
}.apply {
mDialogView.filepicker_favorites_list.adapter = this
}
}
private fun showFavorites() {
mDialogView.apply {
filepicker_favorites_holder.beVisible()
filepicker_files_holder.beGone()
val drawable = activity.resources.getColoredDrawableWithColor(R.drawable.ic_folder_vector, activity.getProperPrimaryColor().getContrastColor())
filepicker_fab_show_favorites.setImageDrawable(drawable)
}
}
private fun hideFavorites() {
mDialogView.apply {
filepicker_favorites_holder.beGone()
filepicker_files_holder.beVisible()
val drawable = activity.resources.getColoredDrawableWithColor(R.drawable.ic_star_vector, activity.getProperPrimaryColor().getContrastColor())
filepicker_fab_show_favorites.setImageDrawable(drawable)
}
}
override fun breadcrumbClicked(id: Int) {
if (id == 0) {
StoragePickerDialog(activity, currPath, forceShowRoot, true) {
currPath = it
tryUpdateItems()
}
} else {
val item = mDialogView.filepicker_breadcrumbs.getItem(id)
if (currPath != item.path.trimEnd('/')) {
currPath = item.path
tryUpdateItems()
}
}
}
}

View File

@ -0,0 +1,28 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.baseConfig
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_textview.view.*
class FolderLockingNoticeDialog(val activity: Activity, val callback: () -> Unit) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_textview, null).apply {
text_view.text = activity.getString(R.string.lock_folder_notice)
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.disclaimer)
}
}
private fun dialogConfirmed() {
activity.baseConfig.wasFolderLockingNoticeShown = true
callback()
}
}

View File

@ -0,0 +1,147 @@
package com.simplemobiletools.commons.dialogs
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.google.android.material.appbar.MaterialToolbar
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.interfaces.LineColorPickerListener
import kotlinx.android.synthetic.main.dialog_line_color_picker.view.*
class LineColorPickerDialog(
val activity: BaseSimpleActivity, val color: Int, val isPrimaryColorPicker: Boolean, val primaryColors: Int = R.array.md_primary_colors,
val appIconIDs: ArrayList<Int>? = null, val toolbar: MaterialToolbar? = null, val callback: (wasPositivePressed: Boolean, color: Int) -> Unit
) {
private val PRIMARY_COLORS_COUNT = 19
private val DEFAULT_PRIMARY_COLOR_INDEX = 14
private val DEFAULT_SECONDARY_COLOR_INDEX = 6
private val DEFAULT_COLOR_VALUE = activity.resources.getColor(R.color.color_primary)
private var wasDimmedBackgroundRemoved = false
private var dialog: AlertDialog? = null
private var view: View = activity.layoutInflater.inflate(R.layout.dialog_line_color_picker, null)
init {
view.apply {
hex_code.text = color.toHex()
hex_code.setOnLongClickListener {
activity.copyToClipboard(hex_code.value.substring(1))
true
}
line_color_picker_icon.beGoneIf(isPrimaryColorPicker)
val indexes = getColorIndexes(color)
val primaryColorIndex = indexes.first
primaryColorChanged(primaryColorIndex)
primary_line_color_picker.updateColors(getColors(primaryColors), primaryColorIndex)
primary_line_color_picker.listener = object : LineColorPickerListener {
override fun colorChanged(index: Int, color: Int) {
val secondaryColors = getColorsForIndex(index)
secondary_line_color_picker.updateColors(secondaryColors)
val newColor = if (isPrimaryColorPicker) secondary_line_color_picker.getCurrentColor() else color
colorUpdated(newColor)
if (!isPrimaryColorPicker) {
primaryColorChanged(index)
}
}
}
secondary_line_color_picker.beVisibleIf(isPrimaryColorPicker)
secondary_line_color_picker.updateColors(getColorsForIndex(primaryColorIndex), indexes.second)
secondary_line_color_picker.listener = object : LineColorPickerListener {
override fun colorChanged(index: Int, color: Int) {
colorUpdated(color)
}
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel) { dialog, which -> dialogDismissed() }
.setOnCancelListener { dialogDismissed() }
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
}
}
}
fun getSpecificColor() = view.secondary_line_color_picker.getCurrentColor()
private fun colorUpdated(color: Int) {
view.hex_code.text = color.toHex()
if (isPrimaryColorPicker) {
if (toolbar != null) {
activity.updateTopBarColors(toolbar, color)
}
if (!wasDimmedBackgroundRemoved) {
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
wasDimmedBackgroundRemoved = true
}
}
}
private fun getColorIndexes(color: Int): Pair<Int, Int> {
if (color == DEFAULT_COLOR_VALUE) {
return getDefaultColorPair()
}
for (i in 0 until PRIMARY_COLORS_COUNT) {
getColorsForIndex(i).indexOfFirst { color == it }.apply {
if (this != -1) {
return Pair(i, this)
}
}
}
return getDefaultColorPair()
}
private fun primaryColorChanged(index: Int) {
view.line_color_picker_icon.setImageResource(appIconIDs?.getOrNull(index) ?: 0)
}
private fun getDefaultColorPair() = Pair(DEFAULT_PRIMARY_COLOR_INDEX, DEFAULT_SECONDARY_COLOR_INDEX)
private fun dialogDismissed() {
callback(false, 0)
}
private fun dialogConfirmed() {
val targetView = if (isPrimaryColorPicker) view.secondary_line_color_picker else view.primary_line_color_picker
val color = targetView.getCurrentColor()
callback(true, color)
}
private fun getColorsForIndex(index: Int) = when (index) {
0 -> getColors(R.array.md_reds)
1 -> getColors(R.array.md_pinks)
2 -> getColors(R.array.md_purples)
3 -> getColors(R.array.md_deep_purples)
4 -> getColors(R.array.md_indigos)
5 -> getColors(R.array.md_blues)
6 -> getColors(R.array.md_light_blues)
7 -> getColors(R.array.md_cyans)
8 -> getColors(R.array.md_teals)
9 -> getColors(R.array.md_greens)
10 -> getColors(R.array.md_light_greens)
11 -> getColors(R.array.md_limes)
12 -> getColors(R.array.md_yellows)
13 -> getColors(R.array.md_ambers)
14 -> getColors(R.array.md_oranges)
15 -> getColors(R.array.md_deep_oranges)
16 -> getColors(R.array.md_browns)
17 -> getColors(R.array.md_blue_greys)
18 -> getColors(R.array.md_greys)
else -> throw RuntimeException("Invalid color id $index")
}
private fun getColors(id: Int) = activity.resources.getIntArray(id).toCollection(ArrayList())
}

View File

@ -0,0 +1,30 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_textview.view.*
class NewAppDialog(val activity: Activity, val packageName: String, val title: String, val packageName2: String, val title2: String) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_textview, null).apply {
val text = String.format(
activity.getString(R.string.new_app),
"https://play.google.com/store/apps/details?id=$packageName", title,
"https://play.google.com/store/apps/details?id=$packageName2", title2
)
text_view.text = Html.fromHtml(text)
text_view.movementMethod = LinkMovementMethod.getInstance()
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false)
}
}
}

View File

@ -0,0 +1,40 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.launchViewIntent
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_new_apps_icons.view.*
class NewAppsIconsDialog(val activity: Activity) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_new_apps_icons, null).apply {
val dialerUrl = "https://play.google.com/store/apps/details?id=com.simplemobiletools.dialer"
val smsMessengerUrl = "https://play.google.com/store/apps/details?id=com.simplemobiletools.smsmessenger"
val voiceRecorderUrl = "https://play.google.com/store/apps/details?id=com.simplemobiletools.voicerecorder"
val text = String.format(
activity.getString(R.string.new_app),
dialerUrl, activity.getString(R.string.simple_dialer),
smsMessengerUrl, activity.getString(R.string.simple_sms_messenger),
voiceRecorderUrl, activity.getString(R.string.simple_voice_recorder)
)
new_apps_text.text = Html.fromHtml(text)
new_apps_text.movementMethod = LinkMovementMethod.getInstance()
new_apps_dialer.setOnClickListener { activity.launchViewIntent(dialerUrl) }
new_apps_sms_messenger.setOnClickListener { activity.launchViewIntent(smsMessengerUrl) }
new_apps_voice_recorder.setOnClickListener { activity.launchViewIntent(voiceRecorderUrl) }
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false)
}
}
}

View File

@ -0,0 +1,27 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.openNotificationSettings
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_message.view.*
class PermissionRequiredDialog(val activity: Activity, textId: Int) {
private var dialog: AlertDialog? = null
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_message, null)
view.message.text = activity.getString(textId)
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.grant_permission) { dialog, which -> activity.openNotificationSettings() }
.setNegativeButton(R.string.cancel, null).apply {
val title = activity.getString(R.string.permission_required)
activity.setupDialogStuff(view, this, titleText = title) { alertDialog ->
dialog = alertDialog
}
}
}
}

View File

@ -0,0 +1,373 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.content.res.Resources
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.exifinterface.media.ExifInterface
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import kotlinx.android.synthetic.main.dialog_properties.view.*
import kotlinx.android.synthetic.main.item_property.view.*
import java.io.File
import java.util.*
class PropertiesDialog() {
private lateinit var mInflater: LayoutInflater
private lateinit var mPropertyView: ViewGroup
private lateinit var mResources: Resources
private lateinit var mActivity: Activity
private lateinit var mDialogView: View
private var mCountHiddenItems = false
/**
* A File Properties dialog constructor with an optional parameter, usable at 1 file selected
*
* @param activity request activity to avoid some Theme.AppCompat issues
* @param path the file path
* @param countHiddenItems toggle determining if we will count hidden files themselves and their sizes (reasonable only at directory properties)
*/
constructor(activity: Activity, path: String, countHiddenItems: Boolean = false) : this() {
if (!activity.getDoesFilePathExist(path) && !path.startsWith("content://")) {
activity.toast(String.format(activity.getString(R.string.source_file_doesnt_exist), path))
return
}
mActivity = activity
mInflater = LayoutInflater.from(activity)
mResources = activity.resources
mDialogView = mInflater.inflate(R.layout.dialog_properties, null)
mCountHiddenItems = countHiddenItems
mPropertyView = mDialogView.properties_holder!!
addProperties(path)
val builder = activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
if (!path.startsWith("content://") && path.canModifyEXIF() && activity.isPathOnInternalStorage(path)) {
if ((isRPlus() && Environment.isExternalStorageManager()) || (!isRPlus() && activity.hasPermission(PERMISSION_WRITE_STORAGE))) {
builder.setNeutralButton(R.string.remove_exif, null)
}
}
builder.apply {
mActivity.setupDialogStuff(mDialogView, this, R.string.properties) { alertDialog ->
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
removeEXIFFromPath(path)
}
}
}
}
private fun addProperties(path: String) {
val fileDirItem = FileDirItem(path, path.getFilenameFromPath(), mActivity.getIsPathDirectory(path))
addProperty(R.string.name, fileDirItem.name)
addProperty(R.string.path, fileDirItem.getParentPath())
addProperty(R.string.size, "", R.id.properties_size)
ensureBackgroundThread {
val fileCount = fileDirItem.getProperFileCount(mActivity, mCountHiddenItems)
val size = fileDirItem.getProperSize(mActivity, mCountHiddenItems).formatSize()
val directChildrenCount = if (fileDirItem.isDirectory) {
fileDirItem.getDirectChildrenCount(mActivity, mCountHiddenItems).toString()
} else {
0
}
this.mActivity.runOnUiThread {
(mDialogView.findViewById<LinearLayout>(R.id.properties_size).property_value as TextView).text = size
if (fileDirItem.isDirectory) {
(mDialogView.findViewById<LinearLayout>(R.id.properties_file_count).property_value as TextView).text = fileCount.toString()
(mDialogView.findViewById<LinearLayout>(R.id.properties_direct_children_count).property_value as TextView).text =
directChildrenCount.toString()
}
}
if (!fileDirItem.isDirectory) {
val projection = arrayOf(MediaStore.Images.Media.DATE_MODIFIED)
val uri = MediaStore.Files.getContentUri("external")
val selection = "${MediaStore.MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(path)
val cursor = mActivity.contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
val dateModified = cursor.getLongValue(MediaStore.Images.Media.DATE_MODIFIED) * 1000L
updateLastModified(mActivity, mDialogView, dateModified)
} else {
updateLastModified(mActivity, mDialogView, fileDirItem.getLastModified(mActivity))
}
}
val exif = if (isNougatPlus() && mActivity.isPathOnOTG(fileDirItem.path)) {
ExifInterface((mActivity as BaseSimpleActivity).getFileInputStreamSync(fileDirItem.path)!!)
} else if (isNougatPlus() && fileDirItem.path.startsWith("content://")) {
try {
ExifInterface(mActivity.contentResolver.openInputStream(Uri.parse(fileDirItem.path))!!)
} catch (e: Exception) {
return@ensureBackgroundThread
}
} else if (mActivity.isRestrictedSAFOnlyRoot(path)) {
try {
ExifInterface(mActivity.contentResolver.openInputStream(mActivity.getAndroidSAFUri(path))!!)
} catch (e: Exception) {
return@ensureBackgroundThread
}
} else {
try {
ExifInterface(fileDirItem.path)
} catch (e: Exception) {
return@ensureBackgroundThread
}
}
val latLon = FloatArray(2)
if (exif.getLatLong(latLon)) {
mActivity.runOnUiThread {
addProperty(R.string.gps_coordinates, "${latLon[0]}, ${latLon[1]}")
}
}
val altitude = exif.getAltitude(0.0)
if (altitude != 0.0) {
mActivity.runOnUiThread {
addProperty(R.string.altitude, "${altitude}m")
}
}
}
}
when {
fileDirItem.isDirectory -> {
addProperty(R.string.direct_children_count, "", R.id.properties_direct_children_count)
addProperty(R.string.files_count, "", R.id.properties_file_count)
}
fileDirItem.path.isImageSlow() -> {
fileDirItem.getResolution(mActivity)?.let { addProperty(R.string.resolution, it.formatAsResolution()) }
}
fileDirItem.path.isAudioSlow() -> {
fileDirItem.getDuration(mActivity)?.let { addProperty(R.string.duration, it) }
fileDirItem.getTitle(mActivity)?.let { addProperty(R.string.song_title, it) }
fileDirItem.getArtist(mActivity)?.let { addProperty(R.string.artist, it) }
fileDirItem.getAlbum(mActivity)?.let { addProperty(R.string.album, it) }
}
fileDirItem.path.isVideoSlow() -> {
fileDirItem.getDuration(mActivity)?.let { addProperty(R.string.duration, it) }
fileDirItem.getResolution(mActivity)?.let { addProperty(R.string.resolution, it.formatAsResolution()) }
fileDirItem.getArtist(mActivity)?.let { addProperty(R.string.artist, it) }
fileDirItem.getAlbum(mActivity)?.let { addProperty(R.string.album, it) }
}
}
if (fileDirItem.isDirectory) {
addProperty(R.string.last_modified, fileDirItem.getLastModified(mActivity).formatDate(mActivity))
} else {
addProperty(R.string.last_modified, "", R.id.properties_last_modified)
try {
addExifProperties(path, mActivity)
} catch (e: Exception) {
mActivity.showErrorToast(e)
return
}
if (mActivity.baseConfig.appId.removeSuffix(".debug") == "com.simplemobiletools.filemanager.pro") {
addProperty(R.string.md5, "", R.id.properties_md5)
ensureBackgroundThread {
val md5 = if (mActivity.isRestrictedSAFOnlyRoot(path)) {
mActivity.contentResolver.openInputStream(mActivity.getAndroidSAFUri(path))?.md5()
} else {
File(path).md5()
}
mActivity.runOnUiThread {
if (md5 != null) {
(mDialogView.findViewById<LinearLayout>(R.id.properties_md5).property_value as TextView).text = md5
} else {
mDialogView.findViewById<LinearLayout>(R.id.properties_md5).beGone()
}
}
}
}
}
}
private fun updateLastModified(activity: Activity, view: View, timestamp: Long) {
activity.runOnUiThread {
(view.findViewById<LinearLayout>(R.id.properties_last_modified).property_value as TextView).text = timestamp.formatDate(activity)
}
}
/**
* A File Properties dialog constructor with an optional parameter, usable at multiple items selected
*
* @param activity request activity to avoid some Theme.AppCompat issues
* @param path the file path
* @param countHiddenItems toggle determining if we will count hidden files themselves and their sizes
*/
constructor(activity: Activity, paths: List<String>, countHiddenItems: Boolean = false) : this() {
mActivity = activity
mInflater = LayoutInflater.from(activity)
mResources = activity.resources
mDialogView = mInflater.inflate(R.layout.dialog_properties, null)
mCountHiddenItems = countHiddenItems
mPropertyView = mDialogView.properties_holder
val fileDirItems = ArrayList<FileDirItem>(paths.size)
paths.forEach {
val fileDirItem = FileDirItem(it, it.getFilenameFromPath(), activity.getIsPathDirectory(it))
fileDirItems.add(fileDirItem)
}
val isSameParent = isSameParent(fileDirItems)
addProperty(R.string.items_selected, paths.size.toString())
if (isSameParent) {
addProperty(R.string.path, fileDirItems[0].getParentPath())
}
addProperty(R.string.size, "", R.id.properties_size)
addProperty(R.string.files_count, "", R.id.properties_file_count)
ensureBackgroundThread {
val fileCount = fileDirItems.sumByInt { it.getProperFileCount(activity, countHiddenItems) }
val size = fileDirItems.sumByLong { it.getProperSize(activity, countHiddenItems) }.formatSize()
activity.runOnUiThread {
(mDialogView.findViewById<LinearLayout>(R.id.properties_size).property_value as TextView).text = size
(mDialogView.findViewById<LinearLayout>(R.id.properties_file_count).property_value as TextView).text = fileCount.toString()
}
}
val builder = activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
if (!paths.any { it.startsWith("content://") } && paths.any { it.canModifyEXIF() } && paths.any { activity.isPathOnInternalStorage(it) }) {
if ((isRPlus() && Environment.isExternalStorageManager()) || (!isRPlus() && activity.hasPermission(PERMISSION_WRITE_STORAGE))) {
builder.setNeutralButton(R.string.remove_exif, null)
}
}
builder.apply {
mActivity.setupDialogStuff(mDialogView, this, R.string.properties) { alertDialog ->
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
removeEXIFFromPaths(paths)
}
}
}
}
private fun addExifProperties(path: String, activity: Activity) {
val exif = if (isNougatPlus() && activity.isPathOnOTG(path)) {
ExifInterface((activity as BaseSimpleActivity).getFileInputStreamSync(path)!!)
} else if (isNougatPlus() && path.startsWith("content://")) {
try {
ExifInterface(activity.contentResolver.openInputStream(Uri.parse(path))!!)
} catch (e: Exception) {
return
}
} else if (activity.isRestrictedSAFOnlyRoot(path)) {
try {
ExifInterface(activity.contentResolver.openInputStream(activity.getAndroidSAFUri(path))!!)
} catch (e: Exception) {
return
}
} else {
ExifInterface(path)
}
val dateTaken = exif.getExifDateTaken(activity)
if (dateTaken.isNotEmpty()) {
addProperty(R.string.date_taken, dateTaken)
}
val cameraModel = exif.getExifCameraModel()
if (cameraModel.isNotEmpty()) {
addProperty(R.string.camera, cameraModel)
}
val exifString = exif.getExifProperties()
if (exifString.isNotEmpty()) {
addProperty(R.string.exif, exifString)
}
}
private fun removeEXIFFromPath(path: String) {
ConfirmationDialog(mActivity, "", R.string.remove_exif_confirmation) {
try {
ExifInterface(path).removeValues()
mActivity.toast(R.string.exif_removed)
mPropertyView.properties_holder.removeAllViews()
addProperties(path)
} catch (e: Exception) {
mActivity.showErrorToast(e)
}
}
}
private fun removeEXIFFromPaths(paths: List<String>) {
ConfirmationDialog(mActivity, "", R.string.remove_exif_confirmation) {
try {
paths.filter { mActivity.isPathOnInternalStorage(it) && it.canModifyEXIF() }.forEach {
ExifInterface(it).removeValues()
}
mActivity.toast(R.string.exif_removed)
} catch (e: Exception) {
mActivity.showErrorToast(e)
}
}
}
private fun isSameParent(fileDirItems: List<FileDirItem>): Boolean {
var parent = fileDirItems[0].getParentPath()
for (file in fileDirItems) {
val curParent = file.getParentPath()
if (curParent != parent) {
return false
}
parent = curParent
}
return true
}
private fun addProperty(labelId: Int, value: String?, viewId: Int = 0) {
if (value == null) {
return
}
mInflater.inflate(R.layout.item_property, mPropertyView, false).apply {
property_value.setTextColor(mActivity.getProperTextColor())
property_label.setTextColor(mActivity.getProperTextColor())
property_label.text = mResources.getString(labelId)
property_value.text = value
mPropertyView.properties_holder.addView(this)
setOnLongClickListener {
mActivity.copyToClipboard(property_value.value)
true
}
if (labelId == R.string.gps_coordinates) {
setOnClickListener {
mActivity.showLocationOnMap(value)
}
}
if (viewId != 0) {
id = viewId
}
}
}
}

View File

@ -0,0 +1,30 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import android.text.method.LinkMovementMethod
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_purchase_thank_you.view.*
class PurchaseThankYouDialog(val activity: Activity) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_purchase_thank_you, null).apply {
var text = activity.getString(R.string.purchase_thank_you)
if (activity.baseConfig.appId.removeSuffix(".debug").endsWith(".pro")) {
text += "<br><br>${activity.getString(R.string.shared_theme_note)}"
}
purchase_thank_you.text = Html.fromHtml(text)
purchase_thank_you.movementMethod = LinkMovementMethod.getInstance()
purchase_thank_you.removeUnderlines()
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.purchase) { dialog, which -> activity.launchPurchaseThankYouIntent() }
.setNegativeButton(R.string.later, null)
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false)
}
}
}

View File

@ -0,0 +1,73 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.onGlobalLayout
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.models.RadioItem
import kotlinx.android.synthetic.main.dialog_radio_group.view.*
class RadioGroupDialog(
val activity: Activity, val items: ArrayList<RadioItem>, val checkedItemId: Int = -1, val titleId: Int = 0,
showOKButton: Boolean = false, val cancelCallback: (() -> Unit)? = null, val callback: (newValue: Any) -> Unit
) {
private var dialog: AlertDialog? = null
private var wasInit = false
private var selectedItemId = -1
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_radio_group, null)
view.dialog_radio_group.apply {
for (i in 0 until items.size) {
val radioButton = (activity.layoutInflater.inflate(R.layout.radio_button, null) as RadioButton).apply {
text = items[i].title
isChecked = items[i].id == checkedItemId
id = i
setOnClickListener { itemSelected(i) }
}
if (items[i].id == checkedItemId) {
selectedItemId = i
}
addView(radioButton, RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
}
}
val builder = activity.getAlertDialogBuilder()
.setOnCancelListener { cancelCallback?.invoke() }
if (selectedItemId != -1 && showOKButton) {
builder.setPositiveButton(R.string.ok) { dialog, which -> itemSelected(selectedItemId) }
}
builder.apply {
activity.setupDialogStuff(view, this, titleId) { alertDialog ->
dialog = alertDialog
}
}
if (selectedItemId != -1) {
view.dialog_radio_holder.apply {
onGlobalLayout {
scrollY = view.dialog_radio_group.findViewById<View>(selectedItemId).bottom - height
}
}
}
wasInit = true
}
private fun itemSelected(checkedId: Int) {
if (wasInit) {
callback(items[checkedId].value)
dialog?.dismiss()
}
}
}

View File

@ -0,0 +1,46 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_rate_stars.view.*
class RateStarsDialog(val activity: Activity) {
private var dialog: AlertDialog? = null
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_rate_stars, null).apply {
val primaryColor = activity.getProperPrimaryColor()
arrayOf(rate_star_1, rate_star_2, rate_star_3, rate_star_4, rate_star_5).forEach {
it.applyColorFilter(primaryColor)
}
rate_star_1.setOnClickListener { dialogCancelled(true) }
rate_star_2.setOnClickListener { dialogCancelled(true) }
rate_star_3.setOnClickListener { dialogCancelled(true) }
rate_star_4.setOnClickListener { dialogCancelled(true) }
rate_star_5.setOnClickListener {
activity.redirectToRateUs()
dialogCancelled(true)
}
}
activity.getAlertDialogBuilder()
.setNegativeButton(R.string.later) { dialog, which -> dialogCancelled(false) }
.setOnCancelListener { dialogCancelled(false) }
.apply {
activity.setupDialogStuff(view, this, cancelOnTouchOutside = false) { alertDialog ->
dialog = alertDialog
}
}
}
private fun dialogCancelled(showThankYou: Boolean) {
dialog?.dismiss()
if (showThankYou) {
activity.toast(R.string.thank_you)
activity.baseConfig.wasAppRated = true
}
}
}

View File

@ -0,0 +1,24 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.content.ActivityNotFoundException
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getStoreUrl
import com.simplemobiletools.commons.extensions.launchViewIntent
class RateUsDialog(val activity: Activity) {
init {
ConfirmationDialog(activity, "", R.string.rate_us_prompt, R.string.rate, R.string.cancel) {
launchGooglePlay()
}
}
private fun launchGooglePlay() {
try {
activity.launchViewIntent("market://details?id=${activity.packageName.removeSuffix(".debug")}")
} catch (ignored: ActivityNotFoundException) {
activity.launchViewIntent(activity.getStoreUrl())
}
}
}

View File

@ -0,0 +1,76 @@
package com.simplemobiletools.commons.dialogs
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.adapters.RenameAdapter
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.RENAME_PATTERN
import com.simplemobiletools.commons.helpers.RENAME_SIMPLE
import com.simplemobiletools.commons.views.MyViewPager
import kotlinx.android.synthetic.main.dialog_rename.view.*
class RenameDialog(val activity: BaseSimpleActivity, val paths: ArrayList<String>, val useMediaFileExtension: Boolean, val callback: () -> Unit) {
var dialog: AlertDialog? = null
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_rename, null)
var tabsAdapter: RenameAdapter
var viewPager: MyViewPager
init {
view.apply {
viewPager = findViewById(R.id.dialog_tab_view_pager)
tabsAdapter = RenameAdapter(activity, paths)
viewPager.adapter = tabsAdapter
viewPager.onPageChangeListener {
dialog_tab_layout.getTabAt(it)!!.select()
}
viewPager.currentItem = activity.baseConfig.lastRenameUsed
if (activity.baseConfig.isUsingSystemTheme) {
dialog_tab_layout.setBackgroundColor(activity.resources.getColor(R.color.you_dialog_background_color))
} else {
dialog_tab_layout.setBackgroundColor(context.getProperBackgroundColor())
}
val textColor = context.getProperTextColor()
dialog_tab_layout.setTabTextColors(textColor, textColor)
dialog_tab_layout.setSelectedTabIndicatorColor(context.getProperPrimaryColor())
if (activity.baseConfig.isUsingSystemTheme) {
dialog_tab_layout.setBackgroundColor(activity.resources.getColor(R.color.you_dialog_background_color))
}
dialog_tab_layout.onTabSelectionChanged(tabSelectedAction = {
viewPager.currentItem = when {
it.text.toString().equals(resources.getString(R.string.simple_renaming), true) -> RENAME_SIMPLE
else -> RENAME_PATTERN
}
})
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel) { dialog, which -> dismissDialog() }
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
alertDialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
tabsAdapter.dialogConfirmed(useMediaFileExtension, viewPager.currentItem) {
dismissDialog()
if (it) {
activity.baseConfig.lastRenameUsed = viewPager.currentItem
callback()
}
}
}
}
}
}
private fun dismissDialog() {
dialog?.dismiss()
}
}

View File

@ -0,0 +1,88 @@
package com.simplemobiletools.commons.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_rename_item.view.*
class RenameItemDialog(val activity: BaseSimpleActivity, val path: String, val callback: (newPath: String) -> Unit) {
init {
var ignoreClicks = false
val fullName = path.getFilenameFromPath()
val dotAt = fullName.lastIndexOf(".")
var name = fullName
val view = activity.layoutInflater.inflate(R.layout.dialog_rename_item, null).apply {
if (dotAt > 0 && !activity.getIsPathDirectory(path)) {
name = fullName.substring(0, dotAt)
val extension = fullName.substring(dotAt + 1)
rename_item_extension.setText(extension)
} else {
rename_item_extension_hint.beGone()
}
rename_item_name.setText(name)
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.rename) { alertDialog ->
alertDialog.showKeyboard(view.rename_item_name)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (ignoreClicks) {
return@setOnClickListener
}
var newName = view.rename_item_name.value
val newExtension = view.rename_item_extension.value
if (newName.isEmpty()) {
activity.toast(R.string.empty_name)
return@setOnClickListener
}
if (!newName.isAValidFilename()) {
activity.toast(R.string.invalid_name)
return@setOnClickListener
}
val updatedPaths = ArrayList<String>()
updatedPaths.add(path)
if (!newExtension.isEmpty()) {
newName += ".$newExtension"
}
if (!activity.getDoesFilePathExist(path)) {
activity.toast(String.format(activity.getString(R.string.source_file_doesnt_exist), path))
return@setOnClickListener
}
val newPath = "${path.getParentPath()}/$newName"
if (path == newPath) {
activity.toast(R.string.name_taken)
return@setOnClickListener
}
if (!path.equals(newPath, ignoreCase = true) && activity.getDoesFilePathExist(newPath)) {
activity.toast(R.string.name_taken)
return@setOnClickListener
}
updatedPaths.add(newPath)
ignoreClicks = true
activity.renameFile(path, newPath, false) { success, _ ->
ignoreClicks = false
if (success) {
callback(newPath)
alertDialog.dismiss()
}
}
}
}
}
}
}

View File

@ -0,0 +1,95 @@
package com.simplemobiletools.commons.dialogs
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_rename_items.view.*
// used at renaming folders
class RenameItemsDialog(val activity: BaseSimpleActivity, val paths: ArrayList<String>, val callback: () -> Unit) {
init {
var ignoreClicks = false
val view = activity.layoutInflater.inflate(R.layout.dialog_rename_items, null)
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.rename) { alertDialog ->
alertDialog.showKeyboard(view.rename_items_value)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (ignoreClicks) {
return@setOnClickListener
}
val valueToAdd = view.rename_items_value.text.toString()
val append = view.rename_items_radio_group.checkedRadioButtonId == view.rename_items_radio_append.id
if (valueToAdd.isEmpty()) {
callback()
alertDialog.dismiss()
return@setOnClickListener
}
if (!valueToAdd.isAValidFilename()) {
activity.toast(R.string.invalid_name)
return@setOnClickListener
}
val validPaths = paths.filter { activity.getDoesFilePathExist(it) }
val sdFilePath = validPaths.firstOrNull { activity.isPathOnSD(it) } ?: validPaths.firstOrNull()
if (sdFilePath == null) {
activity.toast(R.string.unknown_error_occurred)
alertDialog.dismiss()
return@setOnClickListener
}
activity.handleSAFDialog(sdFilePath) {
if (!it) {
return@handleSAFDialog
}
ignoreClicks = true
var pathsCnt = validPaths.size
for (path in validPaths) {
val fullName = path.getFilenameFromPath()
var dotAt = fullName.lastIndexOf(".")
if (dotAt == -1) {
dotAt = fullName.length
}
val name = fullName.substring(0, dotAt)
val extension = if (fullName.contains(".")) ".${fullName.getFilenameExtension()}" else ""
val newName = if (append) {
"$name$valueToAdd$extension"
} else {
"$valueToAdd$fullName"
}
val newPath = "${path.getParentPath()}/$newName"
if (activity.getDoesFilePathExist(newPath)) {
continue
}
activity.renameFile(path, newPath, true) { success, _ ->
if (success) {
pathsCnt--
if (pathsCnt == 0) {
callback()
alertDialog.dismiss()
}
} else {
ignoreClicks = false
alertDialog.dismiss()
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,118 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.biometric.auth.AuthPromptHost
import androidx.fragment.app.FragmentActivity
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.adapters.PasswordTypesAdapter
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.interfaces.HashListener
import com.simplemobiletools.commons.views.MyDialogViewPager
import kotlinx.android.synthetic.main.dialog_security.view.*
class SecurityDialog(
private val activity: Activity,
private val requiredHash: String,
private val showTabIndex: Int,
private val callback: (hash: String, type: Int, success: Boolean) -> Unit
) : HashListener {
private var dialog: AlertDialog? = null
private val view = LayoutInflater.from(activity).inflate(R.layout.dialog_security, null)
private var tabsAdapter: PasswordTypesAdapter
private var viewPager: MyDialogViewPager
init {
view.apply {
viewPager = findViewById(R.id.dialog_tab_view_pager)
viewPager.offscreenPageLimit = 2
tabsAdapter = PasswordTypesAdapter(
context = context,
requiredHash = requiredHash,
hashListener = this@SecurityDialog,
scrollView = dialog_scrollview,
biometricPromptHost = AuthPromptHost(activity as FragmentActivity),
showBiometricIdTab = shouldShowBiometricIdTab(),
showBiometricAuthentication = showTabIndex == PROTECTION_FINGERPRINT && isRPlus()
)
viewPager.adapter = tabsAdapter
viewPager.onPageChangeListener {
dialog_tab_layout.getTabAt(it)?.select()
}
viewPager.onGlobalLayout {
updateTabVisibility()
}
if (showTabIndex == SHOW_ALL_TABS) {
val textColor = context.getProperTextColor()
if (shouldShowBiometricIdTab()) {
val tabTitle = if (isRPlus()) R.string.biometrics else R.string.fingerprint
dialog_tab_layout.addTab(dialog_tab_layout.newTab().setText(tabTitle), PROTECTION_FINGERPRINT)
}
if (activity.baseConfig.isUsingSystemTheme) {
dialog_tab_layout.setBackgroundColor(activity.resources.getColor(R.color.you_dialog_background_color))
} else {
dialog_tab_layout.setBackgroundColor(context.getProperBackgroundColor())
}
dialog_tab_layout.setTabTextColors(textColor, textColor)
dialog_tab_layout.setSelectedTabIndicatorColor(context.getProperPrimaryColor())
dialog_tab_layout.onTabSelectionChanged(tabSelectedAction = {
viewPager.currentItem = when {
it.text.toString().equals(resources.getString(R.string.pattern), true) -> PROTECTION_PATTERN
it.text.toString().equals(resources.getString(R.string.pin), true) -> PROTECTION_PIN
else -> PROTECTION_FINGERPRINT
}
updateTabVisibility()
})
} else {
dialog_tab_layout.beGone()
viewPager.currentItem = showTabIndex
viewPager.allowSwiping = false
}
}
activity.getAlertDialogBuilder()
.setOnCancelListener { onCancelFail() }
.setNegativeButton(R.string.cancel) { _, _ -> onCancelFail() }
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
}
}
}
private fun onCancelFail() {
callback("", 0, false)
dialog?.dismiss()
}
override fun receivedHash(hash: String, type: Int) {
callback(hash, type, true)
if (!activity.isFinishing) {
try {
dialog?.dismiss()
} catch (ignored: Exception) {
}
}
}
private fun updateTabVisibility() {
for (i in 0..2) {
tabsAdapter.isTabVisible(i, viewPager.currentItem == i)
}
}
private fun shouldShowBiometricIdTab(): Boolean {
return if (isRPlus()) {
activity.isBiometricIdAvailable()
} else {
activity.isFingerPrintSensorAvailable()
}
}
}

View File

@ -0,0 +1,166 @@
package com.simplemobiletools.commons.dialogs
import android.content.ActivityNotFoundException
import android.content.Intent
import android.media.MediaPlayer
import android.net.Uri
import android.view.ViewGroup
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SILENT
import com.simplemobiletools.commons.models.AlarmSound
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.views.MyCompatRadioButton
import kotlinx.android.synthetic.main.dialog_select_alarm_sound.view.*
class SelectAlarmSoundDialog(
val activity: BaseSimpleActivity, val currentUri: String, val audioStream: Int, val pickAudioIntentId: Int,
val type: Int, val loopAudio: Boolean, val onAlarmPicked: (alarmSound: AlarmSound?) -> Unit,
val onAlarmSoundDeleted: (alarmSound: AlarmSound) -> Unit
) {
private val ADD_NEW_SOUND_ID = -2
private val view = activity.layoutInflater.inflate(R.layout.dialog_select_alarm_sound, null)
private var systemAlarmSounds = ArrayList<AlarmSound>()
private var yourAlarmSounds = ArrayList<AlarmSound>()
private var mediaPlayer: MediaPlayer? = null
private val config = activity.baseConfig
private var dialog: AlertDialog? = null
init {
activity.getAlarmSounds(type) {
systemAlarmSounds = it
gotSystemAlarms()
}
view.dialog_select_alarm_your_label.setTextColor(activity.getProperPrimaryColor())
view.dialog_select_alarm_system_label.setTextColor(activity.getProperPrimaryColor())
addYourAlarms()
activity.getAlertDialogBuilder()
.setOnDismissListener { mediaPlayer?.stop() }
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
alertDialog.window?.volumeControlStream = audioStream
}
}
}
private fun addYourAlarms() {
view.dialog_select_alarm_your_radio.removeAllViews()
val token = object : TypeToken<ArrayList<AlarmSound>>() {}.type
yourAlarmSounds = Gson().fromJson<ArrayList<AlarmSound>>(config.yourAlarmSounds, token) ?: ArrayList()
yourAlarmSounds.add(AlarmSound(ADD_NEW_SOUND_ID, activity.getString(R.string.add_new_sound), ""))
yourAlarmSounds.forEach {
addAlarmSound(it, view.dialog_select_alarm_your_radio)
}
}
private fun gotSystemAlarms() {
systemAlarmSounds.forEach {
addAlarmSound(it, view.dialog_select_alarm_system_radio)
}
}
private fun addAlarmSound(alarmSound: AlarmSound, holder: ViewGroup) {
val radioButton = (activity.layoutInflater.inflate(R.layout.item_select_alarm_sound, null) as MyCompatRadioButton).apply {
text = alarmSound.title
isChecked = alarmSound.uri == currentUri
id = alarmSound.id
setColors(activity.getProperTextColor(), activity.getProperPrimaryColor(), activity.getProperBackgroundColor())
setOnClickListener {
alarmClicked(alarmSound)
if (holder == view.dialog_select_alarm_system_radio) {
view.dialog_select_alarm_your_radio.clearCheck()
} else {
view.dialog_select_alarm_system_radio.clearCheck()
}
}
if (alarmSound.id != -2 && holder == view.dialog_select_alarm_your_radio) {
setOnLongClickListener {
val items = arrayListOf(RadioItem(1, context.getString(R.string.remove)))
RadioGroupDialog(activity, items) {
removeAlarmSound(alarmSound)
}
true
}
}
}
holder.addView(radioButton, RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
}
private fun alarmClicked(alarmSound: AlarmSound) {
when {
alarmSound.uri == SILENT -> mediaPlayer?.stop()
alarmSound.id == ADD_NEW_SOUND_ID -> {
val action = Intent.ACTION_OPEN_DOCUMENT
val intent = Intent(action).apply {
type = "audio/*"
flags = flags or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
}
try {
activity.startActivityForResult(intent, pickAudioIntentId)
} catch (e: ActivityNotFoundException) {
activity.toast(R.string.no_app_found)
}
dialog?.dismiss()
}
else -> try {
mediaPlayer?.reset()
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer().apply {
setAudioStreamType(audioStream)
isLooping = loopAudio
}
}
mediaPlayer?.apply {
setDataSource(activity, Uri.parse(alarmSound.uri))
prepare()
start()
}
} catch (e: Exception) {
activity.showErrorToast(e)
}
}
}
private fun removeAlarmSound(alarmSound: AlarmSound) {
val token = object : TypeToken<ArrayList<AlarmSound>>() {}.type
yourAlarmSounds = Gson().fromJson<ArrayList<AlarmSound>>(config.yourAlarmSounds, token) ?: ArrayList()
yourAlarmSounds.remove(alarmSound)
config.yourAlarmSounds = Gson().toJson(yourAlarmSounds)
addYourAlarms()
if (alarmSound.id == view.dialog_select_alarm_your_radio.checkedRadioButtonId) {
view.dialog_select_alarm_your_radio.clearCheck()
view.dialog_select_alarm_system_radio.check(systemAlarmSounds.firstOrNull()?.id ?: 0)
}
onAlarmSoundDeleted(alarmSound)
}
private fun dialogConfirmed() {
if (view.dialog_select_alarm_your_radio.checkedRadioButtonId != -1) {
val checkedId = view.dialog_select_alarm_your_radio.checkedRadioButtonId
onAlarmPicked(yourAlarmSounds.firstOrNull { it.id == checkedId })
} else {
val checkedId = view.dialog_select_alarm_system_radio.checkedRadioButtonId
onAlarmPicked(systemAlarmSounds.firstOrNull { it.id == checkedId })
}
}
}

View File

@ -0,0 +1,146 @@
package com.simplemobiletools.commons.dialogs
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_radio_group.view.*
/**
* A dialog for choosing between internal, root, SD card (optional) storage
*
* @param activity has to be activity to avoid some Theme.AppCompat issues
* @param currPath current path to decide which storage should be preselected
* @param pickSingleOption if only one option like "Internal" is available, select it automatically
* @param callback an anonymous function
*
*/
class StoragePickerDialog(
val activity: BaseSimpleActivity, val currPath: String, val showRoot: Boolean, pickSingleOption: Boolean,
val callback: (pickedPath: String) -> Unit
) {
private val ID_INTERNAL = 1
private val ID_SD = 2
private val ID_OTG = 3
private val ID_ROOT = 4
private lateinit var radioGroup: RadioGroup
private var dialog: AlertDialog? = null
private var defaultSelectedId = 0
private val availableStorages = ArrayList<String>()
init {
availableStorages.add(activity.internalStoragePath)
when {
activity.hasExternalSDCard() -> availableStorages.add(activity.sdCardPath)
activity.hasOTGConnected() -> availableStorages.add("otg")
showRoot -> availableStorages.add("root")
}
if (pickSingleOption && availableStorages.size == 1) {
callback(availableStorages.first())
} else {
initDialog()
}
}
private fun initDialog() {
val inflater = LayoutInflater.from(activity)
val resources = activity.resources
val layoutParams = RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val view = inflater.inflate(R.layout.dialog_radio_group, null)
radioGroup = view.dialog_radio_group
val basePath = currPath.getBasePath(activity)
val internalButton = inflater.inflate(R.layout.radio_button, null) as RadioButton
internalButton.apply {
id = ID_INTERNAL
text = resources.getString(R.string.internal)
isChecked = basePath == context.internalStoragePath
setOnClickListener { internalPicked() }
if (isChecked) {
defaultSelectedId = id
}
}
radioGroup.addView(internalButton, layoutParams)
if (activity.hasExternalSDCard()) {
val sdButton = inflater.inflate(R.layout.radio_button, null) as RadioButton
sdButton.apply {
id = ID_SD
text = resources.getString(R.string.sd_card)
isChecked = basePath == context.sdCardPath
setOnClickListener { sdPicked() }
if (isChecked) {
defaultSelectedId = id
}
}
radioGroup.addView(sdButton, layoutParams)
}
if (activity.hasOTGConnected()) {
val otgButton = inflater.inflate(R.layout.radio_button, null) as RadioButton
otgButton.apply {
id = ID_OTG
text = resources.getString(R.string.usb)
isChecked = basePath == context.otgPath
setOnClickListener { otgPicked() }
if (isChecked) {
defaultSelectedId = id
}
}
radioGroup.addView(otgButton, layoutParams)
}
// allow for example excluding the root folder at the gallery
if (showRoot) {
val rootButton = inflater.inflate(R.layout.radio_button, null) as RadioButton
rootButton.apply {
id = ID_ROOT
text = resources.getString(R.string.root)
isChecked = basePath == "/"
setOnClickListener { rootPicked() }
if (isChecked) {
defaultSelectedId = id
}
}
radioGroup.addView(rootButton, layoutParams)
}
activity.getAlertDialogBuilder().apply {
activity.setupDialogStuff(view, this, R.string.select_storage) { alertDialog ->
dialog = alertDialog
}
}
}
private fun internalPicked() {
dialog?.dismiss()
callback(activity.internalStoragePath)
}
private fun sdPicked() {
dialog?.dismiss()
callback(activity.sdCardPath)
}
private fun otgPicked() {
activity.handleOTGPermission {
if (it) {
callback(activity.otgPath)
dialog?.dismiss()
} else {
radioGroup.check(defaultSelectedId)
}
}
}
private fun rootPicked() {
dialog?.dismiss()
callback("/")
}
}

View File

@ -0,0 +1,39 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.launchUpgradeToProIntent
import com.simplemobiletools.commons.extensions.launchViewIntent
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_upgrade_to_pro.view.*
class UpgradeToProDialog(val activity: Activity) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_upgrade_to_pro, null).apply {
upgrade_to_pro.text = activity.getString(R.string.upgrade_to_pro_long)
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.upgrade) { dialog, which -> upgradeApp() }
.setNeutralButton(R.string.more_info, null) // do not dismiss the dialog on pressing More Info
.setNegativeButton(R.string.later, null)
.apply {
activity.setupDialogStuff(view, this, R.string.upgrade_to_pro, cancelOnTouchOutside = false) { alertDialog ->
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
moreInfo()
}
}
}
}
private fun upgradeApp() {
activity.launchUpgradeToProIntent()
}
private fun moreInfo() {
activity.launchViewIntent("https://simplemobiletools.com/upgrade_to_pro")
}
}

View File

@ -0,0 +1,35 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.view.LayoutInflater
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.models.Release
import kotlinx.android.synthetic.main.dialog_whats_new.view.*
class WhatsNewDialog(val activity: Activity, val releases: List<Release>) {
init {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_whats_new, null)
view.whats_new_content.text = getNewReleases()
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok, null)
.apply {
activity.setupDialogStuff(view, this, R.string.whats_new, cancelOnTouchOutside = false)
}
}
private fun getNewReleases(): String {
val sb = StringBuilder()
releases.forEach {
val parts = activity.getString(it.textId).split("\n").map(String::trim)
parts.forEach {
sb.append("- $it\n")
}
}
return sb.toString()
}
}

View File

@ -0,0 +1,81 @@
package com.simplemobiletools.commons.dialogs
import android.app.Activity
import android.text.Html
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.humanizePath
import com.simplemobiletools.commons.extensions.setupDialogStuff
import kotlinx.android.synthetic.main.dialog_write_permission.view.*
import kotlinx.android.synthetic.main.dialog_write_permission_otg.view.*
class WritePermissionDialog(activity: Activity, val mode: Mode, val callback: () -> Unit) {
sealed class Mode {
object Otg : Mode()
object SdCard : Mode()
data class OpenDocumentTreeSDK30(val path: String) : Mode()
object CreateDocumentSDK30 : Mode()
}
private var dialog: AlertDialog? = null
init {
val layout = if (mode == Mode.SdCard) R.layout.dialog_write_permission else R.layout.dialog_write_permission_otg
val view = activity.layoutInflater.inflate(layout, null)
var dialogTitle = R.string.confirm_storage_access_title
val glide = Glide.with(activity)
val crossFade = DrawableTransitionOptions.withCrossFade()
when (mode) {
Mode.Otg -> {
view.write_permissions_dialog_otg_text.setText(R.string.confirm_usb_storage_access_text)
glide.load(R.drawable.img_write_storage_otg).transition(crossFade).into(view.write_permissions_dialog_otg_image)
}
Mode.SdCard -> {
glide.load(R.drawable.img_write_storage).transition(crossFade).into(view.write_permissions_dialog_image)
glide.load(R.drawable.img_write_storage_sd).transition(crossFade).into(view.write_permissions_dialog_image_sd)
}
is Mode.OpenDocumentTreeSDK30 -> {
dialogTitle = R.string.confirm_folder_access_title
val humanizedPath = activity.humanizePath(mode.path)
view.write_permissions_dialog_otg_text.text =
Html.fromHtml(activity.getString(R.string.confirm_storage_access_android_text_specific, humanizedPath))
glide.load(R.drawable.img_write_storage_sdk_30).transition(crossFade).into(view.write_permissions_dialog_otg_image)
view.write_permissions_dialog_otg_image.setOnClickListener {
dialogConfirmed()
}
}
Mode.CreateDocumentSDK30 -> {
dialogTitle = R.string.confirm_folder_access_title
view.write_permissions_dialog_otg_text.text = Html.fromHtml(activity.getString(R.string.confirm_create_doc_for_new_folder_text))
glide.load(R.drawable.img_write_storage_create_doc_sdk_30).transition(crossFade).into(view.write_permissions_dialog_otg_image)
view.write_permissions_dialog_otg_image.setOnClickListener {
dialogConfirmed()
}
}
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() }
.setOnCancelListener {
BaseSimpleActivity.funAfterSAFPermission?.invoke(false)
BaseSimpleActivity.funAfterSAFPermission = null
}
.apply {
activity.setupDialogStuff(view, this, dialogTitle) { alertDialog ->
dialog = alertDialog
}
}
}
private fun dialogConfirmed() {
dialog?.dismiss()
callback()
}
}

View File

@ -0,0 +1,77 @@
package com.simplemobiletools.commons.extensions
import android.content.ContentValues
import android.provider.MediaStore
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.models.FileDirItem
import java.io.File
import java.io.InputStream
import java.io.OutputStream
fun BaseSimpleActivity.copySingleFileSdk30(source: FileDirItem, destination: FileDirItem): Boolean {
val directory = destination.getParentPath()
if (!createDirectorySync(directory)) {
val error = String.format(getString(R.string.could_not_create_folder), directory)
showErrorToast(error)
return false
}
var inputStream: InputStream? = null
var out: OutputStream? = null
try {
out = getFileOutputStreamSync(destination.path, source.path.getMimeType())
inputStream = getFileInputStreamSync(source.path)!!
var copiedSize = 0L
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream.read(buffer)
while (bytes >= 0) {
out!!.write(buffer, 0, bytes)
copiedSize += bytes
bytes = inputStream.read(buffer)
}
out?.flush()
return if (source.size == copiedSize && getDoesFilePathExist(destination.path)) {
if (baseConfig.keepLastModified) {
copyOldLastModified(source.path, destination.path)
val lastModified = File(source.path).lastModified()
if (lastModified != 0L) {
File(destination.path).setLastModified(lastModified)
}
}
true
} else {
false
}
} finally {
inputStream?.close()
out?.close()
}
}
fun BaseSimpleActivity.copyOldLastModified(sourcePath: String, destinationPath: String) {
val projection = arrayOf(MediaStore.Images.Media.DATE_TAKEN, MediaStore.Images.Media.DATE_MODIFIED)
val uri = MediaStore.Files.getContentUri("external")
val selection = "${MediaStore.MediaColumns.DATA} = ?"
var selectionArgs = arrayOf(sourcePath)
val cursor = applicationContext.contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
val dateTaken = cursor.getLongValue(MediaStore.Images.Media.DATE_TAKEN)
val dateModified = cursor.getIntValue(MediaStore.Images.Media.DATE_MODIFIED)
val values = ContentValues().apply {
put(MediaStore.Images.Media.DATE_TAKEN, dateTaken)
put(MediaStore.Images.Media.DATE_MODIFIED, dateModified)
}
selectionArgs = arrayOf(destinationPath)
applicationContext.contentResolver.update(uri, values, selection, selectionArgs)
}
}
}

View File

@ -0,0 +1,410 @@
package com.simplemobiletools.commons.extensions
import android.app.Activity
import android.graphics.Color
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.helpers.DARK_GREY
fun Activity.getThemeId(color: Int = baseConfig.primaryColor, showTransparentTop: Boolean = false) = when {
baseConfig.isUsingSystemTheme -> if (isUsingSystemDarkTheme()) R.style.AppTheme_Base_System else R.style.AppTheme_Base_System_Light
isBlackAndWhiteTheme() -> when {
showTransparentTop -> R.style.AppTheme_BlackAndWhite_NoActionBar
baseConfig.primaryColor.getContrastColor() == DARK_GREY -> R.style.AppTheme_BlackAndWhite_DarkTextColor
else -> R.style.AppTheme_BlackAndWhite
}
isWhiteTheme() -> when {
showTransparentTop -> R.style.AppTheme_White_NoActionBar
baseConfig.primaryColor.getContrastColor() == Color.WHITE -> R.style.AppTheme_White_LightTextColor
else -> R.style.AppTheme_White
}
showTransparentTop -> {
when (color) {
-12846 -> R.style.AppTheme_Red_100_core
-1074534 -> R.style.AppTheme_Red_200_core
-1739917 -> R.style.AppTheme_Red_300_core
-1092784 -> R.style.AppTheme_Red_400_core
-769226 -> R.style.AppTheme_Red_500_core
-1754827 -> R.style.AppTheme_Red_600_core
-2937041 -> R.style.AppTheme_Red_700_core
-3790808 -> R.style.AppTheme_Red_800_core
-4776932 -> R.style.AppTheme_Red_900_core
-476208 -> R.style.AppTheme_Pink_100_core
-749647 -> R.style.AppTheme_Pink_200_core
-1023342 -> R.style.AppTheme_Pink_300_core
-1294214 -> R.style.AppTheme_Pink_400_core
-1499549 -> R.style.AppTheme_Pink_500_core
-2614432 -> R.style.AppTheme_Pink_600_core
-4056997 -> R.style.AppTheme_Pink_700_core
-5434281 -> R.style.AppTheme_Pink_800_core
-7860657 -> R.style.AppTheme_Pink_900_core
-1982745 -> R.style.AppTheme_Purple_100_core
-3238952 -> R.style.AppTheme_Purple_200_core
-4560696 -> R.style.AppTheme_Purple_300_core
-5552196 -> R.style.AppTheme_Purple_400_core
-6543440 -> R.style.AppTheme_Purple_500_core
-7461718 -> R.style.AppTheme_Purple_600_core
-8708190 -> R.style.AppTheme_Purple_700_core
-9823334 -> R.style.AppTheme_Purple_800_core
-11922292 -> R.style.AppTheme_Purple_900_core
-3029783 -> R.style.AppTheme_Deep_Purple_100_core
-5005861 -> R.style.AppTheme_Deep_Purple_200_core
-6982195 -> R.style.AppTheme_Deep_Purple_300_core
-8497214 -> R.style.AppTheme_Deep_Purple_400_core
-10011977 -> R.style.AppTheme_Deep_Purple_500_core
-10603087 -> R.style.AppTheme_Deep_Purple_600_core
-11457112 -> R.style.AppTheme_Deep_Purple_700_core
-12245088 -> R.style.AppTheme_Deep_Purple_800_core
-13558894 -> R.style.AppTheme_Deep_Purple_900_core
-3814679 -> R.style.AppTheme_Indigo_100_core
-6313766 -> R.style.AppTheme_Indigo_200_core
-8812853 -> R.style.AppTheme_Indigo_300_core
-10720320 -> R.style.AppTheme_Indigo_400_core
-12627531 -> R.style.AppTheme_Indigo_500_core
-13022805 -> R.style.AppTheme_Indigo_600_core
-13615201 -> R.style.AppTheme_Indigo_700_core
-14142061 -> R.style.AppTheme_Indigo_800_core
-15064194 -> R.style.AppTheme_Indigo_900_core
-4464901 -> R.style.AppTheme_Blue_100_core
-7288071 -> R.style.AppTheme_Blue_200_core
-10177034 -> R.style.AppTheme_Blue_300_core
-12409355 -> R.style.AppTheme_Blue_400_core
-14575885 -> R.style.AppTheme_Blue_500_core
-14776091 -> R.style.AppTheme_Blue_600_core
-15108398 -> R.style.AppTheme_Blue_700_core
-15374912 -> R.style.AppTheme_Blue_800_core
-15906911 -> R.style.AppTheme_Blue_900_core
-4987396 -> R.style.AppTheme_Light_Blue_100_core
-8268550 -> R.style.AppTheme_Light_Blue_200_core
-11549705 -> R.style.AppTheme_Light_Blue_300_core
-14043396 -> R.style.AppTheme_Light_Blue_400_core
-16537100 -> R.style.AppTheme_Light_Blue_500_core
-16540699 -> R.style.AppTheme_Light_Blue_600_core
-16611119 -> R.style.AppTheme_Light_Blue_700_core
-16615491 -> R.style.AppTheme_Light_Blue_800_core
-16689253 -> R.style.AppTheme_Light_Blue_900_core
-5051406 -> R.style.AppTheme_Cyan_100_core
-8331542 -> R.style.AppTheme_Cyan_200_core
-11677471 -> R.style.AppTheme_Cyan_300_core
-14235942 -> R.style.AppTheme_Cyan_400_core
-16728876 -> R.style.AppTheme_Cyan_500_core
-16732991 -> R.style.AppTheme_Cyan_600_core
-16738393 -> R.style.AppTheme_Cyan_700_core
-16743537 -> R.style.AppTheme_Cyan_800_core
-16752540 -> R.style.AppTheme_Cyan_900_core
-5054501 -> R.style.AppTheme_Teal_100_core
-8336444 -> R.style.AppTheme_Teal_200_core
-11684180 -> R.style.AppTheme_Teal_300_core
-14244198 -> R.style.AppTheme_Teal_400_core
-16738680 -> R.style.AppTheme_Teal_500_core
-16742021 -> R.style.AppTheme_Teal_600_core
-16746133 -> R.style.AppTheme_Teal_700_core
-16750244 -> R.style.AppTheme_Teal_800_core
-16757440 -> R.style.AppTheme_Teal_900_core
-3610935 -> R.style.AppTheme_Green_100_core
-5908825 -> R.style.AppTheme_Green_200_core
-8271996 -> R.style.AppTheme_Green_300_core
-10044566 -> R.style.AppTheme_Green_400_core
-11751600 -> R.style.AppTheme_Green_500_core
-12345273 -> R.style.AppTheme_Green_600_core
-13070788 -> R.style.AppTheme_Green_700_core
-13730510 -> R.style.AppTheme_Green_800_core
-14983648 -> R.style.AppTheme_Green_900_core
-2298424 -> R.style.AppTheme_Light_Green_100_core
-3808859 -> R.style.AppTheme_Light_Green_200_core
-5319295 -> R.style.AppTheme_Light_Green_300_core
-6501275 -> R.style.AppTheme_Light_Green_400_core
-7617718 -> R.style.AppTheme_Light_Green_500_core
-8604862 -> R.style.AppTheme_Light_Green_600_core
-9920712 -> R.style.AppTheme_Light_Green_700_core
-11171025 -> R.style.AppTheme_Light_Green_800_core
-13407970 -> R.style.AppTheme_Light_Green_900_core
-985917 -> R.style.AppTheme_Lime_100_core
-1642852 -> R.style.AppTheme_Lime_200_core
-2300043 -> R.style.AppTheme_Lime_300_core
-2825897 -> R.style.AppTheme_Lime_400_core
-3285959 -> R.style.AppTheme_Lime_500_core
-4142541 -> R.style.AppTheme_Lime_600_core
-5983189 -> R.style.AppTheme_Lime_700_core
-6382300 -> R.style.AppTheme_Lime_800_core
-8227049 -> R.style.AppTheme_Lime_900_core
-1596 -> R.style.AppTheme_Yellow_100_core
-2672 -> R.style.AppTheme_Yellow_200_core
-3722 -> R.style.AppTheme_Yellow_300_core
-4520 -> R.style.AppTheme_Yellow_400_core
-5317 -> R.style.AppTheme_Yellow_500_core
-141259 -> R.style.AppTheme_Yellow_600_core
-278483 -> R.style.AppTheme_Yellow_700_core
-415707 -> R.style.AppTheme_Yellow_800_core
-688361 -> R.style.AppTheme_Yellow_900_core
-4941 -> R.style.AppTheme_Amber_100_core
-8062 -> R.style.AppTheme_Amber_200_core
-10929 -> R.style.AppTheme_Amber_300_core
-13784 -> R.style.AppTheme_Amber_400_core
-16121 -> R.style.AppTheme_Amber_500_core
-19712 -> R.style.AppTheme_Amber_600_core
-24576 -> R.style.AppTheme_Amber_700_core
-28928 -> R.style.AppTheme_Amber_800_core
-37120 -> R.style.AppTheme_Amber_900_core
-8014 -> R.style.AppTheme_Orange_100_core
-13184 -> R.style.AppTheme_Orange_200_core
-18611 -> R.style.AppTheme_Orange_300_core
-22746 -> R.style.AppTheme_Orange_400_core
-26624 -> R.style.AppTheme_Orange_500_core
-291840 -> R.style.AppTheme_Orange_600_core
-689152 -> R.style.AppTheme_Orange_700_core
-1086464 -> R.style.AppTheme_Orange_800_core
-1683200 -> R.style.AppTheme_Orange_900_core
-13124 -> R.style.AppTheme_Deep_Orange_100_core
-21615 -> R.style.AppTheme_Deep_Orange_200_core
-30107 -> R.style.AppTheme_Deep_Orange_300_core
-36797 -> R.style.AppTheme_Deep_Orange_400_core
-43230 -> R.style.AppTheme_Deep_Orange_500_core
-765666 -> R.style.AppTheme_Deep_Orange_600_core
-1684967 -> R.style.AppTheme_Deep_Orange_700_core
-2604267 -> R.style.AppTheme_Deep_Orange_800_core
-4246004 -> R.style.AppTheme_Deep_Orange_900_core
-2634552 -> R.style.AppTheme_Brown_100_core
-4412764 -> R.style.AppTheme_Brown_200_core
-6190977 -> R.style.AppTheme_Brown_300_core
-7508381 -> R.style.AppTheme_Brown_400_core
-8825528 -> R.style.AppTheme_Brown_500_core
-9614271 -> R.style.AppTheme_Brown_600_core
-10665929 -> R.style.AppTheme_Brown_700_core
-11652050 -> R.style.AppTheme_Brown_800_core
-12703965 -> R.style.AppTheme_Brown_900_core
-3155748 -> R.style.AppTheme_Blue_Grey_100_core
-5194811 -> R.style.AppTheme_Blue_Grey_200_core
-7297874 -> R.style.AppTheme_Blue_Grey_300_core
-8875876 -> R.style.AppTheme_Blue_Grey_400_core
-10453621 -> R.style.AppTheme_Blue_Grey_500_core
-11243910 -> R.style.AppTheme_Blue_Grey_600_core
-12232092 -> R.style.AppTheme_Blue_Grey_700_core
-13154481 -> R.style.AppTheme_Blue_Grey_800_core
-14273992 -> R.style.AppTheme_Blue_Grey_900_core
-1 -> R.style.AppTheme_Grey_100_core
-1118482 -> R.style.AppTheme_Grey_200_core
-2039584 -> R.style.AppTheme_Grey_300_core
-4342339 -> R.style.AppTheme_Grey_400_core
-6381922 -> R.style.AppTheme_Grey_500_core
-9079435 -> R.style.AppTheme_Grey_600_core
-10395295 -> R.style.AppTheme_Grey_700_core
-12434878 -> R.style.AppTheme_Grey_800_core
-16777216 -> R.style.AppTheme_Grey_900_core
else -> R.style.AppTheme_Orange_700_core
}
}
else -> {
when (color) {
-12846 -> R.style.AppTheme_Red_100
-1074534 -> R.style.AppTheme_Red_200
-1739917 -> R.style.AppTheme_Red_300
-1092784 -> R.style.AppTheme_Red_400
-769226 -> R.style.AppTheme_Red_500
-1754827 -> R.style.AppTheme_Red_600
-2937041 -> R.style.AppTheme_Red_700
-3790808 -> R.style.AppTheme_Red_800
-4776932 -> R.style.AppTheme_Red_900
-476208 -> R.style.AppTheme_Pink_100
-749647 -> R.style.AppTheme_Pink_200
-1023342 -> R.style.AppTheme_Pink_300
-1294214 -> R.style.AppTheme_Pink_400
-1499549 -> R.style.AppTheme_Pink_500
-2614432 -> R.style.AppTheme_Pink_600
-4056997 -> R.style.AppTheme_Pink_700
-5434281 -> R.style.AppTheme_Pink_800
-7860657 -> R.style.AppTheme_Pink_900
-1982745 -> R.style.AppTheme_Purple_100
-3238952 -> R.style.AppTheme_Purple_200
-4560696 -> R.style.AppTheme_Purple_300
-5552196 -> R.style.AppTheme_Purple_400
-6543440 -> R.style.AppTheme_Purple_500
-7461718 -> R.style.AppTheme_Purple_600
-8708190 -> R.style.AppTheme_Purple_700
-9823334 -> R.style.AppTheme_Purple_800
-11922292 -> R.style.AppTheme_Purple_900
-3029783 -> R.style.AppTheme_Deep_Purple_100
-5005861 -> R.style.AppTheme_Deep_Purple_200
-6982195 -> R.style.AppTheme_Deep_Purple_300
-8497214 -> R.style.AppTheme_Deep_Purple_400
-10011977 -> R.style.AppTheme_Deep_Purple_500
-10603087 -> R.style.AppTheme_Deep_Purple_600
-11457112 -> R.style.AppTheme_Deep_Purple_700
-12245088 -> R.style.AppTheme_Deep_Purple_800
-13558894 -> R.style.AppTheme_Deep_Purple_900
-3814679 -> R.style.AppTheme_Indigo_100
-6313766 -> R.style.AppTheme_Indigo_200
-8812853 -> R.style.AppTheme_Indigo_300
-10720320 -> R.style.AppTheme_Indigo_400
-12627531 -> R.style.AppTheme_Indigo_500
-13022805 -> R.style.AppTheme_Indigo_600
-13615201 -> R.style.AppTheme_Indigo_700
-14142061 -> R.style.AppTheme_Indigo_800
-15064194 -> R.style.AppTheme_Indigo_900
-4464901 -> R.style.AppTheme_Blue_100
-7288071 -> R.style.AppTheme_Blue_200
-10177034 -> R.style.AppTheme_Blue_300
-12409355 -> R.style.AppTheme_Blue_400
-14575885 -> R.style.AppTheme_Blue_500
-14776091 -> R.style.AppTheme_Blue_600
-15108398 -> R.style.AppTheme_Blue_700
-15374912 -> R.style.AppTheme_Blue_800
-15906911 -> R.style.AppTheme_Blue_900
-4987396 -> R.style.AppTheme_Light_Blue_100
-8268550 -> R.style.AppTheme_Light_Blue_200
-11549705 -> R.style.AppTheme_Light_Blue_300
-14043396 -> R.style.AppTheme_Light_Blue_400
-16537100 -> R.style.AppTheme_Light_Blue_500
-16540699 -> R.style.AppTheme_Light_Blue_600
-16611119 -> R.style.AppTheme_Light_Blue_700
-16615491 -> R.style.AppTheme_Light_Blue_800
-16689253 -> R.style.AppTheme_Light_Blue_900
-5051406 -> R.style.AppTheme_Cyan_100
-8331542 -> R.style.AppTheme_Cyan_200
-11677471 -> R.style.AppTheme_Cyan_300
-14235942 -> R.style.AppTheme_Cyan_400
-16728876 -> R.style.AppTheme_Cyan_500
-16732991 -> R.style.AppTheme_Cyan_600
-16738393 -> R.style.AppTheme_Cyan_700
-16743537 -> R.style.AppTheme_Cyan_800
-16752540 -> R.style.AppTheme_Cyan_900
-5054501 -> R.style.AppTheme_Teal_100
-8336444 -> R.style.AppTheme_Teal_200
-11684180 -> R.style.AppTheme_Teal_300
-14244198 -> R.style.AppTheme_Teal_400
-16738680 -> R.style.AppTheme_Teal_500
-16742021 -> R.style.AppTheme_Teal_600
-16746133 -> R.style.AppTheme_Teal_700
-16750244 -> R.style.AppTheme_Teal_800
-16757440 -> R.style.AppTheme_Teal_900
-3610935 -> R.style.AppTheme_Green_100
-5908825 -> R.style.AppTheme_Green_200
-8271996 -> R.style.AppTheme_Green_300
-10044566 -> R.style.AppTheme_Green_400
-11751600 -> R.style.AppTheme_Green_500
-12345273 -> R.style.AppTheme_Green_600
-13070788 -> R.style.AppTheme_Green_700
-13730510 -> R.style.AppTheme_Green_800
-14983648 -> R.style.AppTheme_Green_900
-2298424 -> R.style.AppTheme_Light_Green_100
-3808859 -> R.style.AppTheme_Light_Green_200
-5319295 -> R.style.AppTheme_Light_Green_300
-6501275 -> R.style.AppTheme_Light_Green_400
-7617718 -> R.style.AppTheme_Light_Green_500
-8604862 -> R.style.AppTheme_Light_Green_600
-9920712 -> R.style.AppTheme_Light_Green_700
-11171025 -> R.style.AppTheme_Light_Green_800
-13407970 -> R.style.AppTheme_Light_Green_900
-985917 -> R.style.AppTheme_Lime_100
-1642852 -> R.style.AppTheme_Lime_200
-2300043 -> R.style.AppTheme_Lime_300
-2825897 -> R.style.AppTheme_Lime_400
-3285959 -> R.style.AppTheme_Lime_500
-4142541 -> R.style.AppTheme_Lime_600
-5983189 -> R.style.AppTheme_Lime_700
-6382300 -> R.style.AppTheme_Lime_800
-8227049 -> R.style.AppTheme_Lime_900
-1596 -> R.style.AppTheme_Yellow_100
-2672 -> R.style.AppTheme_Yellow_200
-3722 -> R.style.AppTheme_Yellow_300
-4520 -> R.style.AppTheme_Yellow_400
-5317 -> R.style.AppTheme_Yellow_500
-141259 -> R.style.AppTheme_Yellow_600
-278483 -> R.style.AppTheme_Yellow_700
-415707 -> R.style.AppTheme_Yellow_800
-688361 -> R.style.AppTheme_Yellow_900
-4941 -> R.style.AppTheme_Amber_100
-8062 -> R.style.AppTheme_Amber_200
-10929 -> R.style.AppTheme_Amber_300
-13784 -> R.style.AppTheme_Amber_400
-16121 -> R.style.AppTheme_Amber_500
-19712 -> R.style.AppTheme_Amber_600
-24576 -> R.style.AppTheme_Amber_700
-28928 -> R.style.AppTheme_Amber_800
-37120 -> R.style.AppTheme_Amber_900
-8014 -> R.style.AppTheme_Orange_100
-13184 -> R.style.AppTheme_Orange_200
-18611 -> R.style.AppTheme_Orange_300
-22746 -> R.style.AppTheme_Orange_400
-26624 -> R.style.AppTheme_Orange_500
-291840 -> R.style.AppTheme_Orange_600
-689152 -> R.style.AppTheme_Orange_700
-1086464 -> R.style.AppTheme_Orange_800
-1683200 -> R.style.AppTheme_Orange_900
-13124 -> R.style.AppTheme_Deep_Orange_100
-21615 -> R.style.AppTheme_Deep_Orange_200
-30107 -> R.style.AppTheme_Deep_Orange_300
-36797 -> R.style.AppTheme_Deep_Orange_400
-43230 -> R.style.AppTheme_Deep_Orange_500
-765666 -> R.style.AppTheme_Deep_Orange_600
-1684967 -> R.style.AppTheme_Deep_Orange_700
-2604267 -> R.style.AppTheme_Deep_Orange_800
-4246004 -> R.style.AppTheme_Deep_Orange_900
-2634552 -> R.style.AppTheme_Brown_100
-4412764 -> R.style.AppTheme_Brown_200
-6190977 -> R.style.AppTheme_Brown_300
-7508381 -> R.style.AppTheme_Brown_400
-8825528 -> R.style.AppTheme_Brown_500
-9614271 -> R.style.AppTheme_Brown_600
-10665929 -> R.style.AppTheme_Brown_700
-11652050 -> R.style.AppTheme_Brown_800
-12703965 -> R.style.AppTheme_Brown_900
-3155748 -> R.style.AppTheme_Blue_Grey_100
-5194811 -> R.style.AppTheme_Blue_Grey_200
-7297874 -> R.style.AppTheme_Blue_Grey_300
-8875876 -> R.style.AppTheme_Blue_Grey_400
-10453621 -> R.style.AppTheme_Blue_Grey_500
-11243910 -> R.style.AppTheme_Blue_Grey_600
-12232092 -> R.style.AppTheme_Blue_Grey_700
-13154481 -> R.style.AppTheme_Blue_Grey_800
-14273992 -> R.style.AppTheme_Blue_Grey_900
-1 -> R.style.AppTheme_Grey_100
-1118482 -> R.style.AppTheme_Grey_200
-2039584 -> R.style.AppTheme_Grey_300
-4342339 -> R.style.AppTheme_Grey_400
-6381922 -> R.style.AppTheme_Grey_500
-9079435 -> R.style.AppTheme_Grey_600
-10395295 -> R.style.AppTheme_Grey_700
-12434878 -> R.style.AppTheme_Grey_800
-16777216 -> R.style.AppTheme_Grey_900
else -> R.style.AppTheme_Orange_700
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
package com.simplemobiletools.commons.extensions
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatEditText
// in dialogs, lets use findViewById, because while some dialogs use MyEditText, material theme dialogs use TextInputEditText so the system takes care of it
fun AlertDialog.showKeyboard(editText: AppCompatEditText) {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
editText.apply {
requestFocus()
onGlobalLayout {
setSelection(text.toString().length)
}
}
}
fun AlertDialog.hideKeyboard() {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
}

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.commons.extensions
// extensions used mostly at importing app settings for now
fun Any.toBoolean() = toString() == "true"
fun Any.toInt() = Integer.parseInt(toString())
fun Any.toStringSet() = toString().split(",".toRegex()).toSet()

View File

@ -0,0 +1,13 @@
package com.simplemobiletools.commons.extensions
import android.app.Application
import com.simplemobiletools.commons.helpers.isNougatPlus
import java.util.*
fun Application.checkUseEnglish() {
if (baseConfig.useEnglish && !isNougatPlus()) {
val conf = resources.configuration
conf.locale = Locale.ENGLISH
resources.updateConfiguration(conf, resources.displayMetrics)
}
}

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.commons.extensions
import java.util.*
fun <T> ArrayList<T>.moveLastItemToFront() {
val last = removeAt(size - 1)
add(0, last)
}

View File

@ -0,0 +1,15 @@
package com.simplemobiletools.commons.extensions
import android.graphics.Bitmap
import java.io.ByteArrayOutputStream
fun Bitmap.getByteArray(): ByteArray {
var baos: ByteArrayOutputStream? = null
try {
baos = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.JPEG, 80, baos)
return baos.toByteArray()
} finally {
baos?.close()
}
}

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.commons.extensions
import java.io.BufferedWriter
fun BufferedWriter.writeLn(line: String) {
write(line)
newLine()
}

View File

@ -0,0 +1,323 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.ContactsContract
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.databases.ContactsDatabase
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.interfaces.ContactsDao
import com.simplemobiletools.commons.interfaces.GroupsDao
import com.simplemobiletools.commons.models.contacts.Contact
import com.simplemobiletools.commons.models.contacts.ContactSource
import com.simplemobiletools.commons.models.contacts.Organization
import com.simplemobiletools.commons.models.contacts.SocialAction
import java.io.File
val Context.contactsDB: ContactsDao get() = ContactsDatabase.getInstance(applicationContext).ContactsDao()
val Context.groupsDB: GroupsDao get() = ContactsDatabase.getInstance(applicationContext).GroupsDao()
fun Context.getEmptyContact(): Contact {
val originalContactSource = if (hasContactPermissions()) baseConfig.lastUsedContactSource else SMT_PRIVATE
val organization = Organization("", "")
return Contact(
0, "", "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "",
null, "", ArrayList(), organization, ArrayList(), ArrayList(), DEFAULT_MIMETYPE, null
)
}
fun Context.sendAddressIntent(address: String) {
val location = Uri.encode(address)
val uri = Uri.parse("geo:0,0?q=$location")
Intent(Intent.ACTION_VIEW, uri).apply {
launchActivityIntent(this)
}
}
fun Context.openWebsiteIntent(url: String) {
val website = if (url.startsWith("http")) {
url
} else {
"https://$url"
}
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(website)
launchActivityIntent(this)
}
}
fun Context.getLookupUriRawId(dataUri: Uri): Int {
val lookupKey = getLookupKeyFromUri(dataUri)
if (lookupKey != null) {
val uri = lookupContactUri(lookupKey, this)
if (uri != null) {
return getContactUriRawId(uri)
}
}
return -1
}
fun Context.getContactUriRawId(uri: Uri): Int {
val projection = arrayOf(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor!!.moveToFirst()) {
return cursor.getIntValue(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)
}
} catch (ignored: Exception) {
} finally {
cursor?.close()
}
return -1
}
// from https://android.googlesource.com/platform/packages/apps/Dialer/+/68038172793ee0e2ab3e2e56ddfbeb82879d1f58/java/com/android/contacts/common/util/UriUtils.java
fun getLookupKeyFromUri(lookupUri: Uri): String? {
return if (!isEncodedContactUri(lookupUri)) {
val segments = lookupUri.pathSegments
if (segments.size < 3) null else Uri.encode(segments[2])
} else {
null
}
}
fun isEncodedContactUri(uri: Uri?): Boolean {
if (uri == null) {
return false
}
val lastPathSegment = uri.lastPathSegment ?: return false
return lastPathSegment == "encoded"
}
fun lookupContactUri(lookup: String, context: Context): Uri? {
val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookup)
return try {
ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri)
} catch (e: Exception) {
null
}
}
fun Context.getCachePhoto(): File {
val imagesFolder = File(cacheDir, "my_cache")
if (!imagesFolder.exists()) {
imagesFolder.mkdirs()
}
val file = File(imagesFolder, "Photo_${System.currentTimeMillis()}.jpg")
file.createNewFile()
return file
}
fun Context.getPhotoThumbnailSize(): Int {
val uri = ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI
val projection = arrayOf(ContactsContract.DisplayPhoto.THUMBNAIL_MAX_DIM)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor?.moveToFirst() == true) {
return cursor.getIntValue(ContactsContract.DisplayPhoto.THUMBNAIL_MAX_DIM)
}
} catch (ignored: Exception) {
} finally {
cursor?.close()
}
return 0
}
fun Context.hasContactPermissions() = hasPermission(PERMISSION_READ_CONTACTS) && hasPermission(PERMISSION_WRITE_CONTACTS)
fun Context.getPublicContactSource(source: String, callback: (String) -> Unit) {
when (source) {
SMT_PRIVATE -> callback(getString(R.string.phone_storage_hidden))
else -> {
ContactsHelper(this).getContactSources {
var newSource = source
for (contactSource in it) {
if (contactSource.name == source && contactSource.type == TELEGRAM_PACKAGE) {
newSource = getString(R.string.telegram)
break
} else if (contactSource.name == source && contactSource.type == VIBER_PACKAGE) {
newSource = getString(R.string.viber)
break
}
}
Handler(Looper.getMainLooper()).post {
callback(newSource)
}
}
}
}
}
fun Context.getPublicContactSourceSync(source: String, contactSources: ArrayList<ContactSource>): String {
return when (source) {
SMT_PRIVATE -> getString(R.string.phone_storage_hidden)
else -> {
var newSource = source
for (contactSource in contactSources) {
if (contactSource.name == source && contactSource.type == TELEGRAM_PACKAGE) {
newSource = getString(R.string.telegram)
break
} else if (contactSource.name == source && contactSource.type == VIBER_PACKAGE) {
newSource = getString(R.string.viber)
break
}
}
return newSource
}
}
}
fun Context.sendSMSToContacts(contacts: ArrayList<Contact>) {
val numbers = StringBuilder()
contacts.forEach {
val number = it.phoneNumbers.firstOrNull { it.type == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE }
?: it.phoneNumbers.firstOrNull()
if (number != null) {
numbers.append("${Uri.encode(number.value)};")
}
}
val uriString = "smsto:${numbers.toString().trimEnd(';')}"
Intent(Intent.ACTION_SENDTO, Uri.parse(uriString)).apply {
launchActivityIntent(this)
}
}
fun Context.sendEmailToContacts(contacts: ArrayList<Contact>) {
val emails = ArrayList<String>()
contacts.forEach {
it.emails.forEach {
if (it.value.isNotEmpty()) {
emails.add(it.value)
}
}
}
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "message/rfc822"
putExtra(Intent.EXTRA_EMAIL, emails.toTypedArray())
launchActivityIntent(this)
}
}
fun Context.getTempFile(filename: String = DEFAULT_FILE_NAME): File? {
val folder = File(cacheDir, "contacts")
if (!folder.exists()) {
if (!folder.mkdir()) {
toast(R.string.unknown_error_occurred)
return null
}
}
return File(folder, filename)
}
fun Context.addContactsToGroup(contacts: ArrayList<Contact>, groupId: Long) {
val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList<Contact>
val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList<Contact>
if (publicContacts.isNotEmpty()) {
ContactsHelper(this).addContactsToGroup(publicContacts, groupId)
}
if (privateContacts.isNotEmpty()) {
LocalContactsHelper(this).addContactsToGroup(privateContacts, groupId)
}
}
fun Context.removeContactsFromGroup(contacts: ArrayList<Contact>, groupId: Long) {
val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList<Contact>
val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList<Contact>
if (publicContacts.isNotEmpty() && hasContactPermissions()) {
ContactsHelper(this).removeContactsFromGroup(publicContacts, groupId)
}
if (privateContacts.isNotEmpty()) {
LocalContactsHelper(this).removeContactsFromGroup(privateContacts, groupId)
}
}
fun Context.getContactPublicUri(contact: Contact): Uri {
val lookupKey = if (contact.isPrivate()) {
"local_${contact.id}"
} else {
SimpleContactsHelper(this).getContactLookupKey(contact.id.toString())
}
return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey)
}
fun Context.getVisibleContactSources(): ArrayList<String> {
val sources = getAllContactSources()
val ignoredContactSources = baseConfig.ignoredContactSources
return ArrayList(sources).filter { !ignoredContactSources.contains(it.getFullIdentifier()) }
.map { it.name }.toMutableList() as ArrayList<String>
}
fun Context.getAllContactSources(): ArrayList<ContactSource> {
val sources = ContactsHelper(this).getDeviceContactSources()
sources.add(getPrivateContactSource())
return sources.toMutableList() as ArrayList<ContactSource>
}
fun Context.getPrivateContactSource() = ContactSource(SMT_PRIVATE, SMT_PRIVATE, getString(R.string.phone_storage_hidden))
fun Context.getSocialActions(id: Int): ArrayList<SocialAction> {
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data._ID,
ContactsContract.Data.DATA3,
ContactsContract.Data.MIMETYPE,
ContactsContract.Data.ACCOUNT_TYPE_AND_DATA_SET
)
val socialActions = ArrayList<SocialAction>()
var curActionId = 0
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ?"
val selectionArgs = arrayOf(id.toString())
queryCursor(uri, projection, selection, selectionArgs, null, true) { cursor ->
val mimetype = cursor.getStringValue(ContactsContract.Data.MIMETYPE)
val type = when (mimetype) {
// WhatsApp
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.com.whatsapp.voip.call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.whatsapp.video.call" -> SOCIAL_VIDEO_CALL
// Viber
"vnd.android.cursor.item/vnd.com.viber.voip.viber_number_call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_out_call_viber" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_out_call_none_viber" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_number_message" -> SOCIAL_MESSAGE
// Signal
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" -> SOCIAL_VOICE_CALL
// Telegram
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call.video" -> SOCIAL_VIDEO_CALL
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" -> SOCIAL_MESSAGE
// Threema
"vnd.android.cursor.item/vnd.ch.threema.app.profile" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.ch.threema.app.call" -> SOCIAL_VOICE_CALL
else -> return@queryCursor
}
val label = cursor.getStringValue(ContactsContract.Data.DATA3)
val realID = cursor.getLongValue(ContactsContract.Data._ID)
val packageName = cursor.getStringValue(ContactsContract.Data.ACCOUNT_TYPE_AND_DATA_SET)
val socialAction = SocialAction(curActionId++, type, label, mimetype, realID, packageName)
socialActions.add(socialAction)
}
return socialActions
}

View File

@ -0,0 +1,256 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.documentfile.provider.DocumentFile
import com.simplemobiletools.commons.helpers.EXTERNAL_STORAGE_PROVIDER_AUTHORITY
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.commons.helpers.isSPlus
import com.simplemobiletools.commons.models.FileDirItem
import java.io.File
private const val DOWNLOAD_DIR = "Download"
private const val ANDROID_DIR = "Android"
private val DIRS_INACCESSIBLE_WITH_SAF_SDK_30 = listOf(DOWNLOAD_DIR, ANDROID_DIR)
fun Context.hasProperStoredFirstParentUri(path: String): Boolean {
val firstParentUri = createFirstParentTreeUri(path)
return contentResolver.persistedUriPermissions.any { it.uri.toString() == firstParentUri.toString() }
}
fun Context.isAccessibleWithSAFSdk30(path: String): Boolean {
if (path.startsWith(recycleBinPath) || isExternalStorageManager()) {
return false
}
val level = getFirstParentLevel(path)
val firstParentDir = path.getFirstParentDirName(this, level)
val firstParentPath = path.getFirstParentPath(this, level)
val isValidName = firstParentDir != null
val isDirectory = File(firstParentPath).isDirectory
val isAnAccessibleDirectory = DIRS_INACCESSIBLE_WITH_SAF_SDK_30.all { !firstParentDir.equals(it, true) }
return isRPlus() && isValidName && isDirectory && isAnAccessibleDirectory
}
fun Context.getFirstParentLevel(path: String): Int {
return when {
isRPlus() && (isInAndroidDir(path) || isInSubFolderInDownloadDir(path)) -> 1
else -> 0
}
}
fun Context.isRestrictedWithSAFSdk30(path: String): Boolean {
if (path.startsWith(recycleBinPath) || isExternalStorageManager()) {
return false
}
val level = getFirstParentLevel(path)
val firstParentDir = path.getFirstParentDirName(this, level)
val firstParentPath = path.getFirstParentPath(this, level)
val isInvalidName = firstParentDir == null
val isDirectory = File(firstParentPath).isDirectory
val isARestrictedDirectory = DIRS_INACCESSIBLE_WITH_SAF_SDK_30.any { firstParentDir.equals(it, true) }
return isRPlus() && (isInvalidName || (isDirectory && isARestrictedDirectory))
}
fun Context.isInDownloadDir(path: String): Boolean {
if (path.startsWith(recycleBinPath)) {
return false
}
val firstParentDir = path.getFirstParentDirName(this, 0)
return firstParentDir.equals(DOWNLOAD_DIR, true)
}
fun Context.isInSubFolderInDownloadDir(path: String): Boolean {
if (path.startsWith(recycleBinPath)) {
return false
}
val firstParentDir = path.getFirstParentDirName(this, 1)
return if (firstParentDir == null) {
false
} else {
val startsWithDownloadDir = firstParentDir.startsWith(DOWNLOAD_DIR, true)
val hasAtLeast1PathSegment = firstParentDir.split("/").filter { it.isNotEmpty() }.size > 1
val firstParentPath = path.getFirstParentPath(this, 1)
startsWithDownloadDir && hasAtLeast1PathSegment && File(firstParentPath).isDirectory
}
}
fun Context.isInAndroidDir(path: String): Boolean {
if (path.startsWith(recycleBinPath)) {
return false
}
val firstParentDir = path.getFirstParentDirName(this, 0)
return firstParentDir.equals(ANDROID_DIR, true)
}
fun isExternalStorageManager(): Boolean {
return isRPlus() && Environment.isExternalStorageManager()
}
// is the app a Media Management App on Android 12+?
fun Context.canManageMedia(): Boolean {
return isSPlus() && MediaStore.canManageMedia(this)
}
fun Context.createFirstParentTreeUriUsingRootTree(fullPath: String): Uri {
val storageId = getSAFStorageId(fullPath)
val level = getFirstParentLevel(fullPath)
val rootParentDirName = fullPath.getFirstParentDirName(this, level)
val treeUri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, "$storageId:")
val documentId = "${storageId}:$rootParentDirName"
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.createFirstParentTreeUri(fullPath: String): Uri {
val storageId = getSAFStorageId(fullPath)
val level = getFirstParentLevel(fullPath)
val rootParentDirName = fullPath.getFirstParentDirName(this, level)
val firstParentId = "$storageId:$rootParentDirName"
return DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, firstParentId)
}
fun Context.createDocumentUriUsingFirstParentTreeUri(fullPath: String): Uri {
val storageId = getSAFStorageId(fullPath)
val relativePath = when {
fullPath.startsWith(internalStoragePath) -> fullPath.substring(internalStoragePath.length).trim('/')
else -> fullPath.substringAfter(storageId).trim('/')
}
val treeUri = createFirstParentTreeUri(fullPath)
val documentId = "${storageId}:$relativePath"
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.getSAFDocumentId(path: String): String {
val basePath = path.getBasePath(this)
val relativePath = path.substring(basePath.length).trim('/')
val storageId = getSAFStorageId(path)
return "$storageId:$relativePath"
}
fun Context.createSAFDirectorySdk30(path: String): Boolean {
return try {
val treeUri = createFirstParentTreeUri(path)
val parentPath = path.getParentPath()
if (!getDoesFilePathExistSdk30(parentPath)) {
createSAFDirectorySdk30(parentPath)
}
val documentId = getSAFDocumentId(parentPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.createDocument(contentResolver, parentUri, DocumentsContract.Document.MIME_TYPE_DIR, path.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.createSAFFileSdk30(path: String): Boolean {
return try {
val treeUri = createFirstParentTreeUri(path)
val parentPath = path.getParentPath()
if (!getDoesFilePathExistSdk30(parentPath)) {
createSAFDirectorySdk30(parentPath)
}
val documentId = getSAFDocumentId(parentPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.createDocument(contentResolver, parentUri, path.getMimeType(), path.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.getDoesFilePathExistSdk30(path: String): Boolean {
return when {
isAccessibleWithSAFSdk30(path) -> getFastDocumentSdk30(path)?.exists() ?: false
else -> File(path).exists()
}
}
fun Context.getSomeDocumentSdk30(path: String): DocumentFile? = getFastDocumentSdk30(path) ?: getDocumentSdk30(path)
fun Context.getFastDocumentSdk30(path: String): DocumentFile? {
val uri = createDocumentUriUsingFirstParentTreeUri(path)
return DocumentFile.fromSingleUri(this, uri)
}
fun Context.getDocumentSdk30(path: String): DocumentFile? {
val level = getFirstParentLevel(path)
val firstParentPath = path.getFirstParentPath(this, level)
var relativePath = path.substring(firstParentPath.length)
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1)
}
return try {
val treeUri = createFirstParentTreeUri(path)
var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) {
document = document?.findFile(part)
}
document
} catch (ignored: Exception) {
null
}
}
fun Context.deleteDocumentWithSAFSdk30(fileDirItem: FileDirItem, allowDeleteFolder: Boolean, callback: ((wasSuccess: Boolean) -> Unit)?) {
try {
var fileDeleted = false
if (fileDirItem.isDirectory.not() || allowDeleteFolder) {
val fileUri = createDocumentUriUsingFirstParentTreeUri(fileDirItem.path)
fileDeleted = DocumentsContract.deleteDocument(contentResolver, fileUri)
}
if (fileDeleted) {
deleteFromMediaStore(fileDirItem.path)
callback?.invoke(true)
}
} catch (e: Exception) {
callback?.invoke(false)
showErrorToast(e)
}
}
fun Context.renameDocumentSdk30(oldPath: String, newPath: String): Boolean {
return try {
val treeUri = createFirstParentTreeUri(oldPath)
val documentId = getSAFDocumentId(oldPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.renameDocument(contentResolver, parentUri, newPath.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.hasProperStoredDocumentUriSdk30(path: String): Boolean {
val documentUri = buildDocumentUriSdk30(path)
return contentResolver.persistedUriPermissions.any { it.uri.toString() == documentUri.toString() }
}
fun Context.buildDocumentUriSdk30(fullPath: String): Uri {
val storageId = getSAFStorageId(fullPath)
val relativePath = when {
fullPath.startsWith(internalStoragePath) -> fullPath.substring(internalStoragePath.length).trim('/')
else -> fullPath.substringAfter(storageId).trim('/')
}
val documentId = "${storageId}:$relativePath"
return DocumentsContract.buildDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, documentId)
}
fun Context.getPicturesDirectoryPath(fullPath: String): String {
val basePath = fullPath.getBasePath(this)
return File(basePath, Environment.DIRECTORY_PICTURES).absolutePath
}

View File

@ -0,0 +1,993 @@
package com.simplemobiletools.commons.extensions
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.DocumentsContract
import android.provider.DocumentsContract.Document
import android.provider.MediaStore.*
import android.text.TextUtils
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.net.URLDecoder
import java.util.*
import java.util.regex.Pattern
private const val ANDROID_DATA_DIR = "/Android/data/"
private const val ANDROID_OBB_DIR = "/Android/obb/"
val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf(ANDROID_DATA_DIR, ANDROID_OBB_DIR)
val Context.recycleBinPath: String get() = filesDir.absolutePath
// http://stackoverflow.com/a/40582634/1967672
fun Context.getSDCardPath(): String {
val directories = getStorageDirectories().filter {
!it.equals(getInternalStoragePath()) && !it.equals(
"/storage/emulated/0",
true
) && (baseConfig.OTGPartition.isEmpty() || !it.endsWith(baseConfig.OTGPartition))
}
val fullSDpattern = Pattern.compile(SD_OTG_PATTERN)
var sdCardPath = directories.firstOrNull { fullSDpattern.matcher(it).matches() }
?: directories.firstOrNull { !physicalPaths.contains(it.toLowerCase()) } ?: ""
// on some devices no method retrieved any SD card path, so test if its not sdcard1 by any chance. It happened on an Android 5.1
if (sdCardPath.trimEnd('/').isEmpty()) {
val file = File("/storage/sdcard1")
if (file.exists()) {
return file.absolutePath
}
sdCardPath = directories.firstOrNull() ?: ""
}
if (sdCardPath.isEmpty()) {
val SDpattern = Pattern.compile(SD_OTG_SHORT)
try {
File("/storage").listFiles()?.forEach {
if (SDpattern.matcher(it.name).matches()) {
sdCardPath = "/storage/${it.name}"
}
}
} catch (e: Exception) {
}
}
val finalPath = sdCardPath.trimEnd('/')
baseConfig.sdCardPath = finalPath
return finalPath
}
fun Context.hasExternalSDCard() = sdCardPath.isNotEmpty()
fun Context.hasOTGConnected(): Boolean {
return try {
(getSystemService(Context.USB_SERVICE) as UsbManager).deviceList.any {
it.value.getInterface(0).interfaceClass == UsbConstants.USB_CLASS_MASS_STORAGE
}
} catch (e: Exception) {
false
}
}
fun Context.getStorageDirectories(): Array<String> {
val paths = HashSet<String>()
val rawExternalStorage = System.getenv("EXTERNAL_STORAGE")
val rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE")
val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET")
if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
getExternalFilesDirs(null).filterNotNull().map { it.absolutePath }
.mapTo(paths) { it.substring(0, it.indexOf("Android/data")) }
} else {
val path = Environment.getExternalStorageDirectory().absolutePath
val folders = Pattern.compile("/").split(path)
val lastFolder = folders[folders.size - 1]
var isDigit = false
try {
Integer.valueOf(lastFolder)
isDigit = true
} catch (ignored: NumberFormatException) {
}
val rawUserId = if (isDigit) lastFolder else ""
if (TextUtils.isEmpty(rawUserId)) {
paths.add(rawEmulatedStorageTarget!!)
} else {
paths.add(rawEmulatedStorageTarget + File.separator + rawUserId)
}
}
if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) {
val rawSecondaryStorages = rawSecondaryStoragesStr!!.split(File.pathSeparator.toRegex()).dropLastWhile(String::isEmpty).toTypedArray()
Collections.addAll(paths, *rawSecondaryStorages)
}
return paths.map { it.trimEnd('/') }.toTypedArray()
}
fun Context.getHumanReadablePath(path: String): String {
return getString(
when (path) {
"/" -> R.string.root
internalStoragePath -> R.string.internal
otgPath -> R.string.usb
else -> R.string.sd_card
}
)
}
fun Context.humanizePath(path: String): String {
val trimmedPath = path.trimEnd('/')
val basePath = path.getBasePath(this)
return when (basePath) {
"/" -> "${getHumanReadablePath(basePath)}$trimmedPath"
else -> trimmedPath.replaceFirst(basePath, getHumanReadablePath(basePath))
}
}
fun Context.getInternalStoragePath() =
if (File("/storage/emulated/0").exists()) "/storage/emulated/0" else Environment.getExternalStorageDirectory().absolutePath.trimEnd('/')
fun Context.isPathOnSD(path: String) = sdCardPath.isNotEmpty() && path.startsWith(sdCardPath)
fun Context.isPathOnOTG(path: String) = otgPath.isNotEmpty() && path.startsWith(otgPath)
fun Context.isPathOnInternalStorage(path: String) = internalStoragePath.isNotEmpty() && path.startsWith(internalStoragePath)
fun Context.getSAFOnlyDirs(): List<String> {
return DIRS_ACCESSIBLE_ONLY_WITH_SAF.map { "$internalStoragePath$it" } +
DIRS_ACCESSIBLE_ONLY_WITH_SAF.map { "$sdCardPath$it" }
}
fun Context.isSAFOnlyRoot(path: String): Boolean {
return getSAFOnlyDirs().any { "${path.trimEnd('/')}/".startsWith(it) }
}
fun Context.isRestrictedSAFOnlyRoot(path: String): Boolean {
return isRPlus() && isSAFOnlyRoot(path)
}
// no need to use DocumentFile if an SD card is set as the default storage
fun Context.needsStupidWritePermissions(path: String) = !isRPlus() && (isPathOnSD(path) || isPathOnOTG(path)) && !isSDCardSetAsDefaultStorage()
fun Context.isSDCardSetAsDefaultStorage() = sdCardPath.isNotEmpty() && Environment.getExternalStorageDirectory().absolutePath.equals(sdCardPath, true)
fun Context.hasProperStoredTreeUri(isOTG: Boolean): Boolean {
val uri = if (isOTG) baseConfig.OTGTreeUri else baseConfig.sdTreeUri
val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
if (!hasProperUri) {
if (isOTG) {
baseConfig.OTGTreeUri = ""
} else {
baseConfig.sdTreeUri = ""
}
}
return hasProperUri
}
fun Context.hasProperStoredAndroidTreeUri(path: String): Boolean {
val uri = getAndroidTreeUri(path)
val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
if (!hasProperUri) {
storeAndroidTreeUri(path, "")
}
return hasProperUri
}
fun Context.getAndroidTreeUri(path: String): String {
return when {
isPathOnOTG(path) -> if (isAndroidDataDir(path)) baseConfig.otgAndroidDataTreeUri else baseConfig.otgAndroidObbTreeUri
isPathOnSD(path) -> if (isAndroidDataDir(path)) baseConfig.sdAndroidDataTreeUri else baseConfig.sdAndroidObbTreeUri
else -> if (isAndroidDataDir(path)) baseConfig.primaryAndroidDataTreeUri else baseConfig.primaryAndroidObbTreeUri
}
}
fun isAndroidDataDir(path: String): Boolean {
val resolvedPath = "${path.trimEnd('/')}/"
return resolvedPath.contains(ANDROID_DATA_DIR)
}
fun Context.storeAndroidTreeUri(path: String, treeUri: String) {
return when {
isPathOnOTG(path) -> if (isAndroidDataDir(path)) baseConfig.otgAndroidDataTreeUri = treeUri else baseConfig.otgAndroidObbTreeUri = treeUri
isPathOnSD(path) -> if (isAndroidDataDir(path)) baseConfig.sdAndroidDataTreeUri = treeUri else baseConfig.sdAndroidObbTreeUri = treeUri
else -> if (isAndroidDataDir(path)) baseConfig.primaryAndroidDataTreeUri = treeUri else baseConfig.primaryAndroidObbTreeUri = treeUri
}
}
fun Context.getSAFStorageId(fullPath: String): String {
return if (fullPath.startsWith('/')) {
when {
fullPath.startsWith(internalStoragePath) -> "primary"
else -> fullPath.substringAfter("/storage/", "").substringBefore('/')
}
} else {
fullPath.substringBefore(':', "").substringAfterLast('/')
}
}
fun Context.createDocumentUriFromRootTree(fullPath: String): Uri {
val storageId = getSAFStorageId(fullPath)
val relativePath = when {
fullPath.startsWith(internalStoragePath) -> fullPath.substring(internalStoragePath.length).trim('/')
else -> fullPath.substringAfter(storageId).trim('/')
}
val treeUri = DocumentsContract.buildTreeDocumentUri(EXTERNAL_STORAGE_PROVIDER_AUTHORITY, "$storageId:")
val documentId = "${storageId}:$relativePath"
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.createAndroidDataOrObbPath(fullPath: String): String {
return if (isAndroidDataDir(fullPath)) {
fullPath.getBasePath(this).trimEnd('/').plus(ANDROID_DATA_DIR)
} else {
fullPath.getBasePath(this).trimEnd('/').plus(ANDROID_OBB_DIR)
}
}
fun Context.createAndroidDataOrObbUri(fullPath: String): Uri {
val path = createAndroidDataOrObbPath(fullPath)
return createDocumentUriFromRootTree(path)
}
fun Context.getStorageRootIdForAndroidDir(path: String) =
getAndroidTreeUri(path).removeSuffix(if (isAndroidDataDir(path)) "%3AAndroid%2Fdata" else "%3AAndroid%2Fobb").substringAfterLast('/').trimEnd('/')
fun Context.isAStorageRootFolder(path: String): Boolean {
val trimmed = path.trimEnd('/')
return trimmed.isEmpty() || trimmed.equals(internalStoragePath, true) || trimmed.equals(sdCardPath, true) || trimmed.equals(otgPath, true)
}
fun Context.getMyFileUri(file: File): Uri {
return if (isNougatPlus()) {
FileProvider.getUriForFile(this, "$packageName.provider", file)
} else {
Uri.fromFile(file)
}
}
fun Context.tryFastDocumentDelete(path: String, allowDeleteFolder: Boolean): Boolean {
val document = getFastDocumentFile(path)
return if (document?.isFile == true || allowDeleteFolder) {
try {
DocumentsContract.deleteDocument(contentResolver, document?.uri!!)
} catch (e: Exception) {
false
}
} else {
false
}
}
fun Context.getFastDocumentFile(path: String): DocumentFile? {
if (isPathOnOTG(path)) {
return getOTGFastDocumentFile(path)
}
if (baseConfig.sdCardPath.isEmpty()) {
return null
}
val relativePath = Uri.encode(path.substring(baseConfig.sdCardPath.length).trim('/'))
val externalPathPart = baseConfig.sdCardPath.split("/").lastOrNull(String::isNotEmpty)?.trim('/') ?: return null
val fullUri = "${baseConfig.sdTreeUri}/document/$externalPathPart%3A$relativePath"
return DocumentFile.fromSingleUri(this, Uri.parse(fullUri))
}
fun Context.getOTGFastDocumentFile(path: String, otgPathToUse: String? = null): DocumentFile? {
if (baseConfig.OTGTreeUri.isEmpty()) {
return null
}
val otgPath = otgPathToUse ?: baseConfig.OTGPath
if (baseConfig.OTGPartition.isEmpty()) {
baseConfig.OTGPartition = baseConfig.OTGTreeUri.removeSuffix("%3A").substringAfterLast('/').trimEnd('/')
updateOTGPathFromPartition()
}
val relativePath = Uri.encode(path.substring(otgPath.length).trim('/'))
val fullUri = "${baseConfig.OTGTreeUri}/document/${baseConfig.OTGPartition}%3A$relativePath"
return DocumentFile.fromSingleUri(this, Uri.parse(fullUri))
}
fun Context.getDocumentFile(path: String): DocumentFile? {
val isOTG = isPathOnOTG(path)
var relativePath = path.substring(if (isOTG) otgPath.length else sdCardPath.length)
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1)
}
return try {
val treeUri = Uri.parse(if (isOTG) baseConfig.OTGTreeUri else baseConfig.sdTreeUri)
var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) {
document = document?.findFile(part)
}
document
} catch (ignored: Exception) {
null
}
}
fun Context.getSomeDocumentFile(path: String) = getFastDocumentFile(path) ?: getDocumentFile(path)
fun Context.scanFileRecursively(file: File, callback: (() -> Unit)? = null) {
scanFilesRecursively(arrayListOf(file), callback)
}
fun Context.scanPathRecursively(path: String, callback: (() -> Unit)? = null) {
scanPathsRecursively(arrayListOf(path), callback)
}
fun Context.scanFilesRecursively(files: List<File>, callback: (() -> Unit)? = null) {
val allPaths = ArrayList<String>()
for (file in files) {
allPaths.addAll(getPaths(file))
}
rescanPaths(allPaths, callback)
}
fun Context.scanPathsRecursively(paths: List<String>, callback: (() -> Unit)? = null) {
val allPaths = ArrayList<String>()
for (path in paths) {
allPaths.addAll(getPaths(File(path)))
}
rescanPaths(allPaths, callback)
}
fun Context.rescanPath(path: String, callback: (() -> Unit)? = null) {
rescanPaths(arrayListOf(path), callback)
}
// avoid calling this multiple times in row, it can delete whole folder contents
fun Context.rescanPaths(paths: List<String>, callback: (() -> Unit)? = null) {
if (paths.isEmpty()) {
callback?.invoke()
return
}
for (path in paths) {
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).apply {
data = Uri.fromFile(File(path))
sendBroadcast(this)
}
}
var cnt = paths.size
MediaScannerConnection.scanFile(applicationContext, paths.toTypedArray(), null) { s, uri ->
if (--cnt == 0) {
callback?.invoke()
}
}
}
fun getPaths(file: File): ArrayList<String> {
val paths = arrayListOf<String>(file.absolutePath)
if (file.isDirectory) {
val files = file.listFiles() ?: return paths
for (curFile in files) {
paths.addAll(getPaths(curFile))
}
}
return paths
}
fun Context.getFileUri(path: String) = when {
path.isImageSlow() -> Images.Media.EXTERNAL_CONTENT_URI
path.isVideoSlow() -> Video.Media.EXTERNAL_CONTENT_URI
path.isAudioSlow() -> Audio.Media.EXTERNAL_CONTENT_URI
else -> Files.getContentUri("external")
}
// these functions update the mediastore instantly, MediaScannerConnection.scanFileRecursively takes some time to really get applied
fun Context.deleteFromMediaStore(path: String, callback: ((needsRescan: Boolean) -> Unit)? = null) {
if (getIsPathDirectory(path)) {
callback?.invoke(false)
return
}
ensureBackgroundThread {
try {
val where = "${MediaColumns.DATA} = ?"
val args = arrayOf(path)
val needsRescan = contentResolver.delete(getFileUri(path), where, args) != 1
callback?.invoke(needsRescan)
} catch (ignored: Exception) {
callback?.invoke(true)
}
}
}
fun Context.rescanAndDeletePath(path: String, callback: () -> Unit) {
val SCAN_FILE_MAX_DURATION = 1000L
val scanFileHandler = Handler(Looper.getMainLooper())
scanFileHandler.postDelayed({
callback()
}, SCAN_FILE_MAX_DURATION)
MediaScannerConnection.scanFile(applicationContext, arrayOf(path), null) { path, uri ->
scanFileHandler.removeCallbacksAndMessages(null)
try {
applicationContext.contentResolver.delete(uri, null, null)
} catch (e: Exception) {
}
callback()
}
}
fun Context.updateInMediaStore(oldPath: String, newPath: String) {
ensureBackgroundThread {
val values = ContentValues().apply {
put(MediaColumns.DATA, newPath)
put(MediaColumns.DISPLAY_NAME, newPath.getFilenameFromPath())
put(MediaColumns.TITLE, newPath.getFilenameFromPath())
}
val uri = getFileUri(oldPath)
val selection = "${MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(oldPath)
try {
contentResolver.update(uri, values, selection, selectionArgs)
} catch (ignored: Exception) {
}
}
}
fun Context.updateLastModified(path: String, lastModified: Long) {
val values = ContentValues().apply {
put(MediaColumns.DATE_MODIFIED, lastModified / 1000)
}
File(path).setLastModified(lastModified)
val uri = getFileUri(path)
val selection = "${MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(path)
try {
contentResolver.update(uri, values, selection, selectionArgs)
} catch (ignored: Exception) {
}
}
fun Context.getOTGItems(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>()
val OTGTreeUri = baseConfig.OTGTreeUri
var rootUri = try {
DocumentFile.fromTreeUri(applicationContext, Uri.parse(OTGTreeUri))
} catch (e: Exception) {
showErrorToast(e)
baseConfig.OTGPath = ""
baseConfig.OTGTreeUri = ""
baseConfig.OTGPartition = ""
null
}
if (rootUri == null) {
callback(items)
return
}
val parts = path.split("/").dropLastWhile { it.isEmpty() }
for (part in parts) {
if (path == otgPath) {
break
}
if (part == "otg:" || part == "") {
continue
}
val file = rootUri!!.findFile(part)
if (file != null) {
rootUri = file
}
}
val files = rootUri!!.listFiles().filter { it.exists() }
val basePath = "${baseConfig.OTGTreeUri}/document/${baseConfig.OTGPartition}%3A"
for (file in files) {
val name = file.name ?: continue
if (!shouldShowHidden && name.startsWith(".")) {
continue
}
val isDirectory = file.isDirectory
val filePath = file.uri.toString().substring(basePath.length)
val decodedPath = otgPath + "/" + URLDecoder.decode(filePath, "UTF-8")
val fileSize = when {
getProperFileSize -> file.getItemSize(shouldShowHidden)
isDirectory -> 0L
else -> file.length()
}
val childrenCount = if (isDirectory) {
file.listFiles().size
} else {
0
}
val lastModified = file.lastModified()
val fileDirItem = FileDirItem(decodedPath, name, isDirectory, childrenCount, fileSize, lastModified)
items.add(fileDirItem)
}
callback(items)
}
@RequiresApi(Build.VERSION_CODES.O)
fun Context.getAndroidSAFFileItems(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean = true, callback: (ArrayList<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>()
val rootDocId = getStorageRootIdForAndroidDir(path)
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = createAndroidSAFDocumentId(path)
val childrenUri = try {
DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
} catch (e: Exception) {
showErrorToast(e)
storeAndroidTreeUri(path, "")
null
}
if (childrenUri == null) {
callback(items)
return
}
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, Document.COLUMN_LAST_MODIFIED)
try {
val rawCursor = contentResolver.query(childrenUri, projection, null, null)!!
val cursor = ExternalStorageProviderHack.transformQueryResult(rootDocId, childrenUri, rawCursor)
cursor.use {
if (cursor.moveToFirst()) {
do {
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
val name = cursor.getStringValue(Document.COLUMN_DISPLAY_NAME)
val mimeType = cursor.getStringValue(Document.COLUMN_MIME_TYPE)
val lastModified = cursor.getLongValue(Document.COLUMN_LAST_MODIFIED)
val isDirectory = mimeType == Document.MIME_TYPE_DIR
val filePath = docId.substring("${getStorageRootIdForAndroidDir(path)}:".length)
if (!shouldShowHidden && name.startsWith(".")) {
continue
}
val decodedPath = path.getBasePath(this) + "/" + URLDecoder.decode(filePath, "UTF-8")
val fileSize = when {
getProperFileSize -> getFileSize(treeUri, docId)
isDirectory -> 0L
else -> getFileSize(treeUri, docId)
}
val childrenCount = if (isDirectory) {
getDirectChildrenCount(rootDocId, treeUri, docId, shouldShowHidden)
} else {
0
}
val fileDirItem = FileDirItem(decodedPath, name, isDirectory, childrenCount, fileSize, lastModified)
items.add(fileDirItem)
} while (cursor.moveToNext())
}
}
} catch (e: Exception) {
showErrorToast(e)
}
callback(items)
}
fun Context.getDirectChildrenCount(rootDocId: String, treeUri: Uri, documentId: String, shouldShowHidden: Boolean): Int {
return try {
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID)
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
val cursor = ExternalStorageProviderHack.transformQueryResult(rootDocId, childrenUri, rawCursor)
if (shouldShowHidden) {
cursor.count
} else {
var count = 0
cursor.use {
while (cursor.moveToNext()) {
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
if (!docId.getFilenameFromPath().startsWith('.') || shouldShowHidden) {
count++
}
}
}
count
}
} catch (e: Exception) {
0
}
}
fun Context.getProperChildrenCount(rootDocId: String, treeUri: Uri, documentId: String, shouldShowHidden: Boolean): Int {
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE)
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
val cursor = ExternalStorageProviderHack.transformQueryResult(rootDocId, childrenUri, rawCursor)
return if (cursor.count > 0) {
var count = 0
cursor.use {
while (cursor.moveToNext()) {
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
val mimeType = cursor.getStringValue(Document.COLUMN_MIME_TYPE)
if (mimeType == Document.MIME_TYPE_DIR) {
count++
count += getProperChildrenCount(rootDocId, treeUri, docId, shouldShowHidden)
} else if (!docId.getFilenameFromPath().startsWith('.') || shouldShowHidden) {
count++
}
}
}
count
} else {
1
}
}
fun Context.getFileSize(treeUri: Uri, documentId: String): Long {
val projection = arrayOf(Document.COLUMN_SIZE)
val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return contentResolver.query(documentUri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getLongValue(Document.COLUMN_SIZE)
} else {
0L
}
} ?: 0L
}
fun Context.createAndroidSAFDocumentId(path: String): String {
val basePath = path.getBasePath(this)
val relativePath = path.substring(basePath.length).trim('/')
val storageId = getStorageRootIdForAndroidDir(path)
return "$storageId:$relativePath"
}
fun Context.getAndroidSAFUri(path: String): Uri {
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = createAndroidSAFDocumentId(path)
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.getAndroidSAFDocument(path: String): DocumentFile? {
val basePath = path.getBasePath(this)
val androidPath = File(basePath, "Android").path
var relativePath = path.substring(androidPath.length)
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1)
}
return try {
val treeUri = getAndroidTreeUri(path).toUri()
var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) {
document = document?.findFile(part)
}
document
} catch (ignored: Exception) {
null
}
}
fun Context.getSomeAndroidSAFDocument(path: String): DocumentFile? = getFastAndroidSAFDocument(path) ?: getAndroidSAFDocument(path)
fun Context.getFastAndroidSAFDocument(path: String): DocumentFile? {
val treeUri = getAndroidTreeUri(path)
if (treeUri.isEmpty()) {
return null
}
val uri = getAndroidSAFUri(path)
return DocumentFile.fromSingleUri(this, uri)
}
fun Context.getAndroidSAFChildrenUri(path: String): Uri {
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = createAndroidSAFDocumentId(path)
return DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
}
fun Context.createAndroidSAFDirectory(path: String): Boolean {
return try {
val treeUri = getAndroidTreeUri(path).toUri()
val parentPath = path.getParentPath()
if (!getDoesFilePathExist(parentPath)) {
createAndroidSAFDirectory(parentPath)
}
val documentId = createAndroidSAFDocumentId(parentPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.createDocument(contentResolver, parentUri, Document.MIME_TYPE_DIR, path.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.createAndroidSAFFile(path: String): Boolean {
return try {
val treeUri = getAndroidTreeUri(path).toUri()
val parentPath = path.getParentPath()
if (!getDoesFilePathExist(parentPath)) {
createAndroidSAFDirectory(parentPath)
}
val documentId = createAndroidSAFDocumentId(path.getParentPath())
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.createDocument(contentResolver, parentUri, path.getMimeType(), path.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.renameAndroidSAFDocument(oldPath: String, newPath: String): Boolean {
return try {
val treeUri = getAndroidTreeUri(oldPath).toUri()
val documentId = createAndroidSAFDocumentId(oldPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
DocumentsContract.renameDocument(contentResolver, parentUri, newPath.getFilenameFromPath()) != null
} catch (e: IllegalStateException) {
showErrorToast(e)
false
}
}
fun Context.getAndroidSAFFileSize(path: String): Long {
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = createAndroidSAFDocumentId(path)
return getFileSize(treeUri, documentId)
}
fun Context.getAndroidSAFFileCount(path: String, countHidden: Boolean): Int {
val treeUri = getAndroidTreeUri(path).toUri()
if (treeUri == Uri.EMPTY) {
return 0
}
val documentId = createAndroidSAFDocumentId(path)
val rootDocId = getStorageRootIdForAndroidDir(path)
return getProperChildrenCount(rootDocId, treeUri, documentId, countHidden)
}
fun Context.getAndroidSAFDirectChildrenCount(path: String, countHidden: Boolean): Int {
val treeUri = getAndroidTreeUri(path).toUri()
if (treeUri == Uri.EMPTY) {
return 0
}
val documentId = createAndroidSAFDocumentId(path)
val rootDocId = getStorageRootIdForAndroidDir(path)
return getDirectChildrenCount(rootDocId, treeUri, documentId, countHidden)
}
fun Context.getAndroidSAFLastModified(path: String): Long {
val treeUri = getAndroidTreeUri(path).toUri()
if (treeUri == Uri.EMPTY) {
return 0L
}
val documentId = createAndroidSAFDocumentId(path)
val projection = arrayOf(Document.COLUMN_LAST_MODIFIED)
val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return contentResolver.query(documentUri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getLongValue(Document.COLUMN_LAST_MODIFIED)
} else {
0L
}
} ?: 0L
}
fun Context.deleteAndroidSAFDirectory(path: String, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
val treeUri = getAndroidTreeUri(path).toUri()
val documentId = createAndroidSAFDocumentId(path)
try {
val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
val document = DocumentFile.fromSingleUri(this, uri)
val fileDeleted = (document!!.isFile || allowDeleteFolder) && DocumentsContract.deleteDocument(applicationContext.contentResolver, document.uri)
callback?.invoke(fileDeleted)
} catch (e: Exception) {
showErrorToast(e)
callback?.invoke(false)
storeAndroidTreeUri(path, "")
}
}
fun Context.trySAFFileDelete(fileDirItem: FileDirItem, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
var fileDeleted = tryFastDocumentDelete(fileDirItem.path, allowDeleteFolder)
if (!fileDeleted) {
val document = getDocumentFile(fileDirItem.path)
if (document != null && (fileDirItem.isDirectory == document.isDirectory)) {
try {
fileDeleted = (document.isFile || allowDeleteFolder) && DocumentsContract.deleteDocument(applicationContext.contentResolver, document.uri)
} catch (ignored: Exception) {
baseConfig.sdTreeUri = ""
baseConfig.sdCardPath = ""
}
}
}
if (fileDeleted) {
deleteFromMediaStore(fileDirItem.path)
callback?.invoke(true)
}
}
fun Context.getFileInputStreamSync(path: String): InputStream? {
return when {
isRestrictedSAFOnlyRoot(path) -> {
val uri = getAndroidSAFUri(path)
applicationContext.contentResolver.openInputStream(uri)
}
isAccessibleWithSAFSdk30(path) -> {
try {
FileInputStream(File(path))
} catch (e: Exception) {
val uri = createDocumentUriUsingFirstParentTreeUri(path)
applicationContext.contentResolver.openInputStream(uri)
}
}
isPathOnOTG(path) -> {
val fileDocument = getSomeDocumentFile(path)
applicationContext.contentResolver.openInputStream(fileDocument?.uri!!)
}
else -> FileInputStream(File(path))
}
}
fun Context.updateOTGPathFromPartition() {
val otgPath = "/storage/${baseConfig.OTGPartition}"
baseConfig.OTGPath = if (getOTGFastDocumentFile(otgPath, otgPath)?.exists() == true) {
"/storage/${baseConfig.OTGPartition}"
} else {
"/mnt/media_rw/${baseConfig.OTGPartition}"
}
}
fun Context.getDoesFilePathExist(path: String, otgPathToUse: String? = null): Boolean {
val otgPath = otgPathToUse ?: baseConfig.OTGPath
return when {
isRestrictedSAFOnlyRoot(path) -> getFastAndroidSAFDocument(path)?.exists() ?: false
otgPath.isNotEmpty() && path.startsWith(otgPath) -> getOTGFastDocumentFile(path)?.exists() ?: false
else -> File(path).exists()
}
}
fun Context.getIsPathDirectory(path: String): Boolean {
return when {
isRestrictedSAFOnlyRoot(path) -> getFastAndroidSAFDocument(path)?.isDirectory ?: false
isPathOnOTG(path) -> getOTGFastDocumentFile(path)?.isDirectory ?: false
else -> File(path).isDirectory
}
}
fun Context.getFolderLastModifieds(folder: String): HashMap<String, Long> {
val lastModifieds = HashMap<String, Long>()
val projection = arrayOf(
Images.Media.DISPLAY_NAME,
Images.Media.DATE_MODIFIED
)
val uri = Files.getContentUri("external")
val selection = "${Images.Media.DATA} LIKE ? AND ${Images.Media.DATA} NOT LIKE ? AND ${Images.Media.MIME_TYPE} IS NOT NULL" // avoid selecting folders
val selectionArgs = arrayOf("$folder/%", "$folder/%/%")
try {
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
do {
try {
val lastModified = cursor.getLongValue(Images.Media.DATE_MODIFIED) * 1000
if (lastModified != 0L) {
val name = cursor.getStringValue(Images.Media.DISPLAY_NAME)
lastModifieds["$folder/$name"] = lastModified
}
} catch (e: Exception) {
}
} while (cursor.moveToNext())
}
}
} catch (e: Exception) {
}
return lastModifieds
}
// avoid these being set as SD card paths
private val physicalPaths = arrayListOf(
"/storage/sdcard1", // Motorola Xoom
"/storage/extsdcard", // Samsung SGS3
"/storage/sdcard0/external_sdcard", // User request
"/mnt/extsdcard", "/mnt/sdcard/external_sd", // Samsung galaxy family
"/mnt/external_sd", "/mnt/media_rw/sdcard1", // 4.4.2 on CyanogenMod S3
"/removable/microsd", // Asus transformer prime
"/mnt/emmc", "/storage/external_SD", // LG
"/storage/ext_sd", // HTC One Max
"/storage/removable/sdcard1", // Sony Xperia Z1
"/data/sdext", "/data/sdext2", "/data/sdext3", "/data/sdext4", "/sdcard1", // Sony Xperia Z
"/sdcard2", // HTC One M8s
"/storage/usbdisk0",
"/storage/usbdisk1",
"/storage/usbdisk2"
)
// Convert paths like /storage/emulated/0/Pictures/Screenshots/first.jpg to content://media/external/images/media/131799
// so that we can refer to the file in the MediaStore.
// If we found no mediastore uri for a given file, do not return its path either to avoid some mismatching
fun Context.getUrisPathsFromFileDirItems(fileDirItems: List<FileDirItem>): Pair<ArrayList<String>, ArrayList<Uri>> {
val fileUris = ArrayList<Uri>()
val successfulFilePaths = ArrayList<String>()
val allIds = getMediaStoreIds(this)
val filePaths = fileDirItems.map { it.path }
filePaths.forEach { path ->
for ((filePath, mediaStoreId) in allIds) {
if (filePath.lowercase() == path.lowercase()) {
val baseUri = getFileUri(filePath)
val uri = ContentUris.withAppendedId(baseUri, mediaStoreId)
fileUris.add(uri)
successfulFilePaths.add(path)
}
}
}
return Pair(successfulFilePaths, fileUris)
}
fun getMediaStoreIds(context: Context): HashMap<String, Long> {
val ids = HashMap<String, Long>()
val projection = arrayOf(
Images.Media.DATA,
Images.Media._ID
)
val uri = Files.getContentUri("external")
try {
context.queryCursor(uri, projection) { cursor ->
try {
val id = cursor.getLongValue(Images.Media._ID)
if (id != 0L) {
val path = cursor.getStringValue(Images.Media.DATA)
ids[path] = id
}
} catch (e: Exception) {
}
}
} catch (e: Exception) {
}
return ids
}
fun Context.getFileUrisFromFileDirItems(fileDirItems: List<FileDirItem>): List<Uri> {
val fileUris = getUrisPathsFromFileDirItems(fileDirItems).second
if (fileUris.isEmpty()) {
fileDirItems.map { fileDirItem ->
fileUris.add(fileDirItem.assembleContentUri())
}
}
return fileUris
}

View File

@ -0,0 +1,179 @@
package com.simplemobiletools.commons.extensions
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.view.ViewGroup
import androidx.loader.content.CursorLoader
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.SharedTheme
import com.simplemobiletools.commons.views.*
// handle system default theme (Material You) specially as the color is taken from the system, not hardcoded by us
fun Context.getProperTextColor() = if (baseConfig.isUsingSystemTheme) {
resources.getColor(R.color.you_neutral_text_color, theme)
} else {
baseConfig.textColor
}
fun Context.getProperBackgroundColor() = if (baseConfig.isUsingSystemTheme) {
resources.getColor(R.color.you_background_color, theme)
} else {
baseConfig.backgroundColor
}
fun Context.getProperPrimaryColor() = when {
baseConfig.isUsingSystemTheme -> resources.getColor(R.color.you_primary_color, theme)
isWhiteTheme() || isBlackAndWhiteTheme() -> baseConfig.accentColor
else -> baseConfig.primaryColor
}
fun Context.getProperStatusBarColor() = when {
baseConfig.isUsingSystemTheme -> resources.getColor(R.color.you_status_bar_color, theme)
else -> getProperBackgroundColor()
}
// get the color of the statusbar with material activity, if the layout is scrolled down a bit
fun Context.getColoredMaterialStatusBarColor(): Int {
return if (baseConfig.isUsingSystemTheme) {
resources.getColor(R.color.you_status_bar_color, theme)
} else {
getProperPrimaryColor()
}
}
fun Context.updateTextColors(viewGroup: ViewGroup) {
val textColor = when {
baseConfig.isUsingSystemTheme -> getProperTextColor()
else -> baseConfig.textColor
}
val backgroundColor = baseConfig.backgroundColor
val accentColor = when {
isWhiteTheme() || isBlackAndWhiteTheme() -> baseConfig.accentColor
else -> getProperPrimaryColor()
}
val cnt = viewGroup.childCount
(0 until cnt).map { viewGroup.getChildAt(it) }.forEach {
when (it) {
is MyTextView -> it.setColors(textColor, accentColor, backgroundColor)
is MyAppCompatSpinner -> it.setColors(textColor, accentColor, backgroundColor)
is MyCompatRadioButton -> it.setColors(textColor, accentColor, backgroundColor)
is MyAppCompatCheckbox -> it.setColors(textColor, accentColor, backgroundColor)
is MyEditText -> it.setColors(textColor, accentColor, backgroundColor)
is MyAutoCompleteTextView -> it.setColors(textColor, accentColor, backgroundColor)
is MyFloatingActionButton -> it.setColors(textColor, accentColor, backgroundColor)
is MySeekBar -> it.setColors(textColor, accentColor, backgroundColor)
is MyButton -> it.setColors(textColor, accentColor, backgroundColor)
is MyTextInputLayout -> it.setColors(textColor, accentColor, backgroundColor)
is ViewGroup -> updateTextColors(it)
}
}
}
fun Context.isBlackAndWhiteTheme() = baseConfig.textColor == Color.WHITE && baseConfig.primaryColor == Color.BLACK && baseConfig.backgroundColor == Color.BLACK
fun Context.isWhiteTheme() = baseConfig.textColor == DARK_GREY && baseConfig.primaryColor == Color.WHITE && baseConfig.backgroundColor == Color.WHITE
fun Context.isUsingSystemDarkTheme() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES != 0
fun Context.getTimePickerDialogTheme() = when {
baseConfig.isUsingSystemTheme -> if (isUsingSystemDarkTheme()) {
R.style.MyTimePickerMaterialTheme_Dark
} else {
R.style.MyDateTimePickerMaterialTheme
}
baseConfig.backgroundColor.getContrastColor() == Color.WHITE -> R.style.MyDialogTheme_Dark
else -> R.style.MyDialogTheme
}
fun Context.getDatePickerDialogTheme() = when {
baseConfig.isUsingSystemTheme -> R.style.MyDateTimePickerMaterialTheme
baseConfig.backgroundColor.getContrastColor() == Color.WHITE -> R.style.MyDialogTheme_Dark
else -> R.style.MyDialogTheme
}
fun Context.getPopupMenuTheme(): Int {
return if (isSPlus() && baseConfig.isUsingSystemTheme) {
R.style.AppTheme_YouPopupMenuStyle
} else if (isWhiteTheme()) {
R.style.AppTheme_PopupMenuLightStyle
} else {
R.style.AppTheme_PopupMenuDarkStyle
}
}
fun Context.getSharedTheme(callback: (sharedTheme: SharedTheme?) -> Unit) {
if (!isThankYouInstalled()) {
callback(null)
} else {
val cursorLoader = getMyContentProviderCursorLoader()
ensureBackgroundThread {
callback(getSharedThemeSync(cursorLoader))
}
}
}
fun Context.getSharedThemeSync(cursorLoader: CursorLoader): SharedTheme? {
val cursor = cursorLoader.loadInBackground()
cursor?.use {
if (cursor.moveToFirst()) {
try {
val textColor = cursor.getIntValue(MyContentProvider.COL_TEXT_COLOR)
val backgroundColor = cursor.getIntValue(MyContentProvider.COL_BACKGROUND_COLOR)
val primaryColor = cursor.getIntValue(MyContentProvider.COL_PRIMARY_COLOR)
val accentColor = cursor.getIntValue(MyContentProvider.COL_ACCENT_COLOR)
val appIconColor = cursor.getIntValue(MyContentProvider.COL_APP_ICON_COLOR)
val lastUpdatedTS = cursor.getIntValue(MyContentProvider.COL_LAST_UPDATED_TS)
return SharedTheme(textColor, backgroundColor, primaryColor, appIconColor, lastUpdatedTS, accentColor)
} catch (e: Exception) {
}
}
}
return null
}
fun Context.checkAppIconColor() {
val appId = baseConfig.appId
if (appId.isNotEmpty() && baseConfig.lastIconColor != baseConfig.appIconColor) {
getAppIconColors().forEachIndexed { index, color ->
toggleAppIconColor(appId, index, color, false)
}
getAppIconColors().forEachIndexed { index, color ->
if (baseConfig.appIconColor == color) {
toggleAppIconColor(appId, index, color, true)
}
}
}
}
fun Context.toggleAppIconColor(appId: String, colorIndex: Int, color: Int, enable: Boolean) {
val className = "${appId.removeSuffix(".debug")}.activities.SplashActivity${appIconColorStrings[colorIndex]}"
val state = if (enable) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED
try {
packageManager.setComponentEnabledSetting(ComponentName(appId, className), state, PackageManager.DONT_KILL_APP)
if (enable) {
baseConfig.lastIconColor = color
}
} catch (e: Exception) {
}
}
fun Context.getAppIconColors() = resources.getIntArray(R.array.md_app_icon_colors).toCollection(ArrayList())
@SuppressLint("NewApi")
fun Context.getBottomNavigationBackgroundColor(): Int {
val baseColor = baseConfig.backgroundColor
val bottomColor = when {
baseConfig.isUsingSystemTheme -> resources.getColor(R.color.you_status_bar_color, theme)
baseColor == Color.WHITE -> resources.getColor(R.color.bottom_tabs_light_background)
else -> baseConfig.backgroundColor.lightenColor(4)
}
return bottomColor
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
package com.simplemobiletools.commons.extensions
import android.database.Cursor
fun Cursor.getStringValue(key: String) = getString(getColumnIndex(key))
fun Cursor.getStringValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getString(getColumnIndex(key))
fun Cursor.getIntValue(key: String) = getInt(getColumnIndex(key))
fun Cursor.getIntValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getInt(getColumnIndex(key))
fun Cursor.getLongValue(key: String) = getLong(getColumnIndex(key))
fun Cursor.getLongValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getLong(getColumnIndex(key))
fun Cursor.getBlobValue(key: String) = getBlob(getColumnIndex(key))

View File

@ -0,0 +1,52 @@
package com.simplemobiletools.commons.extensions
import androidx.documentfile.provider.DocumentFile
fun DocumentFile.getItemSize(countHiddenItems: Boolean): Long {
return if (isDirectory) {
getDirectorySize(this, countHiddenItems)
} else {
length()
}
}
private fun getDirectorySize(dir: DocumentFile, countHiddenItems: Boolean): Long {
var size = 0L
if (dir.exists()) {
val files = dir.listFiles()
for (i in files.indices) {
val file = files[i]
if (file.isDirectory) {
size += getDirectorySize(file, countHiddenItems)
} else if (!file.name!!.startsWith(".") || countHiddenItems) {
size += file.length()
}
}
}
return size
}
fun DocumentFile.getFileCount(countHiddenItems: Boolean): Int {
return if (isDirectory) {
getDirectoryFileCount(this, countHiddenItems)
} else {
1
}
}
private fun getDirectoryFileCount(dir: DocumentFile, countHiddenItems: Boolean): Int {
var count = 0
if (dir.exists()) {
val files = dir.listFiles()
for (i in files.indices) {
val file = files[i]
if (file.isDirectory) {
count++
count += getDirectoryFileCount(file, countHiddenItems)
} else if (!file.name!!.startsWith(".") || countHiddenItems) {
count++
}
}
}
return count
}

View File

@ -0,0 +1,28 @@
package com.simplemobiletools.commons.extensions
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
fun Drawable.applyColorFilter(color: Int) = mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN)
fun Drawable.convertToBitmap(): Bitmap {
val bitmap = if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
} else {
Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888)
}
if (this is BitmapDrawable) {
if (this.bitmap != null) {
return this.bitmap
}
}
val canvas = Canvas(bitmap!!)
setBounds(0, 0, canvas.width, canvas.height)
draw(canvas)
return bitmap
}

View File

@ -0,0 +1,44 @@
package com.simplemobiletools.commons.extensions
import android.text.Editable
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.style.BackgroundColorSpan
import android.widget.EditText
import android.widget.TextView
import androidx.core.graphics.ColorUtils
val EditText.value: String get() = text.toString().trim()
fun EditText.onTextChangeListener(onTextChangedAction: (newText: String) -> Unit) = addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
onTextChangedAction(s.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
fun EditText.highlightText(highlightText: String, color: Int) {
val content = text.toString()
var indexOf = content.indexOf(highlightText, 0, true)
val wordToSpan = SpannableString(text)
var offset = 0
while (offset < content.length && indexOf != -1) {
indexOf = content.indexOf(highlightText, offset, true)
if (indexOf == -1) {
break
} else {
val spanBgColor = BackgroundColorSpan(ColorUtils.setAlphaComponent(color, 128))
val spanFlag = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
wordToSpan.setSpan(spanBgColor, indexOf, indexOf + highlightText.length, spanFlag)
setText(wordToSpan, TextView.BufferType.SPANNABLE)
}
offset = indexOf + 1
}
}

View File

@ -0,0 +1,13 @@
package com.simplemobiletools.commons.extensions
import android.text.Editable
import android.text.style.BackgroundColorSpan
fun Editable.clearBackgroundSpans() {
val spans = getSpans(0, length, Any::class.java)
for (span in spans) {
if (span is BackgroundColorSpan) {
removeSpan(span)
}
}
}

View File

@ -0,0 +1,148 @@
package com.simplemobiletools.commons.extensions
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import androidx.exifinterface.media.ExifInterface
import java.text.SimpleDateFormat
import java.util.*
fun ExifInterface.copyTo(destination: ExifInterface, copyOrientation: Boolean = true) {
val attributes = arrayListOf(
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_ISO_SPEED_RATINGS,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_WHITE_BALANCE
)
if (copyOrientation) {
attributes.add(ExifInterface.TAG_ORIENTATION)
}
attributes.forEach {
val value = getAttribute(it)
if (value != null) {
destination.setAttribute(it, value)
}
}
try {
destination.saveAttributes()
} catch (ignored: Exception) {
}
}
fun ExifInterface.removeValues() {
val attributes = arrayListOf(
// ExifInterface.TAG_ORIENTATION, // do not remove the orientation, it could lead to unexpected behaviour at displaying the file
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_ISO_SPEED_RATINGS,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_WHITE_BALANCE
)
attributes.forEach {
setAttribute(it, null)
}
saveAttributes()
}
fun ExifInterface.getExifProperties(): String {
var exifString = ""
getAttribute(ExifInterface.TAG_F_NUMBER).let {
if (it?.isNotEmpty() == true) {
val number = it.trimEnd('0').trimEnd('.')
exifString += "F/$number "
}
}
getAttribute(ExifInterface.TAG_FOCAL_LENGTH).let {
if (it?.isNotEmpty() == true) {
val values = it.split('/')
val focalLength = "${values[0].toDouble() / values[1].toDouble()}mm"
exifString += "$focalLength "
}
}
getAttribute(ExifInterface.TAG_EXPOSURE_TIME).let {
if (it?.isNotEmpty() == true) {
val exposureValue = it.toFloat()
exifString += if (exposureValue > 1f) {
"${exposureValue}s "
} else {
"1/${Math.round(1 / exposureValue)}s "
}
}
}
getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS).let {
if (it?.isNotEmpty() == true) {
exifString += "ISO-$it"
}
}
return exifString.trim()
}
@TargetApi(Build.VERSION_CODES.N)
fun ExifInterface.getExifDateTaken(context: Context): String {
val dateTime = getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL) ?: getAttribute(ExifInterface.TAG_DATETIME)
dateTime.let {
if (it?.isNotEmpty() == true) {
try {
val simpleDateFormat = SimpleDateFormat("yyyy:MM:dd kk:mm:ss", Locale.ENGLISH)
return simpleDateFormat.parse(it).time.formatDate(context).trim()
} catch (ignored: Exception) {
}
}
}
return ""
}
fun ExifInterface.getExifCameraModel(): String {
getAttribute(ExifInterface.TAG_MAKE).let {
if (it?.isNotEmpty() == true) {
val model = getAttribute(ExifInterface.TAG_MODEL)
return "$it $model".trim()
}
}
return ""
}

View File

@ -0,0 +1,156 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FileDirItem
import java.io.File
import java.util.*
fun File.isMediaFile() = absolutePath.isMediaFile()
fun File.isGif() = absolutePath.endsWith(".gif", true)
fun File.isApng() = absolutePath.endsWith(".apng", true)
fun File.isVideoFast() = videoExtensions.any { absolutePath.endsWith(it, true) }
fun File.isImageFast() = photoExtensions.any { absolutePath.endsWith(it, true) }
fun File.isAudioFast() = audioExtensions.any { absolutePath.endsWith(it, true) }
fun File.isRawFast() = rawExtensions.any { absolutePath.endsWith(it, true) }
fun File.isSvg() = absolutePath.isSvg()
fun File.isPortrait() = absolutePath.isPortrait()
fun File.isImageSlow() = absolutePath.isImageFast() || getMimeType().startsWith("image")
fun File.isVideoSlow() = absolutePath.isVideoFast() || getMimeType().startsWith("video")
fun File.isAudioSlow() = absolutePath.isAudioFast() || getMimeType().startsWith("audio")
fun File.getMimeType() = absolutePath.getMimeType()
fun File.getProperSize(countHiddenItems: Boolean): Long {
return if (isDirectory) {
getDirectorySize(this, countHiddenItems)
} else {
length()
}
}
private fun getDirectorySize(dir: File, countHiddenItems: Boolean): Long {
var size = 0L
if (dir.exists()) {
val files = dir.listFiles()
if (files != null) {
for (i in files.indices) {
if (files[i].isDirectory) {
size += getDirectorySize(files[i], countHiddenItems)
} else if (!files[i].name.startsWith('.') && !dir.name.startsWith('.') || countHiddenItems) {
size += files[i].length()
}
}
}
}
return size
}
fun File.getFileCount(countHiddenItems: Boolean): Int {
return if (isDirectory) {
getDirectoryFileCount(this, countHiddenItems)
} else {
1
}
}
private fun getDirectoryFileCount(dir: File, countHiddenItems: Boolean): Int {
var count = -1
if (dir.exists()) {
val files = dir.listFiles()
if (files != null) {
count++
for (i in files.indices) {
val file = files[i]
if (file.isDirectory) {
count++
count += getDirectoryFileCount(file, countHiddenItems)
} else if (!file.name.startsWith('.') || countHiddenItems) {
count++
}
}
}
}
return count
}
fun File.getDirectChildrenCount(context: Context, countHiddenItems: Boolean): Int {
val fileCount = if (context.isRestrictedSAFOnlyRoot(path)) {
context.getAndroidSAFDirectChildrenCount(
path,
countHiddenItems
)
} else {
listFiles()?.filter {
if (countHiddenItems) {
true
} else {
!it.name.startsWith('.')
}
}?.size ?: 0
}
return fileCount
}
fun File.toFileDirItem(context: Context) = FileDirItem(absolutePath, name, context.getIsPathDirectory(absolutePath), 0, length(), lastModified())
fun File.containsNoMedia(): Boolean {
return if (!isDirectory) {
false
} else {
File(this, NOMEDIA).exists()
}
}
fun File.doesThisOrParentHaveNoMedia(
folderNoMediaStatuses: HashMap<String, Boolean>,
callback: ((path: String, hasNoMedia: Boolean) -> Unit)?
): Boolean {
var curFile = this
while (true) {
val noMediaPath = "${curFile.absolutePath}/$NOMEDIA"
val hasNoMedia = if (folderNoMediaStatuses.keys.contains(noMediaPath)) {
folderNoMediaStatuses[noMediaPath]!!
} else {
val contains = curFile.containsNoMedia()
callback?.invoke(curFile.absolutePath, contains)
contains
}
if (hasNoMedia) {
return true
}
curFile = curFile.parentFile ?: break
if (curFile.absolutePath == "/") {
break
}
}
return false
}
fun File.doesParentHaveNoMedia(): Boolean {
var curFile = parentFile
while (true) {
if (curFile?.containsNoMedia() == true) {
return true
}
curFile = curFile?.parentFile ?: break
if (curFile.absolutePath == "/") {
break
}
}
return false
}
fun File.getDigest(algorithm: String): String? {
return try {
inputStream().getDigest(algorithm)
} catch (e: Exception) {
null
}
}
fun File.md5() = this.getDigest(MD5)

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import com.simplemobiletools.commons.models.FileDirItem
fun FileDirItem.isRecycleBinPath(context: Context): Boolean {
return path.startsWith(context.recycleBinPath)
}

View File

@ -0,0 +1,30 @@
package com.simplemobiletools.commons.extensions
import android.graphics.PorterDuff
import android.graphics.drawable.GradientDrawable
import android.widget.ImageView
import androidx.annotation.DrawableRes
fun ImageView.setFillWithStroke(fillColor: Int, backgroundColor: Int, drawRectangle: Boolean = false) {
GradientDrawable().apply {
shape = if (drawRectangle) GradientDrawable.RECTANGLE else GradientDrawable.OVAL
setColor(fillColor)
background = this
if (backgroundColor == fillColor || fillColor == -2 && backgroundColor == -1) {
val strokeColor = backgroundColor.getContrastColor().adjustAlpha(0.5f)
setStroke(2, strokeColor)
}
}
}
fun ImageView.applyColorFilter(color: Int) = setColorFilter(color, PorterDuff.Mode.SRC_IN)
fun ImageView.setImageResourceOrBeGone(@DrawableRes imageRes: Int?) {
if (imageRes != null) {
beVisible()
setImageResource(imageRes)
} else {
beGone()
}
}

View File

@ -0,0 +1,21 @@
package com.simplemobiletools.commons.extensions
import com.simplemobiletools.commons.helpers.MD5
import java.io.InputStream
import java.security.MessageDigest
fun InputStream.getDigest(algorithm: String): String {
return use { fis ->
val md = MessageDigest.getInstance(algorithm)
val buffer = ByteArray(8192)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
}
fun InputStream.md5(): String = this.getDigest(MD5)

View File

@ -0,0 +1,195 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.ExifInterface
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.text.format.Time
import com.simplemobiletools.commons.helpers.DARK_GREY
import java.text.DecimalFormat
import java.util.*
fun Int.getContrastColor(): Int {
val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
return if (y >= 149 && this != Color.BLACK) DARK_GREY else Color.WHITE
}
fun Int.toHex() = String.format("#%06X", 0xFFFFFF and this).toUpperCase()
fun Int.adjustAlpha(factor: Float): Int {
val alpha = Math.round(Color.alpha(this) * factor)
val red = Color.red(this)
val green = Color.green(this)
val blue = Color.blue(this)
return Color.argb(alpha, red, green, blue)
}
fun Int.getFormattedDuration(forceShowHours: Boolean = false): String {
val sb = StringBuilder(8)
val hours = this / 3600
val minutes = this % 3600 / 60
val seconds = this % 60
if (this >= 3600) {
sb.append(String.format(Locale.getDefault(), "%02d", hours)).append(":")
} else if (forceShowHours) {
sb.append("0:")
}
sb.append(String.format(Locale.getDefault(), "%02d", minutes))
sb.append(":").append(String.format(Locale.getDefault(), "%02d", seconds))
return sb.toString()
}
fun Int.formatSize(): String {
if (this <= 0) {
return "0 B"
}
val units = arrayOf("B", "kB", "MB", "GB", "TB")
val digitGroups = (Math.log10(toDouble()) / Math.log10(1024.0)).toInt()
return "${DecimalFormat("#,##0.#").format(this / Math.pow(1024.0, digitGroups.toDouble()))} ${units[digitGroups]}"
}
fun Int.formatDate(context: Context, dateFormat: String? = null, timeFormat: String? = null): String {
val useDateFormat = dateFormat ?: context.baseConfig.dateFormat
val useTimeFormat = timeFormat ?: context.getTimeFormat()
val cal = Calendar.getInstance(Locale.ENGLISH)
cal.timeInMillis = this * 1000L
return DateFormat.format("$useDateFormat, $useTimeFormat", cal).toString()
}
// if the given date is today, we show only the time. Else we show the date and optionally the time too
fun Int.formatDateOrTime(context: Context, hideTimeAtOtherDays: Boolean, showYearEvenIfCurrent: Boolean): String {
val cal = Calendar.getInstance(Locale.ENGLISH)
cal.timeInMillis = this * 1000L
return if (DateUtils.isToday(this * 1000L)) {
DateFormat.format(context.getTimeFormat(), cal).toString()
} else {
var format = context.baseConfig.dateFormat
if (!showYearEvenIfCurrent && isThisYear()) {
format = format.replace("y", "").trim().trim('-').trim('.').trim('/')
}
if (!hideTimeAtOtherDays) {
format += ", ${context.getTimeFormat()}"
}
DateFormat.format(format, cal).toString()
}
}
fun Int.isThisYear(): Boolean {
val time = Time()
time.set(this * 1000L)
val thenYear = time.year
time.set(System.currentTimeMillis())
return (thenYear == time.year)
}
fun Int.addBitIf(add: Boolean, bit: Int) =
if (add) {
addBit(bit)
} else {
removeBit(bit)
}
// TODO: how to do "bits & ~bit" in kotlin?
fun Int.removeBit(bit: Int) = addBit(bit) - bit
fun Int.addBit(bit: Int) = this or bit
fun Int.flipBit(bit: Int) = if (this and bit == 0) addBit(bit) else removeBit(bit)
fun ClosedRange<Int>.random() = Random().nextInt(endInclusive - start) + start
// taken from https://stackoverflow.com/a/40964456/1967672
fun Int.darkenColor(factor: Int = 8): Int {
if (this == Color.WHITE || this == Color.BLACK) {
return this
}
val DARK_FACTOR = factor
var hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
val hsl = hsv2hsl(hsv)
hsl[2] -= DARK_FACTOR / 100f
if (hsl[2] < 0)
hsl[2] = 0f
hsv = hsl2hsv(hsl)
return Color.HSVToColor(hsv)
}
fun Int.lightenColor(factor: Int = 8): Int {
if (this == Color.WHITE || this == Color.BLACK) {
return this
}
val LIGHT_FACTOR = factor
var hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
val hsl = hsv2hsl(hsv)
hsl[2] += LIGHT_FACTOR / 100f
if (hsl[2] < 0)
hsl[2] = 0f
hsv = hsl2hsv(hsl)
return Color.HSVToColor(hsv)
}
private fun hsl2hsv(hsl: FloatArray): FloatArray {
val hue = hsl[0]
var sat = hsl[1]
val light = hsl[2]
sat *= if (light < .5) light else 1 - light
return floatArrayOf(hue, 2f * sat / (light + sat), light + sat)
}
private fun hsv2hsl(hsv: FloatArray): FloatArray {
val hue = hsv[0]
val sat = hsv[1]
val value = hsv[2]
val newHue = (2f - sat) * value
var newSat = sat * value / if (newHue < 1f) newHue else 2f - newHue
if (newSat > 1f)
newSat = 1f
return floatArrayOf(hue, newSat, newHue / 2f)
}
fun Int.orientationFromDegrees() = when (this) {
270 -> ExifInterface.ORIENTATION_ROTATE_270
180 -> ExifInterface.ORIENTATION_ROTATE_180
90 -> ExifInterface.ORIENTATION_ROTATE_90
else -> ExifInterface.ORIENTATION_NORMAL
}.toString()
fun Int.degreesFromOrientation() = when (this) {
ExifInterface.ORIENTATION_ROTATE_270 -> 270
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_90 -> 90
else -> 0
}
fun Int.ensureTwoDigits(): String {
return if (toString().length == 1) {
"0$this"
} else {
toString()
}
}
fun Int.getColorStateList(): ColorStateList {
val states = arrayOf(intArrayOf(android.R.attr.state_enabled),
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(-android.R.attr.state_checked),
intArrayOf(android.R.attr.state_pressed)
)
val colors = intArrayOf(this, this, this, this)
return ColorStateList(states, colors)
}

View File

@ -0,0 +1,23 @@
package com.simplemobiletools.commons.extensions
import java.util.*
fun List<String>.getMimeType(): String {
val mimeGroups = HashSet<String>(size)
val subtypes = HashSet<String>(size)
forEach {
val parts = it.getMimeType().split("/")
if (parts.size == 2) {
mimeGroups.add(parts.getOrElse(0) { "" })
subtypes.add(parts.getOrElse(1) { "" })
} else {
return "*/*"
}
}
return when {
subtypes.size == 1 -> "${mimeGroups.first()}/${subtypes.first()}"
mimeGroups.size == 1 -> "${mimeGroups.first()}/*"
else -> "*/*"
}
}

View File

@ -0,0 +1,24 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import android.text.format.DateFormat
import java.text.DecimalFormat
import java.util.*
fun Long.formatSize(): String {
if (this <= 0) {
return "0 B"
}
val units = arrayOf("B", "kB", "MB", "GB", "TB")
val digitGroups = (Math.log10(toDouble()) / Math.log10(1024.0)).toInt()
return "${DecimalFormat("#,##0.#").format(this / Math.pow(1024.0, digitGroups.toDouble()))} ${units[digitGroups]}"
}
fun Long.formatDate(context: Context, dateFormat: String? = null, timeFormat: String? = null): String {
val useDateFormat = dateFormat ?: context.baseConfig.dateFormat
val useTimeFormat = timeFormat ?: context.getTimeFormat()
val cal = Calendar.getInstance(Locale.ENGLISH)
cal.timeInMillis = this
return DateFormat.format("$useDateFormat, $useTimeFormat", cal).toString()
}

View File

@ -0,0 +1,11 @@
package com.simplemobiletools.commons.extensions
import android.graphics.Point
fun Point.formatAsResolution() = "$x x $y ${getMPx()}"
fun Point.getMPx(): String {
val px = x * y / 1000000f
val rounded = Math.round(px * 10) / 10f
return "(${rounded}MP)"
}

View File

@ -0,0 +1,27 @@
package com.simplemobiletools.commons.extensions
import android.graphics.Color
import android.view.View
import android.widget.RemoteViews
fun RemoteViews.setBackgroundColor(id: Int, color: Int) {
setInt(id, "setBackgroundColor", color)
}
fun RemoteViews.setTextSize(id: Int, size: Float) {
setFloat(id, "setTextSize", size)
}
fun RemoteViews.setText(id: Int, text: String) {
setTextViewText(id, text)
}
fun RemoteViews.setVisibleIf(id: Int, beVisible: Boolean) {
val visibility = if (beVisible) View.VISIBLE else View.GONE
setViewVisibility(id, visibility)
}
fun RemoteViews.applyColorFilter(id: Int, color: Int) {
setInt(id, "setColorFilter", color)
setInt(id, "setImageAlpha", Color.alpha(color))
}

View File

@ -0,0 +1,40 @@
package com.simplemobiletools.commons.extensions
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
fun Resources.getColoredBitmap(resourceId: Int, newColor: Int): Bitmap {
val drawable = getDrawable(resourceId)
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.colorFilter = PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_IN)
drawable.draw(canvas)
return bitmap
}
fun Resources.getColoredDrawable(drawableId: Int, colorId: Int, alpha: Int = 255) = getColoredDrawableWithColor(drawableId, getColor(colorId), alpha)
fun Resources.getColoredDrawableWithColor(drawableId: Int, color: Int, alpha: Int = 255): Drawable {
val drawable = getDrawable(drawableId)
drawable.mutate().applyColorFilter(color)
drawable.mutate().alpha = alpha
return drawable
}
fun Resources.hasNavBar(): Boolean {
val id = getIdentifier("config_showNavigationBar", "bool", "android")
return id > 0 && getBoolean(id)
}
fun Resources.getNavBarHeight(): Int {
val id = getIdentifier("navigation_bar_height", "dimen", "android")
return if (id > 0 && hasNavBar()) {
getDimensionPixelSize(id)
} else
0
}

View File

@ -0,0 +1,13 @@
package com.simplemobiletools.commons.extensions
import android.widget.SeekBar
fun SeekBar.onSeekBarChangeListener(seekBarChangeListener: (progress: Int) -> Unit) = setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
seekBarChangeListener(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})

View File

@ -0,0 +1,930 @@
package com.simplemobiletools.commons.extensions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Point
import android.os.StatFs
import android.provider.MediaStore
import android.telephony.PhoneNumberUtils
import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.widget.TextView
import com.bumptech.glide.signature.ObjectKey
import com.simplemobiletools.commons.helpers.*
import org.joda.time.DateTime
import org.joda.time.Years
import org.joda.time.format.DateTimeFormat
import java.io.File
import java.text.DateFormat
import java.text.Normalizer
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
fun String.getFilenameFromPath() = substring(lastIndexOf("/") + 1)
fun String.getFilenameExtension() = substring(lastIndexOf(".") + 1)
fun String.getBasePath(context: Context): String {
return when {
startsWith(context.internalStoragePath) -> context.internalStoragePath
context.isPathOnSD(this) -> context.sdCardPath
context.isPathOnOTG(this) -> context.otgPath
else -> "/"
}
}
fun String.getFirstParentDirName(context: Context, level: Int): String? {
val basePath = getBasePath(context)
val startIndex = basePath.length + 1
return if (length > startIndex) {
val pathWithoutBasePath = substring(startIndex)
val pathSegments = pathWithoutBasePath.split("/")
if (level < pathSegments.size) {
pathSegments.slice(0..level).joinToString("/")
} else {
null
}
} else {
null
}
}
fun String.getFirstParentPath(context: Context, level: Int): String {
val basePath = getBasePath(context)
val startIndex = basePath.length + 1
return if (length > startIndex) {
val pathWithoutBasePath = substring(basePath.length + 1)
val pathSegments = pathWithoutBasePath.split("/")
val firstParentPath = if (level < pathSegments.size) {
pathSegments.slice(0..level).joinToString("/")
} else {
pathWithoutBasePath
}
"$basePath/$firstParentPath"
} else {
basePath
}
}
fun String.isAValidFilename(): Boolean {
val ILLEGAL_CHARACTERS = charArrayOf('/', '\n', '\r', '\t', '\u0000', '`', '?', '*', '\\', '<', '>', '|', '\"', ':')
ILLEGAL_CHARACTERS.forEach {
if (contains(it))
return false
}
return true
}
fun String.getOTGPublicPath(context: Context) =
"${context.baseConfig.OTGTreeUri}/document/${context.baseConfig.OTGPartition}%3A${substring(context.baseConfig.OTGPath.length).replace("/", "%2F")}"
fun String.isMediaFile() = isImageFast() || isVideoFast() || isGif() || isRawFast() || isSvg() || isPortrait()
fun String.isWebP() = endsWith(".webp", true)
fun String.isGif() = endsWith(".gif", true)
fun String.isPng() = endsWith(".png", true)
fun String.isApng() = endsWith(".apng", true)
fun String.isJpg() = endsWith(".jpg", true) or endsWith(".jpeg", true)
fun String.isSvg() = endsWith(".svg", true)
fun String.isPortrait() = getFilenameFromPath().contains("portrait", true) && File(this).parentFile?.name?.startsWith("img_", true) == true
// fast extension checks, not guaranteed to be accurate
fun String.isVideoFast() = videoExtensions.any { endsWith(it, true) }
fun String.isImageFast() = photoExtensions.any { endsWith(it, true) }
fun String.isAudioFast() = audioExtensions.any { endsWith(it, true) }
fun String.isRawFast() = rawExtensions.any { endsWith(it, true) }
fun String.isImageSlow() = isImageFast() || getMimeType().startsWith("image") || startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
fun String.isVideoSlow() = isVideoFast() || getMimeType().startsWith("video") || startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())
fun String.isAudioSlow() = isAudioFast() || getMimeType().startsWith("audio") || startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())
fun String.canModifyEXIF() = extensionsSupportingEXIF.any { endsWith(it, true) }
fun String.getCompressionFormat() = when (getFilenameExtension().toLowerCase()) {
"png" -> Bitmap.CompressFormat.PNG
"webp" -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.JPEG
}
fun String.areDigitsOnly() = matches(Regex("[0-9]+"))
fun String.areLettersOnly() = matches(Regex("[a-zA-Z]+"))
fun String.getGenericMimeType(): String {
if (!contains("/"))
return this
val type = substring(0, indexOf("/"))
return "$type/*"
}
fun String.getParentPath() = removeSuffix("/${getFilenameFromPath()}")
fun String.relativizeWith(path: String) = this.substring(path.length)
fun String.containsNoMedia() = File(this).containsNoMedia()
fun String.doesThisOrParentHaveNoMedia(folderNoMediaStatuses: HashMap<String, Boolean>, callback: ((path: String, hasNoMedia: Boolean) -> Unit)?) =
File(this).doesThisOrParentHaveNoMedia(folderNoMediaStatuses, callback)
fun String.getImageResolution(context: Context): Point? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
if (context.isRestrictedSAFOnlyRoot(this)) {
BitmapFactory.decodeStream(context.contentResolver.openInputStream(context.getAndroidSAFUri(this)), null, options)
} else {
BitmapFactory.decodeFile(this, options)
}
val width = options.outWidth
val height = options.outHeight
return if (width > 0 && height > 0) {
Point(options.outWidth, options.outHeight)
} else {
null
}
}
fun String.getPublicUri(context: Context) = context.getDocumentFile(this)?.uri ?: ""
fun String.substringTo(cnt: Int): String {
return if (isEmpty()) {
""
} else {
substring(0, Math.min(length, cnt))
}
}
fun String.highlightTextPart(textToHighlight: String, color: Int, highlightAll: Boolean = false, ignoreCharsBetweenDigits: Boolean = false): SpannableString {
val spannableString = SpannableString(this)
if (textToHighlight.isEmpty()) {
return spannableString
}
var startIndex = normalizeString().indexOf(textToHighlight, 0, true)
val indexes = ArrayList<Int>()
while (startIndex >= 0) {
if (startIndex != -1) {
indexes.add(startIndex)
}
startIndex = normalizeString().indexOf(textToHighlight, startIndex + textToHighlight.length, true)
if (!highlightAll) {
break
}
}
// handle cases when we search for 643, but in reality the string contains it like 6-43
if (ignoreCharsBetweenDigits && indexes.isEmpty()) {
try {
val regex = TextUtils.join("(\\D*)", textToHighlight.toCharArray().toTypedArray())
val pattern = Pattern.compile(regex)
val result = pattern.matcher(normalizeString())
if (result.find()) {
spannableString.setSpan(ForegroundColorSpan(color), result.start(), result.end(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
}
} catch (ignored: Exception) {
}
return spannableString
}
indexes.forEach {
val endIndex = Math.min(it + textToHighlight.length, length)
try {
spannableString.setSpan(ForegroundColorSpan(color), it, endIndex, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
} catch (ignored: IndexOutOfBoundsException) {
}
}
return spannableString
}
fun String.searchMatches(textToHighlight: String): ArrayList<Int> {
val indexes = arrayListOf<Int>()
var indexOf = indexOf(textToHighlight, 0, true)
var offset = 0
while (offset < length && indexOf != -1) {
indexOf = indexOf(textToHighlight, offset, true)
if (indexOf == -1) {
break
} else {
indexes.add(indexOf)
}
offset = indexOf + 1
}
return indexes
}
fun String.getFileSignature(lastModified: Long? = null) = ObjectKey(getFileKey(lastModified))
fun String.getFileKey(lastModified: Long? = null): String {
val file = File(this)
val modified = if (lastModified != null && lastModified > 0) {
lastModified
} else {
file.lastModified()
}
return "${file.absolutePath}$modified"
}
fun String.getAvailableStorageB(): Long {
return try {
val stat = StatFs(this)
val bytesAvailable = stat.blockSizeLong * stat.availableBlocksLong
bytesAvailable
} catch (e: Exception) {
-1L
}
}
// remove diacritics, for example č -> c
fun String.normalizeString() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(normalizeRegex, "")
// checks if string is a phone number
fun String.isPhoneNumber(): Boolean {
return this.matches("^[0-9+\\-\\)\\( *#]+\$".toRegex())
}
// if we are comparing phone numbers, compare just the last 9 digits
fun String.trimToComparableNumber(): String {
// don't trim if it's not a phone number
if (!this.isPhoneNumber()) {
return this
}
val normalizedNumber = this.normalizeString()
val startIndex = Math.max(0, normalizedNumber.length - 9)
return normalizedNumber.substring(startIndex)
}
// get the contact names first letter at showing the placeholder without image
fun String.getNameLetter() = normalizeString().toCharArray().getOrNull(0)?.toString()?.toUpperCase(Locale.getDefault()) ?: "A"
fun String.normalizePhoneNumber() = PhoneNumberUtils.normalizeNumber(this)
fun String.highlightTextFromNumbers(textToHighlight: String, primaryColor: Int): SpannableString {
val spannableString = SpannableString(this)
val digits = PhoneNumberUtils.convertKeypadLettersToDigits(this)
if (digits.contains(textToHighlight)) {
val startIndex = digits.indexOf(textToHighlight, 0, true)
val endIndex = Math.min(startIndex + textToHighlight.length, length)
try {
spannableString.setSpan(ForegroundColorSpan(primaryColor), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
} catch (ignored: IndexOutOfBoundsException) {
}
}
return spannableString
}
fun String.getDateTimeFromDateString(showYearsSince: Boolean, viewToUpdate: TextView? = null): DateTime {
val dateFormats = getDateFormats()
var date = DateTime()
for (format in dateFormats) {
try {
date = DateTime.parse(this, DateTimeFormat.forPattern(format))
val formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault())
var localPattern = (formatter as SimpleDateFormat).toLocalizedPattern()
val hasYear = format.contains("y")
if (!hasYear) {
localPattern = localPattern.replace("y", "").replace(",", "").trim()
date = date.withYear(DateTime().year)
}
var formattedString = date.toString(localPattern)
if (showYearsSince && hasYear) {
formattedString += " (${Years.yearsBetween(date, DateTime.now()).years})"
}
viewToUpdate?.text = formattedString
break
} catch (ignored: Exception) {
}
}
return date
}
fun String.getMimeType(): String {
val typesMap = HashMap<String, String>().apply {
put("323", "text/h323")
put("3g2", "video/3gpp2")
put("3gp", "video/3gpp")
put("3gp2", "video/3gpp2")
put("3gpp", "video/3gpp")
put("7z", "application/x-7z-compressed")
put("aa", "audio/audible")
put("aac", "audio/aac")
put("aaf", "application/octet-stream")
put("aax", "audio/vnd.audible.aax")
put("ac3", "audio/ac3")
put("aca", "application/octet-stream")
put("accda", "application/msaccess.addin")
put("accdb", "application/msaccess")
put("accdc", "application/msaccess.cab")
put("accde", "application/msaccess")
put("accdr", "application/msaccess.runtime")
put("accdt", "application/msaccess")
put("accdw", "application/msaccess.webapplication")
put("accft", "application/msaccess.ftemplate")
put("acx", "application/internet-property-stream")
put("addin", "text/xml")
put("ade", "application/msaccess")
put("adobebridge", "application/x-bridge-url")
put("adp", "application/msaccess")
put("adt", "audio/vnd.dlna.adts")
put("adts", "audio/aac")
put("afm", "application/octet-stream")
put("ai", "application/postscript")
put("aif", "audio/aiff")
put("aifc", "audio/aiff")
put("aiff", "audio/aiff")
put("air", "application/vnd.adobe.air-application-installer-package+zip")
put("amc", "application/mpeg")
put("anx", "application/annodex")
put("apk", "application/vnd.android.package-archive")
put("application", "application/x-ms-application")
put("art", "image/x-jg")
put("asa", "application/xml")
put("asax", "application/xml")
put("ascx", "application/xml")
put("asd", "application/octet-stream")
put("asf", "video/x-ms-asf")
put("ashx", "application/xml")
put("asi", "application/octet-stream")
put("asm", "text/plain")
put("asmx", "application/xml")
put("aspx", "application/xml")
put("asr", "video/x-ms-asf")
put("asx", "video/x-ms-asf")
put("atom", "application/atom+xml")
put("au", "audio/basic")
put("avi", "video/x-msvideo")
put("axa", "audio/annodex")
put("axs", "application/olescript")
put("axv", "video/annodex")
put("bas", "text/plain")
put("bcpio", "application/x-bcpio")
put("bin", "application/octet-stream")
put("bmp", "image/bmp")
put("c", "text/plain")
put("cab", "application/octet-stream")
put("caf", "audio/x-caf")
put("calx", "application/vnd.ms-office.calx")
put("cat", "application/vnd.ms-pki.seccat")
put("cc", "text/plain")
put("cd", "text/plain")
put("cdda", "audio/aiff")
put("cdf", "application/x-cdf")
put("cer", "application/x-x509-ca-cert")
put("cfg", "text/plain")
put("chm", "application/octet-stream")
put("class", "application/x-java-applet")
put("clp", "application/x-msclip")
put("cmd", "text/plain")
put("cmx", "image/x-cmx")
put("cnf", "text/plain")
put("cod", "image/cis-cod")
put("config", "application/xml")
put("contact", "text/x-ms-contact")
put("coverage", "application/xml")
put("cpio", "application/x-cpio")
put("cpp", "text/plain")
put("crd", "application/x-mscardfile")
put("crl", "application/pkix-crl")
put("crt", "application/x-x509-ca-cert")
put("cs", "text/plain")
put("csdproj", "text/plain")
put("csh", "application/x-csh")
put("csproj", "text/plain")
put("css", "text/css")
put("csv", "text/csv")
put("cur", "application/octet-stream")
put("cxx", "text/plain")
put("dat", "application/octet-stream")
put("datasource", "application/xml")
put("dbproj", "text/plain")
put("dcr", "application/x-director")
put("def", "text/plain")
put("deploy", "application/octet-stream")
put("der", "application/x-x509-ca-cert")
put("dgml", "application/xml")
put("dib", "image/bmp")
put("dif", "video/x-dv")
put("dir", "application/x-director")
put("disco", "text/xml")
put("divx", "video/divx")
put("dll", "application/x-msdownload")
put("dll.config", "text/xml")
put("dlm", "text/dlm")
put("dng", "image/x-adobe-dng")
put("doc", "application/msword")
put("docm", "application/vnd.ms-word.document.macroEnabled.12")
put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
put("dot", "application/msword")
put("dotm", "application/vnd.ms-word.template.macroEnabled.12")
put("dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template")
put("dsp", "application/octet-stream")
put("dsw", "text/plain")
put("dtd", "text/xml")
put("dtsconfig", "text/xml")
put("dv", "video/x-dv")
put("dvi", "application/x-dvi")
put("dwf", "drawing/x-dwf")
put("dwp", "application/octet-stream")
put("dxr", "application/x-director")
put("eml", "message/rfc822")
put("emz", "application/octet-stream")
put("eot", "application/vnd.ms-fontobject")
put("eps", "application/postscript")
put("etl", "application/etl")
put("etx", "text/x-setext")
put("evy", "application/envoy")
put("exe", "application/octet-stream")
put("exe.config", "text/xml")
put("fdf", "application/vnd.fdf")
put("fif", "application/fractals")
put("filters", "application/xml")
put("fla", "application/octet-stream")
put("flac", "audio/flac")
put("flr", "x-world/x-vrml")
put("flv", "video/x-flv")
put("fsscript", "application/fsharp-script")
put("fsx", "application/fsharp-script")
put("generictest", "application/xml")
put("gif", "image/gif")
put("group", "text/x-ms-group")
put("gsm", "audio/x-gsm")
put("gtar", "application/x-gtar")
put("gz", "application/x-gzip")
put("h", "text/plain")
put("hdf", "application/x-hdf")
put("hdml", "text/x-hdml")
put("hhc", "application/x-oleobject")
put("hhk", "application/octet-stream")
put("hhp", "application/octet-stream")
put("hlp", "application/winhlp")
put("hpp", "text/plain")
put("hqx", "application/mac-binhex40")
put("hta", "application/hta")
put("htc", "text/x-component")
put("htm", "text/html")
put("html", "text/html")
put("htt", "text/webviewhtml")
put("hxa", "application/xml")
put("hxc", "application/xml")
put("hxd", "application/octet-stream")
put("hxe", "application/xml")
put("hxf", "application/xml")
put("hxh", "application/octet-stream")
put("hxi", "application/octet-stream")
put("hxk", "application/xml")
put("hxq", "application/octet-stream")
put("hxr", "application/octet-stream")
put("hxs", "application/octet-stream")
put("hxt", "text/html")
put("hxv", "application/xml")
put("hxw", "application/octet-stream")
put("hxx", "text/plain")
put("i", "text/plain")
put("ico", "image/x-icon")
put("ics", "text/calendar")
put("idl", "text/plain")
put("ief", "image/ief")
put("iii", "application/x-iphone")
put("inc", "text/plain")
put("inf", "application/octet-stream")
put("ini", "text/plain")
put("inl", "text/plain")
put("ins", "application/x-internet-signup")
put("ipa", "application/x-itunes-ipa")
put("ipg", "application/x-itunes-ipg")
put("ipproj", "text/plain")
put("ipsw", "application/x-itunes-ipsw")
put("iqy", "text/x-ms-iqy")
put("isp", "application/x-internet-signup")
put("ite", "application/x-itunes-ite")
put("itlp", "application/x-itunes-itlp")
put("itms", "application/x-itunes-itms")
put("itpc", "application/x-itunes-itpc")
put("ivf", "video/x-ivf")
put("jar", "application/java-archive")
put("java", "application/octet-stream")
put("jck", "application/liquidmotion")
put("jcz", "application/liquidmotion")
put("jfif", "image/pjpeg")
put("jnlp", "application/x-java-jnlp-file")
put("jpb", "application/octet-stream")
put("jpe", "image/jpeg")
put("jpeg", "image/jpeg")
put("jpg", "image/jpeg")
put("js", "application/javascript")
put("json", "application/json")
put("jsx", "text/jscript")
put("jsxbin", "text/plain")
put("latex", "application/x-latex")
put("library-ms", "application/windows-library+xml")
put("lit", "application/x-ms-reader")
put("loadtest", "application/xml")
put("lpk", "application/octet-stream")
put("lsf", "video/x-la-asf")
put("lst", "text/plain")
put("lsx", "video/x-la-asf")
put("lzh", "application/octet-stream")
put("m13", "application/x-msmediaview")
put("m14", "application/x-msmediaview")
put("m1v", "video/mpeg")
put("m2t", "video/vnd.dlna.mpeg-tts")
put("m2ts", "video/vnd.dlna.mpeg-tts")
put("m2v", "video/mpeg")
put("m3u", "audio/x-mpegurl")
put("m3u8", "audio/x-mpegurl")
put("m4a", "audio/m4a")
put("m4b", "audio/m4b")
put("m4p", "audio/m4p")
put("m4r", "audio/x-m4r")
put("m4v", "video/x-m4v")
put("mac", "image/x-macpaint")
put("mak", "text/plain")
put("man", "application/x-troff-man")
put("manifest", "application/x-ms-manifest")
put("map", "text/plain")
put("master", "application/xml")
put("mda", "application/msaccess")
put("mdb", "application/x-msaccess")
put("mde", "application/msaccess")
put("mdp", "application/octet-stream")
put("me", "application/x-troff-me")
put("mfp", "application/x-shockwave-flash")
put("mht", "message/rfc822")
put("mhtml", "message/rfc822")
put("mid", "audio/mid")
put("midi", "audio/mid")
put("mix", "application/octet-stream")
put("mk", "text/plain")
put("mkv", "video/x-matroska")
put("mmf", "application/x-smaf")
put("mno", "text/xml")
put("mny", "application/x-msmoney")
put("mod", "video/mpeg")
put("mov", "video/quicktime")
put("movie", "video/x-sgi-movie")
put("mp2", "video/mpeg")
put("mp2v", "video/mpeg")
put("mp3", "audio/mpeg")
put("mp4", "video/mp4")
put("mp4v", "video/mp4")
put("mpa", "video/mpeg")
put("mpe", "video/mpeg")
put("mpeg", "video/mpeg")
put("mpf", "application/vnd.ms-mediapackage")
put("mpg", "video/mpeg")
put("mpp", "application/vnd.ms-project")
put("mpv2", "video/mpeg")
put("mqv", "video/quicktime")
put("ms", "application/x-troff-ms")
put("msi", "application/octet-stream")
put("mso", "application/octet-stream")
put("mts", "video/vnd.dlna.mpeg-tts")
put("mtx", "application/xml")
put("mvb", "application/x-msmediaview")
put("mvc", "application/x-miva-compiled")
put("mxp", "application/x-mmxp")
put("nc", "application/x-netcdf")
put("nsc", "video/x-ms-asf")
put("nws", "message/rfc822")
put("ocx", "application/octet-stream")
put("oda", "application/oda")
put("odb", "application/vnd.oasis.opendocument.database")
put("odc", "application/vnd.oasis.opendocument.chart")
put("odf", "application/vnd.oasis.opendocument.formula")
put("odg", "application/vnd.oasis.opendocument.graphics")
put("odh", "text/plain")
put("odi", "application/vnd.oasis.opendocument.image")
put("odl", "text/plain")
put("odm", "application/vnd.oasis.opendocument.text-master")
put("odp", "application/vnd.oasis.opendocument.presentation")
put("ods", "application/vnd.oasis.opendocument.spreadsheet")
put("odt", "application/vnd.oasis.opendocument.text")
put("oga", "audio/ogg")
put("ogg", "audio/ogg")
put("ogv", "video/ogg")
put("ogx", "application/ogg")
put("one", "application/onenote")
put("onea", "application/onenote")
put("onepkg", "application/onenote")
put("onetmp", "application/onenote")
put("onetoc", "application/onenote")
put("onetoc2", "application/onenote")
put("opus", "audio/ogg")
put("orderedtest", "application/xml")
put("osdx", "application/opensearchdescription+xml")
put("otf", "application/font-sfnt")
put("otg", "application/vnd.oasis.opendocument.graphics-template")
put("oth", "application/vnd.oasis.opendocument.text-web")
put("otp", "application/vnd.oasis.opendocument.presentation-template")
put("ots", "application/vnd.oasis.opendocument.spreadsheet-template")
put("ott", "application/vnd.oasis.opendocument.text-template")
put("oxt", "application/vnd.openofficeorg.extension")
put("p10", "application/pkcs10")
put("p12", "application/x-pkcs12")
put("p7b", "application/x-pkcs7-certificates")
put("p7c", "application/pkcs7-mime")
put("p7m", "application/pkcs7-mime")
put("p7r", "application/x-pkcs7-certreqresp")
put("p7s", "application/pkcs7-signature")
put("pbm", "image/x-portable-bitmap")
put("pcast", "application/x-podcast")
put("pct", "image/pict")
put("pcx", "application/octet-stream")
put("pcz", "application/octet-stream")
put("pdf", "application/pdf")
put("pfb", "application/octet-stream")
put("pfm", "application/octet-stream")
put("pfx", "application/x-pkcs12")
put("pgm", "image/x-portable-graymap")
put("php", "text/plain")
put("pic", "image/pict")
put("pict", "image/pict")
put("pkgdef", "text/plain")
put("pkgundef", "text/plain")
put("pko", "application/vnd.ms-pki.pko")
put("pls", "audio/scpls")
put("pma", "application/x-perfmon")
put("pmc", "application/x-perfmon")
put("pml", "application/x-perfmon")
put("pmr", "application/x-perfmon")
put("pmw", "application/x-perfmon")
put("png", "image/png")
put("pnm", "image/x-portable-anymap")
put("pnt", "image/x-macpaint")
put("pntg", "image/x-macpaint")
put("pnz", "image/png")
put("pot", "application/vnd.ms-powerpoint")
put("potm", "application/vnd.ms-powerpoint.template.macroEnabled.12")
put("potx", "application/vnd.openxmlformats-officedocument.presentationml.template")
put("ppa", "application/vnd.ms-powerpoint")
put("ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12")
put("ppm", "image/x-portable-pixmap")
put("pps", "application/vnd.ms-powerpoint")
put("ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12")
put("ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow")
put("ppt", "application/vnd.ms-powerpoint")
put("pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12")
put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation")
put("prf", "application/pics-rules")
put("prm", "application/octet-stream")
put("prx", "application/octet-stream")
put("ps", "application/postscript")
put("psc1", "application/PowerShell")
put("psd", "application/octet-stream")
put("psess", "application/xml")
put("psm", "application/octet-stream")
put("psp", "application/octet-stream")
put("pub", "application/x-mspublisher")
put("pwz", "application/vnd.ms-powerpoint")
put("py", "text/plain")
put("qht", "text/x-html-insertion")
put("qhtm", "text/x-html-insertion")
put("qt", "video/quicktime")
put("qti", "image/x-quicktime")
put("qtif", "image/x-quicktime")
put("qtl", "application/x-quicktimeplayer")
put("qxd", "application/octet-stream")
put("ra", "audio/x-pn-realaudio")
put("ram", "audio/x-pn-realaudio")
put("rar", "application/x-rar-compressed")
put("ras", "image/x-cmu-raster")
put("rat", "application/rat-file")
put("rb", "text/plain")
put("rc", "text/plain")
put("rc2", "text/plain")
put("rct", "text/plain")
put("rdlc", "application/xml")
put("reg", "text/plain")
put("resx", "application/xml")
put("rf", "image/vnd.rn-realflash")
put("rgb", "image/x-rgb")
put("rgs", "text/plain")
put("rm", "application/vnd.rn-realmedia")
put("rmi", "audio/mid")
put("rmp", "application/vnd.rn-rn_music_package")
put("roff", "application/x-troff")
put("rpm", "audio/x-pn-realaudio-plugin")
put("rqy", "text/x-ms-rqy")
put("rtf", "application/rtf")
put("rtx", "text/richtext")
put("ruleset", "application/xml")
put("s", "text/plain")
put("safariextz", "application/x-safari-safariextz")
put("scd", "application/x-msschedule")
put("scr", "text/plain")
put("sct", "text/scriptlet")
put("sd2", "audio/x-sd2")
put("sdp", "application/sdp")
put("sea", "application/octet-stream")
put("searchConnector-ms", "application/windows-search-connector+xml")
put("setpay", "application/set-payment-initiation")
put("setreg", "application/set-registration-initiation")
put("settings", "application/xml")
put("sgimb", "application/x-sgimb")
put("sgml", "text/sgml")
put("sh", "application/x-sh")
put("shar", "application/x-shar")
put("shtml", "text/html")
put("sit", "application/x-stuffit")
put("sitemap", "application/xml")
put("skin", "application/xml")
put("sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12")
put("sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide")
put("slk", "application/vnd.ms-excel")
put("sln", "text/plain")
put("slupkg-ms", "application/x-ms-license")
put("smd", "audio/x-smd")
put("smi", "application/octet-stream")
put("smx", "audio/x-smd")
put("smz", "audio/x-smd")
put("snd", "audio/basic")
put("snippet", "application/xml")
put("snp", "application/octet-stream")
put("sol", "text/plain")
put("sor", "text/plain")
put("spc", "application/x-pkcs7-certificates")
put("spl", "application/futuresplash")
put("spx", "audio/ogg")
put("src", "application/x-wais-source")
put("srf", "text/plain")
put("ssisdeploymentmanifest", "text/xml")
put("ssm", "application/streamingmedia")
put("sst", "application/vnd.ms-pki.certstore")
put("stl", "application/vnd.ms-pki.stl")
put("sv4cpio", "application/x-sv4cpio")
put("sv4crc", "application/x-sv4crc")
put("svc", "application/xml")
put("svg", "image/svg+xml")
put("swf", "application/x-shockwave-flash")
put("t", "application/x-troff")
put("tar", "application/x-tar")
put("tcl", "application/x-tcl")
put("testrunconfig", "application/xml")
put("testsettings", "application/xml")
put("tex", "application/x-tex")
put("texi", "application/x-texinfo")
put("texinfo", "application/x-texinfo")
put("tgz", "application/x-compressed")
put("thmx", "application/vnd.ms-officetheme")
put("thn", "application/octet-stream")
put("tif", "image/tiff")
put("tiff", "image/tiff")
put("tlh", "text/plain")
put("tli", "text/plain")
put("toc", "application/octet-stream")
put("tr", "application/x-troff")
put("trm", "application/x-msterminal")
put("trx", "application/xml")
put("ts", "video/vnd.dlna.mpeg-tts")
put("tsv", "text/tab-separated-values")
put("ttf", "application/font-sfnt")
put("tts", "video/vnd.dlna.mpeg-tts")
put("txt", "text/plain")
put("u32", "application/octet-stream")
put("uls", "text/iuls")
put("user", "text/plain")
put("ustar", "application/x-ustar")
put("vb", "text/plain")
put("vbdproj", "text/plain")
put("vbk", "video/mpeg")
put("vbproj", "text/plain")
put("vbs", "text/vbscript")
put("vcf", "text/x-vcard")
put("vcproj", "application/xml")
put("vcs", "text/calendar")
put("vcxproj", "application/xml")
put("vddproj", "text/plain")
put("vdp", "text/plain")
put("vdproj", "text/plain")
put("vdx", "application/vnd.ms-visio.viewer")
put("vml", "text/xml")
put("vscontent", "application/xml")
put("vsct", "text/xml")
put("vsd", "application/vnd.visio")
put("vsi", "application/ms-vsi")
put("vsix", "application/vsix")
put("vsixlangpack", "text/xml")
put("vsixmanifest", "text/xml")
put("vsmdi", "application/xml")
put("vspscc", "text/plain")
put("vss", "application/vnd.visio")
put("vsscc", "text/plain")
put("vssettings", "text/xml")
put("vssscc", "text/plain")
put("vst", "application/vnd.visio")
put("vstemplate", "text/xml")
put("vsto", "application/x-ms-vsto")
put("vsw", "application/vnd.visio")
put("vsx", "application/vnd.visio")
put("vtx", "application/vnd.visio")
put("wav", "audio/wav")
put("wave", "audio/wav")
put("wax", "audio/x-ms-wax")
put("wbk", "application/msword")
put("wbmp", "image/vnd.wap.wbmp")
put("wcm", "application/vnd.ms-works")
put("wdb", "application/vnd.ms-works")
put("wdp", "image/vnd.ms-photo")
put("webarchive", "application/x-safari-webarchive")
put("webm", "video/webm")
put("webp", "image/webp")
put("webtest", "application/xml")
put("wiq", "application/xml")
put("wiz", "application/msword")
put("wks", "application/vnd.ms-works")
put("wlmp", "application/wlmoviemaker")
put("wlpginstall", "application/x-wlpg-detect")
put("wlpginstall3", "application/x-wlpg3-detect")
put("wm", "video/x-ms-wm")
put("wma", "audio/x-ms-wma")
put("wmd", "application/x-ms-wmd")
put("wmf", "application/x-msmetafile")
put("wml", "text/vnd.wap.wml")
put("wmlc", "application/vnd.wap.wmlc")
put("wmls", "text/vnd.wap.wmlscript")
put("wmlsc", "application/vnd.wap.wmlscriptc")
put("wmp", "video/x-ms-wmp")
put("wmv", "video/x-ms-wmv")
put("wmx", "video/x-ms-wmx")
put("wmz", "application/x-ms-wmz")
put("woff", "application/font-woff")
put("wpl", "application/vnd.ms-wpl")
put("wps", "application/vnd.ms-works")
put("wri", "application/x-mswrite")
put("wrl", "x-world/x-vrml")
put("wrz", "x-world/x-vrml")
put("wsc", "text/scriptlet")
put("wsdl", "text/xml")
put("wvx", "video/x-ms-wvx")
put("x", "application/directx")
put("xaf", "x-world/x-vrml")
put("xaml", "application/xaml+xml")
put("xap", "application/x-silverlight-app")
put("xbap", "application/x-ms-xbap")
put("xbm", "image/x-xbitmap")
put("xdr", "text/plain")
put("xht", "application/xhtml+xml")
put("xhtml", "application/xhtml+xml")
put("xla", "application/vnd.ms-excel")
put("xlam", "application/vnd.ms-excel.addin.macroEnabled.12")
put("xlc", "application/vnd.ms-excel")
put("xld", "application/vnd.ms-excel")
put("xlk", "application/vnd.ms-excel")
put("xll", "application/vnd.ms-excel")
put("xlm", "application/vnd.ms-excel")
put("xls", "application/vnd.ms-excel")
put("xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12")
put("xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12")
put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
put("xlt", "application/vnd.ms-excel")
put("xltm", "application/vnd.ms-excel.template.macroEnabled.12")
put("xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template")
put("xlw", "application/vnd.ms-excel")
put("xml", "text/xml")
put("xmta", "application/xml")
put("xof", "x-world/x-vrml")
put("xoml", "text/plain")
put("xpm", "image/x-xpixmap")
put("xps", "application/vnd.ms-xpsdocument")
put("xrm-ms", "text/xml")
put("xsc", "application/xml")
put("xsd", "text/xml")
put("xsf", "text/xml")
put("xsl", "text/xml")
put("xslt", "text/xml")
put("xsn", "application/octet-stream")
put("xss", "application/xml")
put("xspf", "application/xspf+xml")
put("xtp", "application/octet-stream")
put("xwd", "image/x-xwindowdump")
put("z", "application/x-compress")
put("zip", "application/zip")
}
return typesMap[getFilenameExtension().toLowerCase()] ?: ""
}
fun String.isBlockedNumberPattern() = contains("*")

View File

@ -0,0 +1,20 @@
package com.simplemobiletools.commons.extensions
import com.google.android.material.tabs.TabLayout
fun TabLayout.onTabSelectionChanged(
tabUnselectedAction: ((inactiveTab: TabLayout.Tab) -> Unit)? = null,
tabSelectedAction: ((activeTab: TabLayout.Tab) -> Unit)? = null
) = setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
tabSelectedAction?.invoke(tab)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
tabUnselectedAction?.invoke(tab)
}
override fun onTabReselected(tab: TabLayout.Tab) {
tabSelectedAction?.invoke(tab)
}
})

View File

@ -0,0 +1,36 @@
package com.simplemobiletools.commons.extensions
import android.graphics.Paint
import android.text.SpannableString
import android.text.TextPaint
import android.text.style.URLSpan
import android.widget.TextView
import androidx.annotation.StringRes
val TextView.value: String get() = text.toString().trim()
fun TextView.underlineText() {
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
}
fun TextView.removeUnderlines() {
val spannable = SpannableString(text)
for (u in spannable.getSpans(0, spannable.length, URLSpan::class.java)) {
spannable.setSpan(object : URLSpan(u.url) {
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.isUnderlineText = false
}
}, spannable.getSpanStart(u), spannable.getSpanEnd(u), 0)
}
text = spannable
}
fun TextView.setTextOrBeGone(@StringRes textRes: Int?) {
if (textRes != null) {
beVisible()
this.text = context.getString(textRes)
} else {
beGone()
}
}

View File

@ -0,0 +1,51 @@
package com.simplemobiletools.commons.extensions
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewTreeObserver
import com.simplemobiletools.commons.helpers.SHORT_ANIMATION_DURATION
fun View.beInvisibleIf(beInvisible: Boolean) = if (beInvisible) beInvisible() else beVisible()
fun View.beVisibleIf(beVisible: Boolean) = if (beVisible) beVisible() else beGone()
fun View.beGoneIf(beGone: Boolean) = beVisibleIf(!beGone)
fun View.beInvisible() {
visibility = View.INVISIBLE
}
fun View.beVisible() {
visibility = View.VISIBLE
}
fun View.beGone() {
visibility = View.GONE
}
fun View.onGlobalLayout(callback: () -> Unit) {
viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (viewTreeObserver != null) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
callback()
}
}
})
}
fun View.isVisible() = visibility == View.VISIBLE
fun View.isInvisible() = visibility == View.INVISIBLE
fun View.isGone() = visibility == View.GONE
fun View.performHapticFeedback() = performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING)
fun View.fadeIn() {
animate().alpha(1f).setDuration(SHORT_ANIMATION_DURATION).withStartAction { beVisible() }.start()
}
fun View.fadeOut() {
animate().alpha(0f).setDuration(SHORT_ANIMATION_DURATION).withEndAction { beGone() }.start()
}

View File

@ -0,0 +1,14 @@
package com.simplemobiletools.commons.extensions
import androidx.viewpager.widget.ViewPager
fun ViewPager.onPageChangeListener(pageChangedAction: (newPosition: Int) -> Unit) =
addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pageChangedAction(position)
}
})

View File

@ -0,0 +1,50 @@
package com.simplemobiletools.commons.fragments
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_bottom_sheet.view.*
abstract class BaseBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.dialog_bottom_sheet, container, false)
val context = requireContext()
val config = context.baseConfig
if (requireContext().isBlackAndWhiteTheme()) {
view.background = ResourcesCompat.getDrawable(context.resources, R.drawable.bottom_sheet_bg_black, context.theme)
} else if (!config.isUsingSystemTheme) {
view.background = ResourcesCompat.getDrawable(context.resources, R.drawable.bottom_sheet_bg, context.theme).apply {
(this as LayerDrawable).findDrawableByLayerId(R.id.bottom_sheet_background).applyColorFilter(context.getProperBackgroundColor())
}
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val title = arguments?.getInt(BOTTOM_SHEET_TITLE).takeIf { it != 0 }
view.apply {
bottom_sheet_title.setTextColor(context.getProperTextColor())
bottom_sheet_title.setTextOrBeGone(title)
setupContentView(bottom_sheet_content_holder)
}
}
abstract fun setupContentView(parent: ViewGroup)
companion object {
const val BOTTOM_SHEET_TITLE = "title_string"
}
}

View File

@ -0,0 +1,75 @@
package com.simplemobiletools.commons.helpers
// taken from https://gist.github.com/MichaelRocks/1b94bb44c7804e999dbf31dac86955ec
// make IMG_5.jpg come before IMG_10.jpg
class AlphanumericComparator {
fun compare(string1: String, string2: String): Int {
var thisMarker = 0
var thatMarker = 0
val s1Length = string1.length
val s2Length = string2.length
while (thisMarker < s1Length && thatMarker < s2Length) {
val thisChunk = getChunk(string1, s1Length, thisMarker)
thisMarker += thisChunk.length
val thatChunk = getChunk(string2, s2Length, thatMarker)
thatMarker += thatChunk.length
// If both chunks contain numeric characters, sort them numerically.
var result: Int
if (isDigit(thisChunk[0]) && isDigit(thatChunk[0])) {
// Simple chunk comparison by length.
val thisChunkLength = thisChunk.length
result = thisChunkLength - thatChunk.length
// If equal, the first different number counts.
if (result == 0) {
for (i in 0 until thisChunkLength) {
result = thisChunk[i] - thatChunk[i]
if (result != 0) {
return result
}
}
}
} else {
result = thisChunk.compareTo(thatChunk)
}
if (result != 0) {
return result
}
}
return s1Length - s2Length
}
private fun getChunk(string: String, length: Int, marker: Int): String {
var current = marker
val chunk = StringBuilder()
var c = string[current]
chunk.append(c)
current++
if (isDigit(c)) {
while (current < length) {
c = string[current]
if (!isDigit(c)) {
break
}
chunk.append(c)
current++
}
} else {
while (current < length) {
c = string[current]
if (isDigit(c)) {
break
}
chunk.append(c)
current++
}
}
return chunk.toString()
}
private fun isDigit(ch: Char) = ch in '0'..'9'
}

View File

@ -0,0 +1,544 @@
package com.simplemobiletools.commons.helpers
import android.content.Context
import android.text.format.DateFormat
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.getInternalStoragePath
import com.simplemobiletools.commons.extensions.getSDCardPath
import com.simplemobiletools.commons.extensions.getSharedPrefs
import java.text.SimpleDateFormat
import java.util.*
open class BaseConfig(val context: Context) {
protected val prefs = context.getSharedPrefs()
companion object {
fun newInstance(context: Context) = BaseConfig(context)
}
var appRunCount: Int
get() = prefs.getInt(APP_RUN_COUNT, 0)
set(appRunCount) = prefs.edit().putInt(APP_RUN_COUNT, appRunCount).apply()
var lastVersion: Int
get() = prefs.getInt(LAST_VERSION, 0)
set(lastVersion) = prefs.edit().putInt(LAST_VERSION, lastVersion).apply()
var primaryAndroidDataTreeUri: String
get() = prefs.getString(PRIMARY_ANDROID_DATA_TREE_URI, "")!!
set(uri) = prefs.edit().putString(PRIMARY_ANDROID_DATA_TREE_URI, uri).apply()
var sdAndroidDataTreeUri: String
get() = prefs.getString(SD_ANDROID_DATA_TREE_URI, "")!!
set(uri) = prefs.edit().putString(SD_ANDROID_DATA_TREE_URI, uri).apply()
var otgAndroidDataTreeUri: String
get() = prefs.getString(OTG_ANDROID_DATA_TREE_URI, "")!!
set(uri) = prefs.edit().putString(OTG_ANDROID_DATA_TREE_URI, uri).apply()
var primaryAndroidObbTreeUri: String
get() = prefs.getString(PRIMARY_ANDROID_OBB_TREE_URI, "")!!
set(uri) = prefs.edit().putString(PRIMARY_ANDROID_OBB_TREE_URI, uri).apply()
var sdAndroidObbTreeUri: String
get() = prefs.getString(SD_ANDROID_OBB_TREE_URI, "")!!
set(uri) = prefs.edit().putString(SD_ANDROID_OBB_TREE_URI, uri).apply()
var otgAndroidObbTreeUri: String
get() = prefs.getString(OTG_ANDROID_OBB_TREE_URI, "")!!
set(uri) = prefs.edit().putString(OTG_ANDROID_OBB_TREE_URI, uri).apply()
var sdTreeUri: String
get() = prefs.getString(SD_TREE_URI, "")!!
set(uri) = prefs.edit().putString(SD_TREE_URI, uri).apply()
var OTGTreeUri: String
get() = prefs.getString(OTG_TREE_URI, "")!!
set(OTGTreeUri) = prefs.edit().putString(OTG_TREE_URI, OTGTreeUri).apply()
var OTGPartition: String
get() = prefs.getString(OTG_PARTITION, "")!!
set(OTGPartition) = prefs.edit().putString(OTG_PARTITION, OTGPartition).apply()
var OTGPath: String
get() = prefs.getString(OTG_REAL_PATH, "")!!
set(OTGPath) = prefs.edit().putString(OTG_REAL_PATH, OTGPath).apply()
var sdCardPath: String
get() = prefs.getString(SD_CARD_PATH, getDefaultSDCardPath())!!
set(sdCardPath) = prefs.edit().putString(SD_CARD_PATH, sdCardPath).apply()
private fun getDefaultSDCardPath() = if (prefs.contains(SD_CARD_PATH)) "" else context.getSDCardPath()
var internalStoragePath: String
get() = prefs.getString(INTERNAL_STORAGE_PATH, getDefaultInternalPath())!!
set(internalStoragePath) = prefs.edit().putString(INTERNAL_STORAGE_PATH, internalStoragePath).apply()
private fun getDefaultInternalPath() = if (prefs.contains(INTERNAL_STORAGE_PATH)) "" else context.getInternalStoragePath()
var textColor: Int
get() = prefs.getInt(TEXT_COLOR, context.resources.getColor(R.color.default_text_color))
set(textColor) = prefs.edit().putInt(TEXT_COLOR, textColor).apply()
var backgroundColor: Int
get() = prefs.getInt(BACKGROUND_COLOR, context.resources.getColor(R.color.default_background_color))
set(backgroundColor) = prefs.edit().putInt(BACKGROUND_COLOR, backgroundColor).apply()
var primaryColor: Int
get() = prefs.getInt(PRIMARY_COLOR, context.resources.getColor(R.color.default_primary_color))
set(primaryColor) = prefs.edit().putInt(PRIMARY_COLOR, primaryColor).apply()
var accentColor: Int
get() = prefs.getInt(ACCENT_COLOR, context.resources.getColor(R.color.default_accent_color))
set(accentColor) = prefs.edit().putInt(ACCENT_COLOR, accentColor).apply()
var lastHandledShortcutColor: Int
get() = prefs.getInt(LAST_HANDLED_SHORTCUT_COLOR, 1)
set(lastHandledShortcutColor) = prefs.edit().putInt(LAST_HANDLED_SHORTCUT_COLOR, lastHandledShortcutColor).apply()
var appIconColor: Int
get() = prefs.getInt(APP_ICON_COLOR, context.resources.getColor(R.color.default_app_icon_color))
set(appIconColor) {
isUsingModifiedAppIcon = appIconColor != context.resources.getColor(R.color.color_primary)
prefs.edit().putInt(APP_ICON_COLOR, appIconColor).apply()
}
var lastIconColor: Int
get() = prefs.getInt(LAST_ICON_COLOR, context.resources.getColor(R.color.color_primary))
set(lastIconColor) = prefs.edit().putInt(LAST_ICON_COLOR, lastIconColor).apply()
var customTextColor: Int
get() = prefs.getInt(CUSTOM_TEXT_COLOR, textColor)
set(customTextColor) = prefs.edit().putInt(CUSTOM_TEXT_COLOR, customTextColor).apply()
var customBackgroundColor: Int
get() = prefs.getInt(CUSTOM_BACKGROUND_COLOR, backgroundColor)
set(customBackgroundColor) = prefs.edit().putInt(CUSTOM_BACKGROUND_COLOR, customBackgroundColor).apply()
var customPrimaryColor: Int
get() = prefs.getInt(CUSTOM_PRIMARY_COLOR, primaryColor)
set(customPrimaryColor) = prefs.edit().putInt(CUSTOM_PRIMARY_COLOR, customPrimaryColor).apply()
var customAccentColor: Int
get() = prefs.getInt(CUSTOM_ACCENT_COLOR, accentColor)
set(customAccentColor) = prefs.edit().putInt(CUSTOM_ACCENT_COLOR, customAccentColor).apply()
var customAppIconColor: Int
get() = prefs.getInt(CUSTOM_APP_ICON_COLOR, appIconColor)
set(customAppIconColor) = prefs.edit().putInt(CUSTOM_APP_ICON_COLOR, customAppIconColor).apply()
var widgetBgColor: Int
get() = prefs.getInt(WIDGET_BG_COLOR, context.resources.getColor(R.color.default_widget_bg_color))
set(widgetBgColor) = prefs.edit().putInt(WIDGET_BG_COLOR, widgetBgColor).apply()
var widgetTextColor: Int
get() = prefs.getInt(WIDGET_TEXT_COLOR, context.resources.getColor(R.color.default_widget_text_color))
set(widgetTextColor) = prefs.edit().putInt(WIDGET_TEXT_COLOR, widgetTextColor).apply()
// hidden folder visibility protection
var isHiddenPasswordProtectionOn: Boolean
get() = prefs.getBoolean(PASSWORD_PROTECTION, false)
set(isHiddenPasswordProtectionOn) = prefs.edit().putBoolean(PASSWORD_PROTECTION, isHiddenPasswordProtectionOn).apply()
var hiddenPasswordHash: String
get() = prefs.getString(PASSWORD_HASH, "")!!
set(hiddenPasswordHash) = prefs.edit().putString(PASSWORD_HASH, hiddenPasswordHash).apply()
var hiddenProtectionType: Int
get() = prefs.getInt(PROTECTION_TYPE, PROTECTION_PATTERN)
set(hiddenProtectionType) = prefs.edit().putInt(PROTECTION_TYPE, hiddenProtectionType).apply()
// whole app launch protection
var isAppPasswordProtectionOn: Boolean
get() = prefs.getBoolean(APP_PASSWORD_PROTECTION, false)
set(isAppPasswordProtectionOn) = prefs.edit().putBoolean(APP_PASSWORD_PROTECTION, isAppPasswordProtectionOn).apply()
var appPasswordHash: String
get() = prefs.getString(APP_PASSWORD_HASH, "")!!
set(appPasswordHash) = prefs.edit().putString(APP_PASSWORD_HASH, appPasswordHash).apply()
var appProtectionType: Int
get() = prefs.getInt(APP_PROTECTION_TYPE, PROTECTION_PATTERN)
set(appProtectionType) = prefs.edit().putInt(APP_PROTECTION_TYPE, appProtectionType).apply()
// file delete and move protection
var isDeletePasswordProtectionOn: Boolean
get() = prefs.getBoolean(DELETE_PASSWORD_PROTECTION, false)
set(isDeletePasswordProtectionOn) = prefs.edit().putBoolean(DELETE_PASSWORD_PROTECTION, isDeletePasswordProtectionOn).apply()
var deletePasswordHash: String
get() = prefs.getString(DELETE_PASSWORD_HASH, "")!!
set(deletePasswordHash) = prefs.edit().putString(DELETE_PASSWORD_HASH, deletePasswordHash).apply()
var deleteProtectionType: Int
get() = prefs.getInt(DELETE_PROTECTION_TYPE, PROTECTION_PATTERN)
set(deleteProtectionType) = prefs.edit().putInt(DELETE_PROTECTION_TYPE, deleteProtectionType).apply()
// folder locking
fun addFolderProtection(path: String, hash: String, type: Int) {
prefs.edit()
.putString("$PROTECTED_FOLDER_HASH$path", hash)
.putInt("$PROTECTED_FOLDER_TYPE$path", type)
.apply()
}
fun removeFolderProtection(path: String) {
prefs.edit()
.remove("$PROTECTED_FOLDER_HASH$path")
.remove("$PROTECTED_FOLDER_TYPE$path")
.apply()
}
fun isFolderProtected(path: String) = getFolderProtectionType(path) != PROTECTION_NONE
fun getFolderProtectionHash(path: String) = prefs.getString("$PROTECTED_FOLDER_HASH$path", "") ?: ""
fun getFolderProtectionType(path: String) = prefs.getInt("$PROTECTED_FOLDER_TYPE$path", PROTECTION_NONE)
var keepLastModified: Boolean
get() = prefs.getBoolean(KEEP_LAST_MODIFIED, true)
set(keepLastModified) = prefs.edit().putBoolean(KEEP_LAST_MODIFIED, keepLastModified).apply()
var useEnglish: Boolean
get() = prefs.getBoolean(USE_ENGLISH, false)
set(useEnglish) {
wasUseEnglishToggled = true
prefs.edit().putBoolean(USE_ENGLISH, useEnglish).commit()
}
var wasUseEnglishToggled: Boolean
get() = prefs.getBoolean(WAS_USE_ENGLISH_TOGGLED, false)
set(wasUseEnglishToggled) = prefs.edit().putBoolean(WAS_USE_ENGLISH_TOGGLED, wasUseEnglishToggled).apply()
var wasSharedThemeEverActivated: Boolean
get() = prefs.getBoolean(WAS_SHARED_THEME_EVER_ACTIVATED, false)
set(wasSharedThemeEverActivated) = prefs.edit().putBoolean(WAS_SHARED_THEME_EVER_ACTIVATED, wasSharedThemeEverActivated).apply()
var isUsingSharedTheme: Boolean
get() = prefs.getBoolean(IS_USING_SHARED_THEME, false)
set(isUsingSharedTheme) = prefs.edit().putBoolean(IS_USING_SHARED_THEME, isUsingSharedTheme).apply()
// used by Simple Thank You, stop using shared Shared Theme if it has been changed in it
var shouldUseSharedTheme: Boolean
get() = prefs.getBoolean(SHOULD_USE_SHARED_THEME, false)
set(shouldUseSharedTheme) = prefs.edit().putBoolean(SHOULD_USE_SHARED_THEME, shouldUseSharedTheme).apply()
var isUsingAutoTheme: Boolean
get() = prefs.getBoolean(IS_USING_AUTO_THEME, false)
set(isUsingAutoTheme) = prefs.edit().putBoolean(IS_USING_AUTO_THEME, isUsingAutoTheme).apply()
var isUsingSystemTheme: Boolean
get() = prefs.getBoolean(IS_USING_SYSTEM_THEME, isSPlus())
set(isUsingSystemTheme) = prefs.edit().putBoolean(IS_USING_SYSTEM_THEME, isUsingSystemTheme).apply()
var wasCustomThemeSwitchDescriptionShown: Boolean
get() = prefs.getBoolean(WAS_CUSTOM_THEME_SWITCH_DESCRIPTION_SHOWN, false)
set(wasCustomThemeSwitchDescriptionShown) = prefs.edit().putBoolean(WAS_CUSTOM_THEME_SWITCH_DESCRIPTION_SHOWN, wasCustomThemeSwitchDescriptionShown)
.apply()
var wasSharedThemeForced: Boolean
get() = prefs.getBoolean(WAS_SHARED_THEME_FORCED, false)
set(wasSharedThemeForced) = prefs.edit().putBoolean(WAS_SHARED_THEME_FORCED, wasSharedThemeForced).apply()
var showInfoBubble: Boolean
get() = prefs.getBoolean(SHOW_INFO_BUBBLE, true)
set(showInfoBubble) = prefs.edit().putBoolean(SHOW_INFO_BUBBLE, showInfoBubble).apply()
var lastConflictApplyToAll: Boolean
get() = prefs.getBoolean(LAST_CONFLICT_APPLY_TO_ALL, true)
set(lastConflictApplyToAll) = prefs.edit().putBoolean(LAST_CONFLICT_APPLY_TO_ALL, lastConflictApplyToAll).apply()
var lastConflictResolution: Int
get() = prefs.getInt(LAST_CONFLICT_RESOLUTION, CONFLICT_SKIP)
set(lastConflictResolution) = prefs.edit().putInt(LAST_CONFLICT_RESOLUTION, lastConflictResolution).apply()
var sorting: Int
get() = prefs.getInt(SORT_ORDER, context.resources.getInteger(R.integer.default_sorting))
set(sorting) = prefs.edit().putInt(SORT_ORDER, sorting).apply()
fun saveCustomSorting(path: String, value: Int) {
if (path.isEmpty()) {
sorting = value
} else {
prefs.edit().putInt(SORT_FOLDER_PREFIX + path.toLowerCase(), value).apply()
}
}
fun getFolderSorting(path: String) = prefs.getInt(SORT_FOLDER_PREFIX + path.toLowerCase(), sorting)
fun removeCustomSorting(path: String) {
prefs.edit().remove(SORT_FOLDER_PREFIX + path.toLowerCase()).apply()
}
fun hasCustomSorting(path: String) = prefs.contains(SORT_FOLDER_PREFIX + path.toLowerCase())
var hadThankYouInstalled: Boolean
get() = prefs.getBoolean(HAD_THANK_YOU_INSTALLED, false)
set(hadThankYouInstalled) = prefs.edit().putBoolean(HAD_THANK_YOU_INSTALLED, hadThankYouInstalled).apply()
var skipDeleteConfirmation: Boolean
get() = prefs.getBoolean(SKIP_DELETE_CONFIRMATION, false)
set(skipDeleteConfirmation) = prefs.edit().putBoolean(SKIP_DELETE_CONFIRMATION, skipDeleteConfirmation).apply()
var enablePullToRefresh: Boolean
get() = prefs.getBoolean(ENABLE_PULL_TO_REFRESH, true)
set(enablePullToRefresh) = prefs.edit().putBoolean(ENABLE_PULL_TO_REFRESH, enablePullToRefresh).apply()
var scrollHorizontally: Boolean
get() = prefs.getBoolean(SCROLL_HORIZONTALLY, false)
set(scrollHorizontally) = prefs.edit().putBoolean(SCROLL_HORIZONTALLY, scrollHorizontally).apply()
var preventPhoneFromSleeping: Boolean
get() = prefs.getBoolean(PREVENT_PHONE_FROM_SLEEPING, true)
set(preventPhoneFromSleeping) = prefs.edit().putBoolean(PREVENT_PHONE_FROM_SLEEPING, preventPhoneFromSleeping).apply()
var lastUsedViewPagerPage: Int
get() = prefs.getInt(LAST_USED_VIEW_PAGER_PAGE, context.resources.getInteger(R.integer.default_viewpager_page))
set(lastUsedViewPagerPage) = prefs.edit().putInt(LAST_USED_VIEW_PAGER_PAGE, lastUsedViewPagerPage).apply()
var use24HourFormat: Boolean
get() = prefs.getBoolean(USE_24_HOUR_FORMAT, DateFormat.is24HourFormat(context))
set(use24HourFormat) = prefs.edit().putBoolean(USE_24_HOUR_FORMAT, use24HourFormat).apply()
var isSundayFirst: Boolean
get() {
val isSundayFirst = Calendar.getInstance(Locale.getDefault()).firstDayOfWeek == Calendar.SUNDAY
return prefs.getBoolean(SUNDAY_FIRST, isSundayFirst)
}
set(sundayFirst) = prefs.edit().putBoolean(SUNDAY_FIRST, sundayFirst).apply()
var wasAlarmWarningShown: Boolean
get() = prefs.getBoolean(WAS_ALARM_WARNING_SHOWN, false)
set(wasAlarmWarningShown) = prefs.edit().putBoolean(WAS_ALARM_WARNING_SHOWN, wasAlarmWarningShown).apply()
var wasReminderWarningShown: Boolean
get() = prefs.getBoolean(WAS_REMINDER_WARNING_SHOWN, false)
set(wasReminderWarningShown) = prefs.edit().putBoolean(WAS_REMINDER_WARNING_SHOWN, wasReminderWarningShown).apply()
var useSameSnooze: Boolean
get() = prefs.getBoolean(USE_SAME_SNOOZE, true)
set(useSameSnooze) = prefs.edit().putBoolean(USE_SAME_SNOOZE, useSameSnooze).apply()
var snoozeTime: Int
get() = prefs.getInt(SNOOZE_TIME, 10)
set(snoozeDelay) = prefs.edit().putInt(SNOOZE_TIME, snoozeDelay).apply()
var vibrateOnButtonPress: Boolean
get() = prefs.getBoolean(VIBRATE_ON_BUTTON_PRESS, context.resources.getBoolean(R.bool.default_vibrate_on_press))
set(vibrateOnButton) = prefs.edit().putBoolean(VIBRATE_ON_BUTTON_PRESS, vibrateOnButton).apply()
var yourAlarmSounds: String
get() = prefs.getString(YOUR_ALARM_SOUNDS, "")!!
set(yourAlarmSounds) = prefs.edit().putString(YOUR_ALARM_SOUNDS, yourAlarmSounds).apply()
var isUsingModifiedAppIcon: Boolean
get() = prefs.getBoolean(IS_USING_MODIFIED_APP_ICON, false)
set(isUsingModifiedAppIcon) = prefs.edit().putBoolean(IS_USING_MODIFIED_APP_ICON, isUsingModifiedAppIcon).apply()
var appId: String
get() = prefs.getString(APP_ID, "")!!
set(appId) = prefs.edit().putString(APP_ID, appId).apply()
var initialWidgetHeight: Int
get() = prefs.getInt(INITIAL_WIDGET_HEIGHT, 0)
set(initialWidgetHeight) = prefs.edit().putInt(INITIAL_WIDGET_HEIGHT, initialWidgetHeight).apply()
var widgetIdToMeasure: Int
get() = prefs.getInt(WIDGET_ID_TO_MEASURE, 0)
set(widgetIdToMeasure) = prefs.edit().putInt(WIDGET_ID_TO_MEASURE, widgetIdToMeasure).apply()
var wasOrangeIconChecked: Boolean
get() = prefs.getBoolean(WAS_ORANGE_ICON_CHECKED, false)
set(wasOrangeIconChecked) = prefs.edit().putBoolean(WAS_ORANGE_ICON_CHECKED, wasOrangeIconChecked).apply()
var wasAppOnSDShown: Boolean
get() = prefs.getBoolean(WAS_APP_ON_SD_SHOWN, false)
set(wasAppOnSDShown) = prefs.edit().putBoolean(WAS_APP_ON_SD_SHOWN, wasAppOnSDShown).apply()
var wasBeforeAskingShown: Boolean
get() = prefs.getBoolean(WAS_BEFORE_ASKING_SHOWN, false)
set(wasBeforeAskingShown) = prefs.edit().putBoolean(WAS_BEFORE_ASKING_SHOWN, wasBeforeAskingShown).apply()
var wasBeforeRateShown: Boolean
get() = prefs.getBoolean(WAS_BEFORE_RATE_SHOWN, false)
set(wasBeforeRateShown) = prefs.edit().putBoolean(WAS_BEFORE_RATE_SHOWN, wasBeforeRateShown).apply()
var wasInitialUpgradeToProShown: Boolean
get() = prefs.getBoolean(WAS_INITIAL_UPGRADE_TO_PRO_SHOWN, false)
set(wasInitialUpgradeToProShown) = prefs.edit().putBoolean(WAS_INITIAL_UPGRADE_TO_PRO_SHOWN, wasInitialUpgradeToProShown).apply()
var wasAppIconCustomizationWarningShown: Boolean
get() = prefs.getBoolean(WAS_APP_ICON_CUSTOMIZATION_WARNING_SHOWN, false)
set(wasAppIconCustomizationWarningShown) = prefs.edit().putBoolean(WAS_APP_ICON_CUSTOMIZATION_WARNING_SHOWN, wasAppIconCustomizationWarningShown)
.apply()
var appSideloadingStatus: Int
get() = prefs.getInt(APP_SIDELOADING_STATUS, SIDELOADING_UNCHECKED)
set(appSideloadingStatus) = prefs.edit().putInt(APP_SIDELOADING_STATUS, appSideloadingStatus).apply()
var dateFormat: String
get() = prefs.getString(DATE_FORMAT, getDefaultDateFormat())!!
set(dateFormat) = prefs.edit().putString(DATE_FORMAT, dateFormat).apply()
private fun getDefaultDateFormat(): String {
val format = DateFormat.getDateFormat(context)
val pattern = (format as SimpleDateFormat).toLocalizedPattern()
return when (pattern.toLowerCase().replace(" ", "")) {
"d.M.y" -> DATE_FORMAT_ONE
"dd/mm/y" -> DATE_FORMAT_TWO
"mm/dd/y" -> DATE_FORMAT_THREE
"y-mm-dd" -> DATE_FORMAT_FOUR
"dmmmmy" -> DATE_FORMAT_FIVE
"mmmmdy" -> DATE_FORMAT_SIX
"mm-dd-y" -> DATE_FORMAT_SEVEN
"dd-mm-y" -> DATE_FORMAT_EIGHT
else -> DATE_FORMAT_ONE
}
}
var wasOTGHandled: Boolean
get() = prefs.getBoolean(WAS_OTG_HANDLED, false)
set(wasOTGHandled) = prefs.edit().putBoolean(WAS_OTG_HANDLED, wasOTGHandled).apply()
var wasUpgradedFromFreeShown: Boolean
get() = prefs.getBoolean(WAS_UPGRADED_FROM_FREE_SHOWN, false)
set(wasUpgradedFromFreeShown) = prefs.edit().putBoolean(WAS_UPGRADED_FROM_FREE_SHOWN, wasUpgradedFromFreeShown).apply()
var wasRateUsPromptShown: Boolean
get() = prefs.getBoolean(WAS_RATE_US_PROMPT_SHOWN, false)
set(wasRateUsPromptShown) = prefs.edit().putBoolean(WAS_RATE_US_PROMPT_SHOWN, wasRateUsPromptShown).apply()
var wasAppRated: Boolean
get() = prefs.getBoolean(WAS_APP_RATED, false)
set(wasAppRated) = prefs.edit().putBoolean(WAS_APP_RATED, wasAppRated).apply()
var wasSortingByNumericValueAdded: Boolean
get() = prefs.getBoolean(WAS_SORTING_BY_NUMERIC_VALUE_ADDED, false)
set(wasSortingByNumericValueAdded) = prefs.edit().putBoolean(WAS_SORTING_BY_NUMERIC_VALUE_ADDED, wasSortingByNumericValueAdded).apply()
var wasFolderLockingNoticeShown: Boolean
get() = prefs.getBoolean(WAS_FOLDER_LOCKING_NOTICE_SHOWN, false)
set(wasFolderLockingNoticeShown) = prefs.edit().putBoolean(WAS_FOLDER_LOCKING_NOTICE_SHOWN, wasFolderLockingNoticeShown).apply()
var lastRenameUsed: Int
get() = prefs.getInt(LAST_RENAME_USED, RENAME_SIMPLE)
set(lastRenameUsed) = prefs.edit().putInt(LAST_RENAME_USED, lastRenameUsed).apply()
var lastRenamePatternUsed: String
get() = prefs.getString(LAST_RENAME_PATTERN_USED, "")!!
set(lastRenamePatternUsed) = prefs.edit().putString(LAST_RENAME_PATTERN_USED, lastRenamePatternUsed).apply()
var lastExportedSettingsFolder: String
get() = prefs.getString(LAST_EXPORTED_SETTINGS_FOLDER, "")!!
set(lastExportedSettingsFolder) = prefs.edit().putString(LAST_EXPORTED_SETTINGS_FOLDER, lastExportedSettingsFolder).apply()
var lastBlockedNumbersExportPath: String
get() = prefs.getString(LAST_BLOCKED_NUMBERS_EXPORT_PATH, "")!!
set(lastBlockedNumbersExportPath) = prefs.edit().putString(LAST_BLOCKED_NUMBERS_EXPORT_PATH, lastBlockedNumbersExportPath).apply()
var blockUnknownNumbers: Boolean
get() = prefs.getBoolean(BLOCK_UNKNOWN_NUMBERS, false)
set(blockUnknownNumbers) = prefs.edit().putBoolean(BLOCK_UNKNOWN_NUMBERS, blockUnknownNumbers).apply()
var fontSize: Int
get() = prefs.getInt(FONT_SIZE, context.resources.getInteger(R.integer.default_font_size))
set(size) = prefs.edit().putInt(FONT_SIZE, size).apply()
// notify the users about new SMS Messenger and Voice Recorder released
var wasMessengerRecorderShown: Boolean
get() = prefs.getBoolean(WAS_MESSENGER_RECORDER_SHOWN, false)
set(wasMessengerRecorderShown) = prefs.edit().putBoolean(WAS_MESSENGER_RECORDER_SHOWN, wasMessengerRecorderShown).apply()
var defaultTab: Int
get() = prefs.getInt(DEFAULT_TAB, TAB_LAST_USED)
set(defaultTab) = prefs.edit().putInt(DEFAULT_TAB, defaultTab).apply()
var startNameWithSurname: Boolean
get() = prefs.getBoolean(START_NAME_WITH_SURNAME, false)
set(startNameWithSurname) = prefs.edit().putBoolean(START_NAME_WITH_SURNAME, startNameWithSurname).apply()
var favorites: MutableSet<String>
get() = prefs.getStringSet(FAVORITES, HashSet())!!
set(favorites) = prefs.edit().remove(FAVORITES).putStringSet(FAVORITES, favorites).apply()
var showCallConfirmation: Boolean
get() = prefs.getBoolean(SHOW_CALL_CONFIRMATION, false)
set(showCallConfirmation) = prefs.edit().putBoolean(SHOW_CALL_CONFIRMATION, showCallConfirmation).apply()
// color picker last used colors
var colorPickerRecentColors: LinkedList<Int>
get(): LinkedList<Int> {
val defaultList = arrayListOf(
context.resources.getColor(R.color.md_red_700),
context.resources.getColor(R.color.md_blue_700),
context.resources.getColor(R.color.md_green_700),
context.resources.getColor(R.color.md_yellow_700),
context.resources.getColor(R.color.md_orange_700)
)
return LinkedList(prefs.getString(COLOR_PICKER_RECENT_COLORS, null)?.lines()?.map { it.toInt() } ?: defaultList)
}
set(recentColors) = prefs.edit().putString(COLOR_PICKER_RECENT_COLORS, recentColors.joinToString(separator = "\n")).apply()
var ignoredContactSources: HashSet<String>
get() = prefs.getStringSet(IGNORED_CONTACT_SOURCES, hashSetOf(".")) as HashSet
set(ignoreContactSources) = prefs.edit().remove(IGNORED_CONTACT_SOURCES).putStringSet(IGNORED_CONTACT_SOURCES, ignoreContactSources).apply()
var showContactThumbnails: Boolean
get() = prefs.getBoolean(SHOW_CONTACT_THUMBNAILS, true)
set(showContactThumbnails) = prefs.edit().putBoolean(SHOW_CONTACT_THUMBNAILS, showContactThumbnails).apply()
var showPhoneNumbers: Boolean
get() = prefs.getBoolean(SHOW_PHONE_NUMBERS, false)
set(showPhoneNumbers) = prefs.edit().putBoolean(SHOW_PHONE_NUMBERS, showPhoneNumbers).apply()
var showOnlyContactsWithNumbers: Boolean
get() = prefs.getBoolean(SHOW_ONLY_CONTACTS_WITH_NUMBERS, false)
set(showOnlyContactsWithNumbers) = prefs.edit().putBoolean(SHOW_ONLY_CONTACTS_WITH_NUMBERS, showOnlyContactsWithNumbers).apply()
var lastUsedContactSource: String
get() = prefs.getString(LAST_USED_CONTACT_SOURCE, "")!!
set(lastUsedContactSource) = prefs.edit().putString(LAST_USED_CONTACT_SOURCE, lastUsedContactSource).apply()
var onContactClick: Int
get() = prefs.getInt(ON_CONTACT_CLICK, ON_CLICK_VIEW_CONTACT)
set(onContactClick) = prefs.edit().putInt(ON_CONTACT_CLICK, onContactClick).apply()
var showContactFields: Int
get() = prefs.getInt(
SHOW_CONTACT_FIELDS,
SHOW_FIRST_NAME_FIELD or SHOW_SURNAME_FIELD or SHOW_PHONE_NUMBERS_FIELD or SHOW_EMAILS_FIELD or
SHOW_ADDRESSES_FIELD or SHOW_EVENTS_FIELD or SHOW_NOTES_FIELD or SHOW_GROUPS_FIELD or SHOW_CONTACT_SOURCE_FIELD
)
set(showContactFields) = prefs.edit().putInt(SHOW_CONTACT_FIELDS, showContactFields).apply()
var showDialpadButton: Boolean
get() = prefs.getBoolean(SHOW_DIALPAD_BUTTON, true)
set(showDialpadButton) = prefs.edit().putBoolean(SHOW_DIALPAD_BUTTON, showDialpadButton).apply()
var wasLocalAccountInitialized: Boolean
get() = prefs.getBoolean(WAS_LOCAL_ACCOUNT_INITIALIZED, false)
set(wasLocalAccountInitialized) = prefs.edit().putBoolean(WAS_LOCAL_ACCOUNT_INITIALIZED, wasLocalAccountInitialized).apply()
var lastExportPath: String
get() = prefs.getString(LAST_EXPORT_PATH, "")!!
set(lastExportPath) = prefs.edit().putString(LAST_EXPORT_PATH, lastExportPath).apply()
var speedDial: String
get() = prefs.getString(SPEED_DIAL, "")!!
set(speedDial) = prefs.edit().putString(SPEED_DIAL, speedDial).apply()
var showPrivateContacts: Boolean
get() = prefs.getBoolean(SHOW_PRIVATE_CONTACTS, true)
set(showPrivateContacts) = prefs.edit().putBoolean(SHOW_PRIVATE_CONTACTS, showPrivateContacts).apply()
var mergeDuplicateContacts: Boolean
get() = prefs.getBoolean(MERGE_DUPLICATE_CONTACTS, true)
set(mergeDuplicateContacts) = prefs.edit().putBoolean(MERGE_DUPLICATE_CONTACTS, mergeDuplicateContacts).apply()
var favoritesContactsOrder: String
get() = prefs.getString(FAVORITES_CONTACTS_ORDER, "")!!
set(order) = prefs.edit().putString(FAVORITES_CONTACTS_ORDER, order).apply()
var isCustomOrderSelected: Boolean
get() = prefs.getBoolean(FAVORITES_CUSTOM_ORDER_SELECTED, false)
set(selected) = prefs.edit().putBoolean(FAVORITES_CUSTOM_ORDER_SELECTED, selected).apply()
}

Some files were not shown because too many files have changed in this diff Show More