blob: d0913d29f5044e03fc56ef989e7392726e351c04 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.permission.access
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.os.SystemClock
import android.os.UserHandle
import android.util.AtomicFile
import android.util.Slog
import android.util.SparseLongArray
import com.android.internal.annotations.GuardedBy
import com.android.internal.os.BackgroundThread
import com.android.modules.utils.BinaryXmlPullParser
import com.android.modules.utils.BinaryXmlSerializer
import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.util.PermissionApex
import com.android.server.permission.access.util.parseBinaryXml
import com.android.server.permission.access.util.readWithReserveCopy
import com.android.server.permission.access.util.serializeBinaryXml
import com.android.server.permission.access.util.writeWithReserveCopy
import java.io.File
import java.io.FileNotFoundException
class AccessPersistence(private val policy: AccessPolicy) {
private val scheduleLock = Any()
@GuardedBy("scheduleLock") private val pendingMutationTimesMillis = SparseLongArray()
@GuardedBy("scheduleLock") private val pendingStates = MutableIntMap<AccessState>()
@GuardedBy("scheduleLock") private lateinit var writeHandler: WriteHandler
private val writeLock = Any()
fun initialize() {
writeHandler = WriteHandler(BackgroundThread.getHandler().looper)
}
/**
* Reads the state either from the disk or migrate legacy data when the data files are missing.
*/
fun read(state: MutableAccessState) {
readSystemState(state)
state.externalState.userIds.forEachIndexed { _, userId -> readUserState(state, userId) }
}
private fun readSystemState(state: MutableAccessState) {
val fileExists =
systemFile.parse {
// This is the canonical way to call an extension function in a different class.
// TODO(b/259469752): Use context receiver for this when it becomes stable.
with(policy) { parseSystemState(state) }
}
if (!fileExists) {
policy.migrateSystemState(state)
state.systemState.write(state, UserHandle.USER_ALL)
}
}
private fun readUserState(state: MutableAccessState, userId: Int) {
val fileExists =
getUserFile(userId).parse { with(policy) { parseUserState(state, userId) } }
if (!fileExists) {
policy.migrateUserState(state, userId)
state.userStates[userId]!!.write(state, userId)
}
}
/**
* @return {@code true} if the file is successfully read from the disk; {@code false} if the
* file doesn't exist yet.
*/
private inline fun File.parse(block: BinaryXmlPullParser.() -> Unit): Boolean =
try {
AtomicFile(this).readWithReserveCopy { it.parseBinaryXml(block) }
true
} catch (e: FileNotFoundException) {
Slog.i(LOG_TAG, "$this not found")
false
} catch (e: Exception) {
throw IllegalStateException("Failed to read $this", e)
}
fun write(state: AccessState) {
state.systemState.write(state, UserHandle.USER_ALL)
state.userStates.forEachIndexed { _, userId, userState -> userState.write(state, userId) }
}
private fun WritableState.write(state: AccessState, userId: Int) {
when (val writeMode = writeMode) {
WriteMode.NONE -> {}
WriteMode.ASYNCHRONOUS -> {
synchronized(scheduleLock) {
writeHandler.removeMessages(userId)
pendingStates[userId] = state
// SystemClock.uptimeMillis() is used in Handler.sendMessageDelayed().
val currentTimeMillis = SystemClock.uptimeMillis()
val pendingMutationTimeMillis =
pendingMutationTimesMillis.getOrPut(userId) { currentTimeMillis }
val currentDelayMillis = currentTimeMillis - pendingMutationTimeMillis
val message = writeHandler.obtainMessage(userId)
if (currentDelayMillis > MAX_WRITE_DELAY_MILLIS) {
message.sendToTarget()
} else {
val newDelayMillis =
WRITE_DELAY_TIME_MILLIS.coerceAtMost(
MAX_WRITE_DELAY_MILLIS - currentDelayMillis
)
writeHandler.sendMessageDelayed(message, newDelayMillis)
}
}
}
WriteMode.SYNCHRONOUS -> {
synchronized(scheduleLock) { pendingStates[userId] = state }
writePendingState(userId)
}
else -> error(writeMode)
}
}
private fun writePendingState(userId: Int) {
synchronized(writeLock) {
val state: AccessState?
synchronized(scheduleLock) {
pendingMutationTimesMillis -= userId
state = pendingStates.remove(userId)
writeHandler.removeMessages(userId)
}
if (state == null) {
return
}
if (userId == UserHandle.USER_ALL) {
writeSystemState(state)
} else {
writeUserState(state, userId)
}
}
}
private fun writeSystemState(state: AccessState) {
systemFile.serialize { with(policy) { serializeSystemState(state) } }
}
private fun writeUserState(state: AccessState, userId: Int) {
getUserFile(userId).serialize { with(policy) { serializeUserState(state, userId) } }
}
private inline fun File.serialize(block: BinaryXmlSerializer.() -> Unit) {
try {
AtomicFile(this).writeWithReserveCopy { it.serializeBinaryXml(block) }
} catch (e: Exception) {
Slog.e(LOG_TAG, "Failed to serialize $this", e)
}
}
private val systemFile: File
get() = File(PermissionApex.systemDataDirectory, FILE_NAME)
private fun getUserFile(userId: Int): File =
File(PermissionApex.getUserDataDirectory(userId), FILE_NAME)
companion object {
private val LOG_TAG = AccessPersistence::class.java.simpleName
private const val FILE_NAME = "access.abx"
private const val WRITE_DELAY_TIME_MILLIS = 1000L
private const val MAX_WRITE_DELAY_MILLIS = 2000L
}
private inner class WriteHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(message: Message) {
val userId = message.what
writePendingState(userId)
}
}
}