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