Add option to annoy user after manipulation

This commit is contained in:
Jonas Lochmann 2019-08-19 00:00:00 +00:00
parent 31c49c8195
commit 1ff5ab6575
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
14 changed files with 311 additions and 9 deletions

View file

@ -75,6 +75,15 @@
android:exported="false" android:exported="false"
android:name=".ui.manipulation.UnlockAfterManipulationActivity" /> android:name=".ui.manipulation.UnlockAfterManipulationActivity" />
<activity
tools:ignore="UnusedAttribute"
android:excludeFromRecents="true"
android:autoRemoveFromRecents="true"
android:taskAffinity=":annoy"
android:showOnLockScreen="true"
android:exported="false"
android:name=".ui.manipulation.AnnoyActivity" />
<!-- system integration --> <!-- system integration -->
<receiver android:name=".integration.platform.android.receiver.BootReceiver"> <receiver android:name=".integration.platform.android.receiver.BootReceiver">

View file

@ -192,4 +192,6 @@ object HintsToShow {
object ExperimentalFlags { object ExperimentalFlags {
const val DISABLE_BLOCK_ON_MANIPULATION = 1L const val DISABLE_BLOCK_ON_MANIPULATION = 1L
const val SYSTEM_LEVEL_BLOCKING = 2L 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
} }

View file

@ -42,6 +42,7 @@ abstract class PlatformIntegration(
abstract fun showOverlayMessage(text: String) abstract fun showOverlayMessage(text: String)
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
abstract fun showAnnoyScreen(annoyDuration: Long)
abstract fun muteAudioIfPossible(packageName: String) abstract fun muteAudioIfPossible(packageName: String)
abstract fun setShowBlockingOverlay(show: Boolean) abstract fun setShowBlockingOverlay(show: Boolean)
// this should throw an SecurityException if the permission is missing // this should throw an SecurityException if the permission is missing

View file

@ -46,6 +46,7 @@ import io.timelimit.android.data.model.AppActivity
import io.timelimit.android.integration.platform.* import io.timelimit.android.integration.platform.*
import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper
import io.timelimit.android.ui.lock.LockActivity import io.timelimit.android.ui.lock.LockActivity
import io.timelimit.android.ui.manipulation.AnnoyActivity
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -225,6 +226,10 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
LockActivity.start(context, currentPackageName, currentActivityName) LockActivity.start(context, currentPackageName, currentActivityName)
} }
override fun showAnnoyScreen(annoyDuration: Long) {
AnnoyActivity.start(context, annoyDuration)
}
override fun muteAudioIfPossible(packageName: String) { override fun muteAudioIfPossible(packageName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) { if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {

View file

@ -87,6 +87,10 @@ class DummyIntegration(
launchLockScreenForPackage = currentPackageName launchLockScreenForPackage = currentPackageName
} }
override fun showAnnoyScreen(annoyDuration: Long) {
// ignore
}
override fun muteAudioIfPossible(packageName: String) { override fun muteAudioIfPossible(packageName: String) {
// ignore // ignore
} }

View file

@ -19,6 +19,7 @@ import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseLongArray import android.util.SparseLongArray
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.async.Threads 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.livedata.*
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil 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.AndroidVersion
import io.timelimit.android.util.TimeTextUtil import io.timelimit.android.util.TimeTextUtil
import io.timelimit.android.work.PeriodicSyncInBackgroundWorker import io.timelimit.android.work.PeriodicSyncInBackgroundWorker
@ -67,6 +69,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
runAsyncExpectForever { backgroundServiceLoop() } runAsyncExpectForever { backgroundServiceLoop() }
runAsyncExpectForever { syncDeviceStatusLoop() } runAsyncExpectForever { syncDeviceStatusLoop() }
runAsyncExpectForever { backupDatabaseLoop() } runAsyncExpectForever { backupDatabaseLoop() }
runAsyncExpectForever { annoyUserOnManipulationLoop() }
runAsync { runAsync {
// this is effective after an reboot // this is effective after an reboot
@ -691,4 +694,66 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.timeApi.sleep(1000 * 60 * 60 * 3 /* 3 hours */) 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)
}
}
} }

View file

@ -54,7 +54,6 @@ class DiagnoseExperimentalFlagFragment : Fragment() {
val checkboxes = flags.map { val checkboxes = flags.map {
CheckBox(context).apply { CheckBox(context).apply {
setText(it.label) setText(it.label)
isEnabled = it.enable
} }
} }
@ -63,15 +62,20 @@ class DiagnoseExperimentalFlagFragment : Fragment() {
database.config().experimentalFlags.observe(this, Observer { setFlags -> database.config().experimentalFlags.observe(this, Observer { setFlags ->
flags.forEachIndexed { index, flag -> flags.forEachIndexed { index, flag ->
val checkbox = checkboxes[index] val checkbox = checkboxes[index]
val isFlagSet = (setFlags and flag.flag) == flag.flag val isFlagSet = (setFlags and flag.enableFlags) == flag.enableFlags
checkbox.setOnCheckedChangeListener { _, _ -> } checkbox.setOnCheckedChangeListener { _, _ -> }
checkbox.isChecked = isFlagSet checkbox.isChecked = isFlagSet
checkbox.isEnabled = flag.enable(setFlags)
checkbox.setOnCheckedChangeListener { _, didCheck -> checkbox.setOnCheckedChangeListener { _, didCheck ->
if (didCheck != isFlagSet) { if (didCheck != isFlagSet) {
if (auth.requestAuthenticationOrReturnTrue()) { if (auth.requestAuthenticationOrReturnTrue()) {
Threads.database.execute { 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 { } else {
checkbox.isChecked = isFlagSet checkbox.isChecked = isFlagSet
@ -87,20 +91,31 @@ class DiagnoseExperimentalFlagFragment : Fragment() {
data class DiagnoseExperimentalFlagItem( data class DiagnoseExperimentalFlagItem(
val label: Int, val label: Int,
val flag: Long, val enableFlags: Long,
val enable: Boolean val disableFlags: Long,
val enable: (flags: Long) -> Boolean
) { ) {
companion object { companion object {
val items = listOf( val items = listOf(
DiagnoseExperimentalFlagItem( DiagnoseExperimentalFlagItem(
label = R.string.diagnose_exf_lom, label = R.string.diagnose_exf_lom,
flag = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION, enableFlags = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION,
enable = !BuildConfig.storeCompilant disableFlags = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION,
enable = { flags ->
(!BuildConfig.storeCompilant) and ((flags and ExperimentalFlags.MANIPULATION_ANNOY_USER_ONLY) == 0L)
}
), ),
DiagnoseExperimentalFlagItem( DiagnoseExperimentalFlagItem(
label = R.string.diagnose_exf_slb, label = R.string.diagnose_exf_slb,
flag = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING, enableFlags = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING,
enable = !BuildConfig.storeCompilant 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 }
) )
) )
} }

View file

@ -0,0 +1,79 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -0,0 +1,48 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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<Long>()
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
}
}
}
}
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.timelimit.android.ui.manipulation.AnnoyActivity">
<TextView
android:gravity="center_horizontal"
android:layout_margin="16dp"
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/annoy_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:layout_margin="16dp"
android:gravity="center"
android:id="@+id/annoy_timer"
android:text="@string/annoy_timer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="annoy_info">
TimeLimit wurde manipuliert. Deshalb wird das Gerät für einen Moment gesperrt.
</string>
<string name="annoy_timer">
Das Gerät wird in %s entsperrt.
</string>
</resources>

View file

@ -55,4 +55,5 @@
<string name="diagnose_exf_title">Experimentelle Parameter</string> <string name="diagnose_exf_title">Experimentelle Parameter</string>
<string name="diagnose_exf_lom">Sperrung nach Manipulationen deaktivieren</string> <string name="diagnose_exf_lom">Sperrung nach Manipulationen deaktivieren</string>
<string name="diagnose_exf_slb">Apps auf Systemebene sperren</string> <string name="diagnose_exf_slb">Apps auf Systemebene sperren</string>
<string name="diagnose_exf_mau">Nutzer nach Manipulation nerven</string>
</resources> </resources>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="annoy_info">
There was a manipulation of TimeLimit. Due to that, this device is locked for a moment.
</string>
<string name="annoy_timer">
The device will be unlocked in %s.
</string>
</resources>

View file

@ -55,4 +55,5 @@
<string name="diagnose_exf_title">Experimental flags</string> <string name="diagnose_exf_title">Experimental flags</string>
<string name="diagnose_exf_lom">Disable locking after manipulations</string> <string name="diagnose_exf_lom">Disable locking after manipulations</string>
<string name="diagnose_exf_slb">Block Apps at system level</string> <string name="diagnose_exf_slb">Block Apps at system level</string>
<string name="diagnose_exf_mau">Annoy user after manipulation</string>
</resources> </resources>