diff --git a/app/build.gradle b/app/build.gradle
index c9751c52..c9602d71 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -135,10 +135,11 @@ dependencies {
implementation "eu.szkolny:agendacalendarview:1799f8ef47"
implementation "eu.szkolny:cafebar:5bf0c618de"
implementation "eu.szkolny.fslogin:lib:2.0.0"
- implementation "eu.szkolny:material-about-library:0534abf316"
+ implementation "eu.szkolny:material-about-library:1d5ebaf47c"
implementation "eu.szkolny:mhttp:af4b62e6e9"
implementation "eu.szkolny:nachos:0e5dfcaceb"
implementation "eu.szkolny.selective-dao:annotation:27f8f3f194"
+ implementation "eu.szkolny:ssl-provider:1.0.0"
implementation "pl.szczodrzynski:navlib:0.7.2"
implementation "pl.szczodrzynski:numberslidingpicker:2921225f76"
implementation "pl.szczodrzynski:recyclertablayout:700f980584"
@@ -149,7 +150,7 @@ dependencies {
implementation "com.mikepenz:iconics-core:5.2.8"
implementation "com.mikepenz:iconics-views:5.2.8"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
- implementation "eu.szkolny:szkolny-font:1dab7d64ed"
+ implementation "eu.szkolny:szkolny-font:1.3"
// Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0"
@@ -159,6 +160,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2"
implementation "com.github.bassaer:chatmessageview:2.0.1"
+ implementation "com.github.CanHub:Android-Image-Cropper:2.2.2"
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1"
implementation "com.github.jetradarmobile:android-snowfall:1.2.0"
implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31"
@@ -167,7 +169,6 @@ dependencies {
implementation "com.jaredrummler:colorpicker:1.1.0"
implementation "com.qifan.powerpermission:powerpermission-coroutines:1.3.0"
implementation "com.qifan.powerpermission:powerpermission:1.3.0"
- implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
implementation "com.wdullaer:materialdatetimepicker:4.2.3"
implementation "com.yuyh.json:jsonviewer:1.0.6"
implementation "io.coil-kt:coil:1.1.1"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c5bfa07f..bf1d5732 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -143,8 +143,7 @@
-
-
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt
index 43a8b921..e8aa4cd2 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt
@@ -26,6 +26,8 @@ import com.google.firebase.messaging.FirebaseMessaging
import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics
+import eu.szkolny.sslprovider.SSLProvider
+import eu.szkolny.sslprovider.enableSupportedTls
import im.wangchao.mhttp.MHttp
import kotlinx.coroutines.*
import me.leolin.shortcutbadger.ShortcutBadger
@@ -44,6 +46,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.*
+import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@@ -51,8 +54,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
companion object {
@Volatile
lateinit var db: AppDb
- val config: Config by lazy { Config(db) }
- var profile: Profile by mutableLazy { Profile(0, 0, 0, "") }
+ lateinit var config: Config
+ lateinit var profile: Profile
val profileId
get() = profile.id
@@ -95,19 +98,22 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
| __ | | | | | | ___/
| | | | | | | | | |
|_| |_| |_| |_| |*/
- val http: OkHttpClient by lazy {
- val builder = OkHttpClient.Builder()
- .cache(null)
- .followRedirects(true)
- .followSslRedirects(true)
- .retryOnConnectionFailure(true)
- .cookieJar(cookieJar)
- .connectTimeout(15, TimeUnit.SECONDS)
- .writeTimeout(10, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- builder.installHttpsSupport(this)
+ lateinit var http: OkHttpClient
+ lateinit var httpLazy: OkHttpClient
- if (devMode || BuildConfig.DEBUG) {
+ private fun buildHttp() {
+ val builder = OkHttpClient.Builder()
+ .cache(null)
+ .followRedirects(true)
+ .followSslRedirects(true)
+ .retryOnConnectionFailure(true)
+ .cookieJar(cookieJar)
+ .connectTimeout(15, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .enableSupportedTls(enableCleartext = true)
+
+ if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
@@ -116,13 +122,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
builder.addInterceptor(chuckerInterceptor)
}
- builder.build()
- }
- val httpLazy: OkHttpClient by lazy {
- http.newBuilder()
- .followRedirects(false)
- .followSslRedirects(false)
- .build()
+ http = builder.build()
+
+ httpLazy = http.newBuilder()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build()
+
+ MHttp.instance().customOkHttpClient(http)
}
val cookieJar by lazy { DumbCookieJar(this) }
@@ -159,32 +166,46 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
.errorActivity(CrashActivity::class.java)
.apply()
Iconics.init(applicationContext)
+
+ // initialize companion object values
App.db = AppDb(this)
- Themes.themeInt = config.ui.theme
- devMode = config.debugMode
- MHttp.instance().customOkHttpClient(http)
+ App.config = Config(App.db)
+ App.profile = Profile(0, 0, 0, "")
+ debugMode = BuildConfig.DEBUG
+ devMode = config.debugMode || debugMode
if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) }
}
+ buildHttp()
+
+ Themes.themeInt = config.ui.theme
config.ui.language?.let {
setLanguage(it)
}
- debugMode = BuildConfig.DEBUG
- if (BuildConfig.DEBUG)
- devMode = true
-
Signing.getCert(this)
launch {
withContext(Dispatchers.Default) {
config.migrate(this@App)
+ SSLProvider.install(
+ applicationContext,
+ downloadIfNeeded = true,
+ supportTls13 = false,
+ onFinish = {
+ buildHttp()
+ },
+ onError = {
+ Timber.e("Failed to install SSLProvider: $it")
+ it.printStackTrace()
+ }
+ )
+
if (config.devModePassword != null)
checkDevModePassword()
- devMode = debugMode || config.debugMode
if (config.sync.enabled)
SyncWorker.scheduleNext(this@App, false)
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
index 71febada..af15b543 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
@@ -1,12 +1,9 @@
package pl.szczodrzynski.edziennik
-import android.Manifest
-import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
import android.database.Cursor
@@ -29,7 +26,6 @@ import android.view.View
import android.view.WindowManager
import android.widget.*
import androidx.annotation.*
-import androidx.core.app.ActivityCompat
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
@@ -40,7 +36,6 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager.widget.ViewPager
-import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.*
@@ -50,10 +45,7 @@ import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import okhttp3.ConnectionSpec
-import okhttp3.OkHttpClient
import okhttp3.RequestBody
-import okhttp3.TlsVersion
import okio.Buffer
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@@ -63,7 +55,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team
-import pl.szczodrzynski.edziennik.network.TLSSocketFactory
import pl.szczodrzynski.edziennik.utils.models.Time
import java.io.InterruptedIOException
import java.io.PrintWriter
@@ -73,17 +64,13 @@ import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.nio.charset.Charset
-import java.security.KeyStore
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.CRC32
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
-import javax.net.ssl.SSLContext
import javax.net.ssl.SSLException
-import javax.net.ssl.TrustManagerFactory
-import javax.net.ssl.X509TrustManager
import kotlin.Pair
@@ -304,19 +291,6 @@ fun colorFromCssName(name: String): Int {
fun List.filterOutArchived() = this.filter { !it.archived }
-fun Activity.isStoragePermissionGranted(): Boolean {
- return if (Build.VERSION.SDK_INT >= 23) {
- if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
- true
- } else {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
- false
- }
- } else {
- true
- }
-}
-
fun Response?.getUnixDate(): Long {
val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix()
val pattern = "EEE, dd MMM yyyy HH:mm:ss Z"
@@ -1107,40 +1081,6 @@ fun Cursor?.getString(columnName: String) = this?.getStringOrNull(getColumnIndex
fun Cursor?.getInt(columnName: String) = this?.getIntOrNull(getColumnIndex(columnName))
fun Cursor?.getLong(columnName: String) = this?.getLongOrNull(getColumnIndex(columnName))
-fun OkHttpClient.Builder.installHttpsSupport(context: Context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
- try {
- try {
- ProviderInstaller.installIfNeeded(context)
- } catch (e: Exception) {
- Log.e("OkHttpTLSCompat", "Play Services not found or outdated")
-
- val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
- trustManagerFactory.init(null as KeyStore?)
-
- val x509TrustManager = trustManagerFactory.trustManagers.singleOrNull { it is X509TrustManager } as X509TrustManager?
- ?: return
-
- val sc = SSLContext.getInstance("TLSv1.2")
- sc.init(null, null, null)
- sslSocketFactory(TLSSocketFactory(sc.socketFactory), x509TrustManager)
- val cs: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
- .tlsVersions(TlsVersion.TLS_1_0)
- .tlsVersions(TlsVersion.TLS_1_1)
- .tlsVersions(TlsVersion.TLS_1_2)
- .build()
- val specs: MutableList = ArrayList()
- specs.add(cs)
- specs.add(ConnectionSpec.COMPATIBLE_TLS)
- specs.add(ConnectionSpec.CLEARTEXT)
- connectionSpecs(specs)
- }
- } catch (exc: Exception) {
- Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc)
- }
- }
-}
-
fun CharSequence.containsAll(list: List, ignoreCase: Boolean = false): Boolean {
for (i in list) {
if (!contains(i, ignoreCase))
@@ -1278,3 +1218,41 @@ operator fun Iterable>.get(key: K): V? {
}
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
+
+fun MutableList.after(what: E, insert: E) {
+ val index = indexOf(what)
+ if (index != -1)
+ add(index + 1, insert)
+}
+
+fun MutableList.before(what: E, insert: E) {
+ val index = indexOf(what)
+ if (index != -1)
+ add(index, insert)
+}
+
+fun MutableList.after(what: E, insert: Collection) {
+ val index = indexOf(what)
+ if (index != -1)
+ addAll(index + 1, insert)
+}
+
+fun MutableList.before(what: E, insert: Collection) {
+ val index = indexOf(what)
+ if (index != -1)
+ addAll(index, insert)
+}
+
+fun Context.getSyncInterval(interval: Int): String {
+ val hours = interval / 60 / 60
+ val minutes = interval / 60 % 60
+ val hoursText = if (hours > 0)
+ plural(R.plurals.time_till_hours, hours)
+ else
+ null
+ val minutesText = if (minutes > 0)
+ plural(R.plurals.time_till_minutes, minutes)
+ else
+ ""
+ return hoursText?.plus(" $minutesText") ?: minutesText
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
index 294f68f9..5b6676d0 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
@@ -10,13 +10,11 @@ import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle
-import android.os.Environment
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
@@ -56,7 +54,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
-import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
+import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment
@@ -79,7 +77,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
-import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
+import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
@@ -97,7 +95,6 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
-import java.io.File
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
@@ -110,8 +107,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
const val TAG = "MainActivity"
- const val REQUEST_LOGIN_ACTIVITY = 20222
-
const val DRAWER_PROFILE_ADD_NEW = 200
const val DRAWER_PROFILE_SYNC_ALL = 201
const val DRAWER_PROFILE_EXPORT_DATA = 202
@@ -199,7 +194,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.isStatic(true)
.isBelowSeparator(true)
- list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsNewFragment::class)
+ list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
.isInDrawer(true)
.isStatic(true)
@@ -257,6 +252,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
+ val requestHandler by lazy { MainActivityRequestHandler(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@@ -307,6 +303,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val versionBadge = app.buildManager.versionBadge
b.nightlyText.isVisible = versionBadge != null
b.nightlyText.text = versionBadge
+ if (versionBadge != null) {
+ b.nightlyText.background.setTintColor(0xa0ff0000.toInt())
+ }
navLoading = true
@@ -382,7 +381,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
drawerProfileLongClickListener = { _, profile, _, view ->
if (view != null && profile is ProfileDrawerItem) {
- showProfileContextMenu(profile, view)
+ launch {
+ val appProfile = withContext(Dispatchers.IO) {
+ App.db.profileDao().getByIdNow(profile.identifier.toInt())
+ } ?: return@launch
+ drawer.close()
+ ProfileConfigDialog(this@MainActivity, appProfile)
+ }
true
}
else {
@@ -462,28 +467,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
// APP BACKGROUND
- if (app.config.ui.appBackground != null) {
- try {
- app.config.ui.appBackground?.let {
- var bg = it
- val bgDir = File(Environment.getExternalStoragePublicDirectory("Szkolny.eu"), "bg")
- if (bgDir.exists()) {
- val files = bgDir.listFiles()
- val r = Random()
- val i = r.nextInt(files.size)
- bg = files[i].toString()
- }
- val linearLayout = b.root
- if (bg.endsWith(".gif")) {
- linearLayout.background = GifDrawable(bg)
- } else {
- linearLayout.background = BitmapDrawable.createFromPath(bg)
- }
- }
- } catch (e: IOException) {
- e.printStackTrace()
- }
- }
+ setAppBackground()
// IT'S WINTER MY DUDES
val today = Date.getToday()
@@ -565,7 +549,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
- .withIcon(CommunityMaterial.Icon.cmd_android_studio)
+ .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) })
}
}
@@ -573,7 +557,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
private var profileSettingClickListener = { id: Int, view: View? ->
when (id) {
DRAWER_PROFILE_ADD_NEW -> {
- startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_LOGIN_ACTIVITY)
+ requestHandler.requestLogin()
}
DRAWER_PROFILE_SYNC_ALL -> {
EdziennikTask.sync().enqueue(this)
@@ -821,7 +805,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
handleIntent(intent?.extras)
}
}
- private fun handleIntent(extras: Bundle?) {
+ fun handleIntent(extras: Bundle?) {
d(TAG, "handleIntent() {")
extras?.keySet()?.forEach { key ->
@@ -979,13 +963,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == REQUEST_LOGIN_ACTIVITY) {
- if (!app.config.loginFinished)
- finish()
- else {
- handleIntent(data?.extras)
- }
- }
+ requestHandler.handleResult(requestCode, resultCode, data)
}
/* _ _ _ _ _
@@ -1211,6 +1189,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}, 3000)
}
+ fun setAppBackground() {
+ try {
+ b.root.background = app.config.ui.appBackground?.let {
+ if (it.endsWith(".gif"))
+ GifDrawable(it)
+ else
+ BitmapDrawable.createFromPath(it)
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+
/* _____ _ _
| __ \ (_) |
| | | |_ __ __ ___ _____ _ __ _| |_ ___ _ __ ___ ___
@@ -1286,26 +1277,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.addProfileSettings(*drawerProfiles.toTypedArray())
}
- private fun showProfileContextMenu(profile: IProfile, view: View) {
- val profileId = profile.identifier.toInt()
- val popupMenu = PopupMenu(this, view)
- popupMenu.menu.add(0, 1, 1, R.string.profile_menu_open_settings)
- popupMenu.menu.add(0, 2, 2, R.string.profile_menu_remove)
- popupMenu.setOnMenuItemClickListener { item ->
- if (item.itemId == 1) {
- if (profileId != app.profile.id) {
- loadProfile(profileId, DRAWER_ITEM_SETTINGS)
- return@setOnMenuItemClickListener true
- }
- loadTarget(DRAWER_ITEM_SETTINGS, null)
- } else if (item.itemId == 2) {
- ProfileRemoveDialog(this, profileId, profile.name?.getText(this) ?: "?")
- }
- true
- }
- popupMenu.show()
- }
-
private val targetPopToHomeList = arrayListOf()
private var targetHomeId: Int = -1
override fun onBackPressed() {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt
new file mode 100644
index 00000000..a39a5872
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-3-23.
+ */
+
+package pl.szczodrzynski.edziennik
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.provider.OpenableColumns
+import com.canhub.cropper.CropImage
+import com.canhub.cropper.CropImageView
+import pl.szczodrzynski.edziennik.data.db.entity.Profile
+import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
+import java.io.File
+import java.io.FileOutputStream
+
+class MainActivityRequestHandler(val activity: MainActivity) {
+ companion object {
+ private const val REQUEST_LOGIN_ACTIVITY = 2000
+ private const val REQUEST_FILE_HEADER_BACKGROUND = 3000
+ private const val REQUEST_FILE_APP_BACKGROUND = 4000
+ private const val REQUEST_FILE_PROFILE_IMAGE = 5000
+ private const val REQUEST_CROP_HEADER_BACKGROUND = 3100
+ private const val REQUEST_CROP_APP_BACKGROUND = 4100
+ private const val REQUEST_CROP_PROFILE_IMAGE = 5100
+ }
+
+ private val app = activity.app
+ private val requestData = mutableMapOf()
+ private val listeners = mutableMapOf Unit>()
+
+ private val manager
+ get() = app.permissionManager
+
+ fun requestLogin() = activity.startActivityForResult(
+ Intent(activity, LoginActivity::class.java),
+ REQUEST_LOGIN_ACTIVITY
+ )
+
+ fun requestHeaderBackground(listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_HEADER_BACKGROUND] = listener
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_HEADER_BACKGROUND
+ )
+ }
+
+ fun requestAppBackground(listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_APP_BACKGROUND] = listener
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_APP_BACKGROUND
+ )
+ }
+
+ fun requestProfileImage(profile: Profile, listener: (Any?) -> Unit) =
+ manager.requestCameraPermission(
+ activity, 0, isRequired = false
+ ) {
+ listeners[REQUEST_FILE_PROFILE_IMAGE] = listener
+ requestData[REQUEST_FILE_PROFILE_IMAGE] = profile
+ activity.startActivityForResult(
+ CropImage.getPickImageChooserIntent(
+ activity,
+ activity.getString(R.string.pick_image_intent_chooser_title),
+ true,
+ true
+ ),
+ REQUEST_FILE_PROFILE_IMAGE
+ )
+ }
+
+ private fun getFileInfo(uri: Uri): Pair {
+ if (uri.scheme == "file") {
+ return (uri.lastPathSegment ?: "unknown") to null
+ }
+ val cursor = activity.contentResolver.query(
+ uri,
+ null,
+ null,
+ null,
+ null,
+ null
+ )
+
+ return cursor?.use {
+ if (it.moveToFirst()) {
+ val name = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+ val mimeIndex = it.getColumnIndex("mime_type")
+ val mimeType = if (mimeIndex != -1) it.getString(mimeIndex) else null
+
+ name to mimeType
+ } else
+ null
+ } ?: "unknown" to null
+ }
+
+ private fun shouldCrop(uri: Uri): Boolean {
+ val (filename, mimeType) = getFileInfo(uri)
+ return !filename.endsWith(".gif") && mimeType?.endsWith("/gif") != true
+ }
+
+ private fun saveFile(uri: Uri, name: String): String {
+ val (filename, _) = getFileInfo(uri)
+ val extension = filename.substringAfterLast('.')
+ val file = File(activity.filesDir, "$name.$extension")
+ activity.contentResolver.openInputStream(uri)?.use { input ->
+ FileOutputStream(file).use { output ->
+ input.copyTo(output)
+ }
+ }
+ return file.absolutePath
+ }
+
+ fun handleResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode != Activity.RESULT_OK)
+ return
+ var uri = CropImage.getPickImageResultUri(activity, data)
+ when (requestCode) {
+ REQUEST_LOGIN_ACTIVITY -> {
+ if (!app.config.loginFinished)
+ activity.finish()
+ else {
+ activity.handleIntent(data?.extras)
+ }
+ }
+ REQUEST_FILE_HEADER_BACKGROUND -> {
+ if (uri == null)
+ return // TODO: 2021-03-24 if the app returns no data
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setAspectRatio(512, 288)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .setRequestedSize(512, 288)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_HEADER_BACKGROUND)
+ } else {
+ val path = saveFile(uri, "header")
+ app.config.ui.headerBackground = path
+ listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
+ }
+ }
+ REQUEST_FILE_APP_BACKGROUND -> {
+ if (uri == null)
+ return
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_APP_BACKGROUND)
+ } else {
+ val path = saveFile(uri, "background")
+ app.config.ui.appBackground = path
+ listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
+ }
+ }
+ REQUEST_FILE_PROFILE_IMAGE -> {
+ if (uri == null)
+ return
+ if (shouldCrop(uri)) {
+ val intent = CropImage.activity(uri)
+ .setAspectRatio(1, 1)
+ .setCropShape(CropImageView.CropShape.OVAL)
+ .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
+ .setAllowFlipping(true)
+ .setAllowRotation(true)
+ .setRequestedSize(512, 512)
+ .getIntent(activity)
+ activity.startActivityForResult(intent, REQUEST_CROP_PROFILE_IMAGE)
+ } else {
+ val profile =
+ requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
+ val path = saveFile(uri, "profile${profile.id}")
+ profile.image = path
+ listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
+ }
+ }
+ REQUEST_CROP_HEADER_BACKGROUND -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val path = saveFile(uri, "header")
+ app.config.ui.headerBackground = path
+ listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
+ }
+ REQUEST_CROP_APP_BACKGROUND -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val path = saveFile(uri, "background")
+ app.config.ui.appBackground = path
+ listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
+ }
+ REQUEST_CROP_PROFILE_IMAGE -> {
+ uri = CropImage.getActivityResult(data)?.uri ?: return
+ val profile = requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
+ val path = saveFile(uri, "profile${profile.id}")
+ profile.image = path
+ listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt
new file mode 100644
index 00000000..13dcb6f9
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/bell/BellSyncConfigDialog.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-3-20.
+ */
+
+package pl.szczodrzynski.edziennik.ui.dialogs.bell
+
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.widget.addTextChangedListener
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.utils.models.Time
+import kotlin.coroutines.CoroutineContext
+
+class BellSyncConfigDialog(
+ val activity: AppCompatActivity,
+ val onChangeListener: (() -> Unit)? = null,
+ val onShowListener: ((tag: String) -> Unit)? = null,
+ val onDismissListener: ((tag: String) -> Unit)? = null
+) : CoroutineScope {
+ companion object {
+ private const val TAG = "BellSyncConfigDialog"
+ }
+
+ private lateinit var app: App
+ private lateinit var dialog: AlertDialog
+
+ private val job = Job()
+ override val coroutineContext: CoroutineContext
+ get() = job + Dispatchers.Main
+
+ // local variables go here
+
+ private fun parse(input: String): Pair