diff --git a/app/build.gradle b/app/build.gradle index ff94355..7dc8893 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,19 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + flavorDimensions += "version" + productFlavors { + freeVersion { + dimension "version" + applicationId "com.phpbg.easysync.trial" + resValue "string", "flavored_app_name", "Easy Sync Trial" + + } + paidVersion { + dimension "version" + resValue "string", "flavored_app_name", "Easy Sync" + } + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 09994a5..85464bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" + android:label="@string/flavored_app_name" android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" diff --git a/app/src/main/java/com/phpbg/easysync/MyApp.kt b/app/src/main/java/com/phpbg/easysync/MyApp.kt index 620df74..9206df9 100644 --- a/app/src/main/java/com/phpbg/easysync/MyApp.kt +++ b/app/src/main/java/com/phpbg/easysync/MyApp.kt @@ -25,11 +25,36 @@ package com.phpbg.easysync import android.app.Application +import android.content.Context import android.os.StrictMode +import java.lang.System.currentTimeMillis +import kotlin.math.floor +import kotlin.math.max + +const val TRIAL_DURATION_DAYS = 30 class MyApp : Application() { init { if (BuildConfig.DEBUG) StrictMode.enableDefaults() } + + companion object { + fun isTrial(): Boolean { + @Suppress("KotlinConstantConditions") + return (BuildConfig.FLAVOR == "freeVersion") + } + + fun getTrialRemainingDays(context: Context): Int { + val installed = context + .packageManager + .getPackageInfo(context.packageName, 0).firstInstallTime + val now = currentTimeMillis() + return max(0, TRIAL_DURATION_DAYS- floor((now-installed)/(24*3600*1000).toFloat()).toInt()) + } + + fun isTrialExpired(context: Context): Boolean { + return isTrial() && getTrialRemainingDays(context) == 0 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/phpbg/easysync/Notifications.kt b/app/src/main/java/com/phpbg/easysync/Notifications.kt index d786429..ddc263e 100644 --- a/app/src/main/java/com/phpbg/easysync/Notifications.kt +++ b/app/src/main/java/com/phpbg/easysync/Notifications.kt @@ -25,5 +25,6 @@ package com.phpbg.easysync enum class Notifications(val id: Int) { - MISSING_PERMISSIONS(1) + MISSING_PERMISSIONS(1), + TRIAL_EXPIRED(2) } \ No newline at end of file diff --git a/app/src/main/java/com/phpbg/easysync/ui/MainActivity.kt b/app/src/main/java/com/phpbg/easysync/ui/MainActivity.kt index 51f92cc..e92ff0b 100644 --- a/app/src/main/java/com/phpbg/easysync/ui/MainActivity.kt +++ b/app/src/main/java/com/phpbg/easysync/ui/MainActivity.kt @@ -24,6 +24,7 @@ package com.phpbg.easysync.ui +import android.content.ActivityNotFoundException import android.content.Intent import android.content.res.Configuration import android.net.Uri @@ -47,6 +48,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Help +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Warning @@ -58,8 +60,10 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.IntState import androidx.compose.runtime.State import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -67,6 +71,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -126,7 +131,9 @@ class MainActivity : ComponentActivity() { showDavStatus = viewModel.showDavStatus, isDavLoading = viewModel.isDavLoading, isDavConnected = viewModel.isDavConnected, - hasOptionalPermissions = hasOptionalPermissions + isTrial = viewModel.isTrial, + hasOptionalPermissions = hasOptionalPermissions, + trialRemainingDays = viewModel.trialRemainingDays ) } } @@ -148,7 +155,9 @@ private fun Main( showDavStatus: State, isDavLoading: State, isDavConnected: State, + isTrial: State, hasOptionalPermissions: State, + trialRemainingDays: IntState, ) { val mContext = LocalContext.current val syncEnabled = workerState == null || workerState != WorkInfo.State.RUNNING @@ -163,7 +172,7 @@ private fun Main( .padding(16.dp) .verticalScroll(rememberScrollState()) ) { - Title(text = stringResource(R.string.app_name)) + Title(text = stringResource(R.string.flavored_app_name)) val davSettingsHandler = fun(_: Int) { val myIntent = Intent(mContext, DavSettingsActivity::class.java) @@ -247,6 +256,23 @@ private fun Main( }, ) + if (isTrial.value) { + val msg = if (trialRemainingDays.intValue == 0) stringResource(R.string.home_trial_over) else pluralStringResource(R.plurals.home_trial_days_left, trialRemainingDays.intValue, trialRemainingDays.intValue) + StatusTitleClickable( + title = null, + actionTitle = msg, + statusColor = Color.Gray, + statusIcon = Icons.Default.Info, + clickHandler = { + try { + mContext.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.phpbg.easysync"))) + } catch (e: ActivityNotFoundException) { + mContext.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.phpbg.easysync"))) + } + }, + ) + } + Spacer(modifier = Modifier.height(16.dp)) val syncedPercent = if (localCount > 0 && syncedCount >= 0) { @@ -354,7 +380,29 @@ private fun MainPreview() { showDavStatus = remember { mutableStateOf(true) }, isDavLoading = remember { mutableStateOf(false) }, isDavConnected = remember { mutableStateOf(true) }, + isTrial = remember { mutableStateOf(false) }, + hasOptionalPermissions = remember { mutableStateOf(false) }, + trialRemainingDays = remember { mutableIntStateOf(0) } + ) + } +} + +@Preview(name = "Trial Mode", showBackground = false) +@Composable +private fun MainPreviewTrial() { + MyApplicationTheme { + Main( + fullSyncNowHandler = {}, + workerState = WorkInfo.State.RUNNING, + syncedCount = 10000, + localCount = 100, + jobCount = -1, + showDavStatus = remember { mutableStateOf(true) }, + isDavLoading = remember { mutableStateOf(false) }, + isDavConnected = remember { mutableStateOf(true) }, + isTrial = remember { mutableStateOf(true) }, hasOptionalPermissions = remember { mutableStateOf(false) }, + trialRemainingDays = remember { mutableIntStateOf(28) } ) } } \ No newline at end of file diff --git a/app/src/main/java/com/phpbg/easysync/ui/MainViewModel.kt b/app/src/main/java/com/phpbg/easysync/ui/MainViewModel.kt index f8e324c..d334138 100644 --- a/app/src/main/java/com/phpbg/easysync/ui/MainViewModel.kt +++ b/app/src/main/java/com/phpbg/easysync/ui/MainViewModel.kt @@ -32,6 +32,7 @@ import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.util.Log +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData @@ -39,6 +40,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo +import com.phpbg.easysync.MyApp import com.phpbg.easysync.dav.CollectionPath import com.phpbg.easysync.dav.MisconfigurationException import com.phpbg.easysync.dav.WebDavService @@ -85,6 +87,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val isDavConnected = mutableStateOf(false) val isDavLoading = mutableStateOf(false) + val isTrial = mutableStateOf(false) + val trialRemainingDays = mutableIntStateOf(0) + val workInfosList = FullSyncWorker.getLiveData(this.getApplication()).map { x -> if (x.isEmpty()) { return@map null @@ -120,6 +125,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { viewModelScope.launch { loadDav() } + + isTrial.value = MyApp.isTrial() + trialRemainingDays.intValue = MyApp.getTrialRemainingDays(getApplication()) } private fun loadImages() { diff --git a/app/src/main/java/com/phpbg/easysync/worker/FileDetectWorker.kt b/app/src/main/java/com/phpbg/easysync/worker/FileDetectWorker.kt index 5757691..488c940 100644 --- a/app/src/main/java/com/phpbg/easysync/worker/FileDetectWorker.kt +++ b/app/src/main/java/com/phpbg/easysync/worker/FileDetectWorker.kt @@ -32,6 +32,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters +import com.phpbg.easysync.MyApp import com.phpbg.easysync.mediastore.MediaStoreService import com.phpbg.easysync.mediastore.URIS @@ -45,6 +46,9 @@ private const val TAG = "FileDetectWorker" class FileDetectWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { + if (MyApp.isTrialExpired(this.applicationContext)) { + return Result.success() + } try { _doWork() } catch (e: Exception) { diff --git a/app/src/main/java/com/phpbg/easysync/worker/FullSyncWorker.kt b/app/src/main/java/com/phpbg/easysync/worker/FullSyncWorker.kt index 7a6d922..6a0885c 100644 --- a/app/src/main/java/com/phpbg/easysync/worker/FullSyncWorker.kt +++ b/app/src/main/java/com/phpbg/easysync/worker/FullSyncWorker.kt @@ -39,6 +39,7 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters +import com.phpbg.easysync.MyApp.Companion.isTrialExpired import com.phpbg.easysync.Notifications import com.phpbg.easysync.R import com.phpbg.easysync.dav.MisconfigurationException @@ -57,6 +58,10 @@ class FullSyncWorker(context: Context, parameters: WorkerParameters) : NotificationManager override suspend fun doWork(): Result { + if (isTrialExpired(this.applicationContext)) { + showTrialExpiredNotification() + return Result.success() + } val immediate = inputData.getBoolean(IMMEDIATE_KEY, false) return try { val syncService = SyncService.getInstance(this.applicationContext) @@ -83,17 +88,28 @@ class FullSyncWorker(context: Context, parameters: WorkerParameters) : } } + private fun showTrialExpiredNotification() { + val title = applicationContext.getString(R.string.notification_trial_over_title) + val text = applicationContext.getString(R.string.notification_trial_over_text) + val notificationId = Notifications.TRIAL_EXPIRED + showNotification(title, text, notificationId) + } + private fun showMissingPermissionsNotification() { - val id = applicationContext.getString(R.string.notification_channel_id) val title = applicationContext.getString(R.string.notification_missing_permissions_title) val text = applicationContext.getString(R.string.notification_missing_permissions_text) + val notificationId = Notifications.MISSING_PERMISSIONS + showNotification(title, text, notificationId) + } + + private fun showNotification(title: String, text: String, notificationId: Notifications) { + val id = applicationContext.getString(R.string.notification_channel_id) val intent = Intent(this.applicationContext, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } val pendingIntent = PendingIntent.getActivity(this.applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) - createChannel(id) val notification = Notification.Builder(applicationContext, id) .setContentTitle(title) @@ -105,7 +121,7 @@ class FullSyncWorker(context: Context, parameters: WorkerParameters) : .setContentIntent(pendingIntent) .build() - notificationManager.notify(Notifications.MISSING_PERMISSIONS.id, notification) + notificationManager.notify(notificationId.id, notification) } private fun createChannel(channelId: String) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8f7fab..f1b666a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2,6 +2,8 @@ Bitte erteilen Sie Berechtigungen, um die Synchronisation zu ermöglichen Synchronisation fehlgeschlagen + Synchronisation gestoppt + Bitte kaufen Sie die Vollversion Einstellungen Passwort ausblenden Passwort @@ -25,6 +27,11 @@ Beheben… läuft synchronisiert + Die Testversion ist abgelaufen, bitte kaufen Sie die Vollversion + + Testversion: %d Tag verbleibend + Testversion: %d Tage verbleibend + Berechtigungen Um Ihre Dateien zu synchronisieren, benötigen wir vollen Zugriff auf Ihre Dateien. Gehen Sie zu den Einstellungen, um die Berechtigung zu aktivieren Dateieinstellungen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5c6e44c..c5c92f6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2,6 +2,8 @@ Veuillez accorder les permissions pour permettre la synchronisation Synchronisation KO + Synchronisation interrompue + Veuillez acheter la version complète Paramètres Nom d\'utilisateur Mot de passe @@ -23,6 +25,11 @@ Corriger… attente en cours + La période d\'essai est terminée, veuillez acheter la version complète + + Essai gratuit : %d jour restant + Essai gratuit : %d jours restants + Paramètres DAV Cette URL n\'est pas sécurisée. Vos données pourront être interceptées. Vous devriez utiliser HTTPS. Permissions diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3246b88..678f58c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,8 @@ Пожалуйста предоставьте разрешения для синхронизации. Синхронизация неуспешна + Синхронизация остановлена + Пожалуйста, купите полную версию, чтобы разрешить синхронизацию Настройки Скрыть пароль Пароль @@ -24,6 +26,13 @@ Исправить… выполняется синхронизация + Испытательный период завершен, пожалуйста, купите полную версию + + Испытание: %d день остался + Испытание: %d дня осталось + Испытание: %d дней осталось + Испытание: %d дней осталось + Разрешения Для синхронизации ваших файлов нам необходим полный доступ к вашим файлам. Зайдите в настройки, чтобы включить разрешение Настройки файлов diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79ad368..c12c2b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ - Easy Sync 1001 Please grant permissions to allow synchronization Synchronization failed + Synchronization stopped + Please buy the full version to allow synchronization Settings Hide password Password @@ -26,6 +27,11 @@ Fix… running syncing + Trial is over, please buy the full version + + Trial: %d day remaining + Trial: %d days remaining + Permissions In order to synchronize your files we need full access to your files. Go to settings to enable the permission File settings