diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42e1a1b..f5118e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -75,6 +75,15 @@ android:exported="false" android:name=".ui.manipulation.UnlockAfterManipulationActivity" /> + + diff --git a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt index 21b6227..2464e0e 100644 --- a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt +++ b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt @@ -192,4 +192,6 @@ object HintsToShow { object ExperimentalFlags { const val DISABLE_BLOCK_ON_MANIPULATION = 1L const val SYSTEM_LEVEL_BLOCKING = 2L + const val MANIPULATION_ANNOY_USER_ONLY = 4L + const val MANIPULATION_ANNOY_USER = MANIPULATION_ANNOY_USER_ONLY or DISABLE_BLOCK_ON_MANIPULATION // otherwise there would be a conflict between both features } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt index ae56176..70b8a6f 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt @@ -42,6 +42,7 @@ abstract class PlatformIntegration( abstract fun showOverlayMessage(text: String) abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) + abstract fun showAnnoyScreen(annoyDuration: Long) abstract fun muteAudioIfPossible(packageName: String) abstract fun setShowBlockingOverlay(show: Boolean) // this should throw an SecurityException if the permission is missing diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt index 53d2b98..33c75b7 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt @@ -46,6 +46,7 @@ import io.timelimit.android.data.model.AppActivity import io.timelimit.android.integration.platform.* import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper import io.timelimit.android.ui.lock.LockActivity +import io.timelimit.android.ui.manipulation.AnnoyActivity import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.delay @@ -225,6 +226,10 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio LockActivity.start(context, currentPackageName, currentActivityName) } + override fun showAnnoyScreen(annoyDuration: Long) { + AnnoyActivity.start(context, annoyDuration) + } + override fun muteAudioIfPossible(packageName: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) { diff --git a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt index fc8296f..cb940eb 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt @@ -87,6 +87,10 @@ class DummyIntegration( launchLockScreenForPackage = currentPackageName } + override fun showAnnoyScreen(annoyDuration: Long) { + // ignore + } + override fun muteAudioIfPossible(packageName: String) { // ignore } diff --git a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt index b839ce3..b776549 100644 --- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt @@ -19,6 +19,7 @@ import android.util.Log import android.util.SparseArray import android.util.SparseLongArray import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.timelimit.android.BuildConfig import io.timelimit.android.R import io.timelimit.android.async.Threads @@ -37,6 +38,7 @@ import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.livedata.* import io.timelimit.android.sync.actions.UpdateDeviceStatusAction import io.timelimit.android.sync.actions.apply.ApplyActionUtil +import io.timelimit.android.ui.IsAppInForeground import io.timelimit.android.util.AndroidVersion import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.work.PeriodicSyncInBackgroundWorker @@ -67,6 +69,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { runAsyncExpectForever { backgroundServiceLoop() } runAsyncExpectForever { syncDeviceStatusLoop() } runAsyncExpectForever { backupDatabaseLoop() } + runAsyncExpectForever { annoyUserOnManipulationLoop() } runAsync { // this is effective after an reboot @@ -691,4 +694,66 @@ class BackgroundTaskLogic(val appLogic: AppLogic) { appLogic.timeApi.sleep(1000 * 60 * 60 * 3 /* 3 hours */) } } + + // first time: annoy for 20 seconds; free for 5 minutes + // second time: annoy for 30 seconds; free for 2 minutes + // third time: annoy for 1 minute; free for 1 minute + // then: annoy for 2 minutes; free for 1 minute + private suspend fun annoyUserOnManipulationLoop() { + val isManipulated = appLogic.deviceEntryIfEnabled.map { it?.hasActiveManipulationWarning ?: false } + val enableAnnoy = appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.MANIPULATION_ANNOY_USER) + val timeLimitNotActive = IsAppInForeground.isRunning.invert() + + var counter = 0 + var globalCounter = 0 + + val shouldAnnoyNow = isManipulated.and(enableAnnoy).and(timeLimitNotActive) + + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "delay before enabling annoying") + } + + delay(1000 * 15) + + while (true) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "wait until should annoy") + } + + shouldAnnoyNow.waitUntilValueMatches { it == true } + + val annoyDurationInSeconds = when (counter) { + 0 -> 20 + 1 -> 30 + 2 -> 60 + else -> 120 + } + + val freeDurationInSeconds = when (counter) { + 0 -> 5 * 60 + 1 -> 2 * 60 + else -> 60 + } + + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "annoy for $annoyDurationInSeconds seconds; free for $freeDurationInSeconds seconds") + } + + appLogic.platformIntegration.showAnnoyScreen(annoyDurationInSeconds.toLong()) + + counter++ + globalCounter++ + + // reset counter if there was nothing for one hour + val globalCounterBackup = globalCounter + appLogic.timeApi.runDelayed(Runnable { + if (globalCounter == globalCounterBackup) { + counter = 0 + } + }, 1000 * 60 * 60 /* 1 hour */) + + // wait before annoying next time + delay((annoyDurationInSeconds + freeDurationInSeconds) * 1000L) + } + } } diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt index ddc292a..cdb92e1 100644 --- a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt @@ -54,7 +54,6 @@ class DiagnoseExperimentalFlagFragment : Fragment() { val checkboxes = flags.map { CheckBox(context).apply { setText(it.label) - isEnabled = it.enable } } @@ -63,15 +62,20 @@ class DiagnoseExperimentalFlagFragment : Fragment() { database.config().experimentalFlags.observe(this, Observer { setFlags -> flags.forEachIndexed { index, flag -> val checkbox = checkboxes[index] - val isFlagSet = (setFlags and flag.flag) == flag.flag + val isFlagSet = (setFlags and flag.enableFlags) == flag.enableFlags checkbox.setOnCheckedChangeListener { _, _ -> } checkbox.isChecked = isFlagSet + checkbox.isEnabled = flag.enable(setFlags) checkbox.setOnCheckedChangeListener { _, didCheck -> if (didCheck != isFlagSet) { if (auth.requestAuthenticationOrReturnTrue()) { Threads.database.execute { - database.config().setExperimentalFlag(flag.flag, didCheck) + if (didCheck) { + database.config().setExperimentalFlag(flag.enableFlags, true) + } else { + database.config().setExperimentalFlag(flag.disableFlags, false) + } } } else { checkbox.isChecked = isFlagSet @@ -87,20 +91,31 @@ class DiagnoseExperimentalFlagFragment : Fragment() { data class DiagnoseExperimentalFlagItem( val label: Int, - val flag: Long, - val enable: Boolean + val enableFlags: Long, + val disableFlags: Long, + val enable: (flags: Long) -> Boolean ) { companion object { val items = listOf( DiagnoseExperimentalFlagItem( label = R.string.diagnose_exf_lom, - flag = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION, - enable = !BuildConfig.storeCompilant + enableFlags = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION, + disableFlags = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION, + enable = { flags -> + (!BuildConfig.storeCompilant) and ((flags and ExperimentalFlags.MANIPULATION_ANNOY_USER_ONLY) == 0L) + } ), DiagnoseExperimentalFlagItem( label = R.string.diagnose_exf_slb, - flag = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING, - enable = !BuildConfig.storeCompilant + enableFlags = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING, + disableFlags = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING, + enable = { !BuildConfig.storeCompilant } + ), + DiagnoseExperimentalFlagItem( + label = R.string.diagnose_exf_mau, + enableFlags = ExperimentalFlags.MANIPULATION_ANNOY_USER, + disableFlags = ExperimentalFlags.MANIPULATION_ANNOY_USER_ONLY, + enable = { !BuildConfig.storeCompilant } ) ) } diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt new file mode 100644 index 0000000..e993d3f --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt @@ -0,0 +1,79 @@ +/* + * TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manipulation + +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import io.timelimit.android.R +import io.timelimit.android.util.TimeTextUtil +import kotlinx.android.synthetic.main.annoy_activity.* + +class AnnoyActivity : AppCompatActivity() { + companion object { + private const val EXTRA_DURATION = "duration" + + fun start(context: Context, duration: Long) { + context.startActivity( + Intent(context, AnnoyActivity::class.java) + .putExtra(EXTRA_DURATION, duration) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val duration = intent!!.getLongExtra(EXTRA_DURATION, 10) + val model = ViewModelProviders.of(this).get(AnnoyModel::class.java) + + setContentView(R.layout.annoy_activity) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startLockTask() + } + + model.init(duration = duration) + model.countdown.observe(this, Observer { + if (it == 0L) { + shutdown() + } + + annoy_timer.setText( + getString(R.string.annoy_timer, TimeTextUtil.seconds(it.toInt(), this@AnnoyActivity)) + ) + }) + } + + private fun shutdown() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + stopLockTask() + finish() + } + } + + override fun onBackPressed() { + // super.onBackPressed() + // just ignore it + } +} diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyModel.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyModel.kt new file mode 100644 index 0000000..4bf24db --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyModel.kt @@ -0,0 +1,48 @@ +/* + * TimeLimit Copyright 2019 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manipulation + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import io.timelimit.android.coroutines.runAsync +import io.timelimit.android.livedata.castDown +import kotlinx.coroutines.delay + +class AnnoyModel: ViewModel() { + private val countdownInternal = MutableLiveData() + private var hadInit = false + + val countdown = countdownInternal.castDown() + + fun init(duration: Long) { + if (!hadInit) { + hadInit = true + + countdownInternal.value = duration + + runAsync { + var timer = duration + + while (timer >= 0) { + delay(1000) + timer-- + + countdownInternal.value = timer + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/annoy_activity.xml b/app/src/main/res/layout/annoy_activity.xml new file mode 100644 index 0000000..fa65b4b --- /dev/null +++ b/app/src/main/res/layout/annoy_activity.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/values-de/strings-annoy.xml b/app/src/main/res/values-de/strings-annoy.xml new file mode 100644 index 0000000..c158f85 --- /dev/null +++ b/app/src/main/res/values-de/strings-annoy.xml @@ -0,0 +1,23 @@ + + + + + TimeLimit wurde manipuliert. Deshalb wird das Gerät für einen Moment gesperrt. + + + Das Gerät wird in %s entsperrt. + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings-diagnose.xml b/app/src/main/res/values-de/strings-diagnose.xml index 20eda08..faed03d 100644 --- a/app/src/main/res/values-de/strings-diagnose.xml +++ b/app/src/main/res/values-de/strings-diagnose.xml @@ -55,4 +55,5 @@ Experimentelle Parameter Sperrung nach Manipulationen deaktivieren Apps auf Systemebene sperren + Nutzer nach Manipulation nerven diff --git a/app/src/main/res/values/strings-annoy.xml b/app/src/main/res/values/strings-annoy.xml new file mode 100644 index 0000000..c329c29 --- /dev/null +++ b/app/src/main/res/values/strings-annoy.xml @@ -0,0 +1,23 @@ + + + + + There was a manipulation of TimeLimit. Due to that, this device is locked for a moment. + + + The device will be unlocked in %s. + + \ No newline at end of file diff --git a/app/src/main/res/values/strings-diagnose.xml b/app/src/main/res/values/strings-diagnose.xml index b1a0e14..0f8c4d5 100644 --- a/app/src/main/res/values/strings-diagnose.xml +++ b/app/src/main/res/values/strings-diagnose.xml @@ -55,4 +55,5 @@ Experimental flags Disable locking after manipulations Block Apps at system level + Annoy user after manipulation