Merge "[MultiUser] Add file access wrapper." into tm-qpr-dev am: 1ff91194a8

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19014406

Change-Id: I79e542a8558046555b57b2d816b627d12a2a07ca
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Aaron Liu
2022-07-14 16:28:25 +00:00
committed by Automerger Merge Worker
7 changed files with 335 additions and 7 deletions

View File

@@ -0,0 +1,13 @@
# UserFileManager
This class is used to generate file paths and SharedPreferences that is compatible for multiple
users in SystemUI. Due to constraints in SystemUI, we can only read/write files as the system user.
Therefore, for secondary users, we want to store secondary user specific files into the system user
directory.
## Handling User Removal
This class will listen for Intent.ACTION_USER_REMOVED and remove directories that no longer
corresponding to active users. Additionally, upon start up, the class will run the same query for
deletion to ensure that there is no stale data.

View File

@@ -33,6 +33,7 @@ import com.android.systemui.log.SessionTracker
import com.android.systemui.media.RingtonePlayer
import com.android.systemui.power.PowerUI
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
@@ -51,7 +52,7 @@ import dagger.multibindings.IntoMap
/**
* Collection of {@link CoreStartable}s that should be run on AOSP.
*/
@Module
@Module(includes = [MultiUserUtilsModule::class])
abstract class SystemUICoreStartableModule {
/** Inject into AuthController. */
@Binds
@@ -205,4 +206,4 @@ abstract class SystemUICoreStartableModule {
@IntoMap
@ClassKey(KeyguardLiftController::class)
abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
}
}

View File

@@ -50,7 +50,7 @@ import com.android.systemui.plugins.BcSmartspaceDataPlugin;
import com.android.systemui.privacy.PrivacyModule;
import com.android.systemui.recents.Recents;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.settings.dagger.SettingsModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.smartspace.dagger.SmartspaceModule;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -127,7 +127,7 @@ import dagger.Provides;
QsFrameTranslateModule.class,
ScreenshotModule.class,
SensorModule.class,
SettingsModule.class,
MultiUserUtilsModule.class,
SettingsUtilModule.class,
SmartRepliesInflationModule.class,
SmartspaceModule.class,

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2022 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.systemui.settings
import android.content.Context
import android.content.SharedPreferences
import java.io.File
/**
* Interface for retrieving file paths for file storage of system and secondary users.
*/
interface UserFileManager {
/**
* Return the file based on current user.
*/
fun getFile(fileName: String, userId: Int): File
/**
* Get shared preferences from user.
*/
fun getSharedPreferences(
fileName: String,
@Context.PreferencesMode mode: Int,
userId: Int
): SharedPreferences
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2022 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.systemui.settings
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.os.Environment
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.CoreStartable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.File
import javax.inject.Inject
/**
* Implementation for retrieving file paths for file storage of system and secondary users.
* Files lie in {File Directory}/UserFileManager/{User Id} for secondary user.
* For system user, we use the conventional {File Directory}
*/
@SysUISingleton
class UserFileManagerImpl @Inject constructor(
// Context of system process and system user.
val context: Context,
val userManager: UserManager,
val broadcastDispatcher: BroadcastDispatcher,
@Background val backgroundExecutor: DelayableExecutor
) : UserFileManager, CoreStartable(context) {
companion object {
private const val FILES = "files"
private const val SHARED_PREFS = "shared_prefs"
internal const val ID = "UserFileManager"
}
private val broadcastReceiver = object : BroadcastReceiver() {
/**
* Listen to Intent.ACTION_USER_REMOVED to clear user data.
*/
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_USER_REMOVED) {
clearDeletedUserData()
}
}
}
/**
* Poll for user-specific directories to delete upon start up.
*/
override fun start() {
clearDeletedUserData()
val filter = IntentFilter().apply {
addAction(Intent.ACTION_USER_REMOVED)
}
broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
}
/**
* Return the file based on current user.
*/
override fun getFile(fileName: String, userId: Int): File {
return if (UserHandle(userId).isSystem) {
Environment.buildPath(
context.filesDir,
fileName
)
} else {
Environment.buildPath(
context.filesDir,
ID,
userId.toString(),
FILES,
fileName
)
}
}
/**
* Get shared preferences from user.
*/
override fun getSharedPreferences(
fileName: String,
@Context.PreferencesMode mode: Int,
userId: Int
): SharedPreferences {
if (UserHandle(userId).isSystem) {
return context.getSharedPreferences(fileName, mode)
}
val secondaryUserDir = Environment.buildPath(
context.filesDir,
ID,
userId.toString(),
SHARED_PREFS,
fileName
)
return context.getSharedPreferences(secondaryUserDir, mode)
}
/**
* Remove dirs for deleted users.
*/
@VisibleForTesting
internal fun clearDeletedUserData() {
backgroundExecutor.execute {
val file = Environment.buildPath(context.filesDir, ID)
if (!file.exists()) return@execute
val aliveUsers = userManager.aliveUsers.map { it.id.toString() }
val dirsToDelete = file.list().filter { !aliveUsers.contains(it) }
dirsToDelete.forEach { dir ->
try {
val dirToDelete = Environment.buildPath(
file,
dir,
)
dirToDelete.deleteRecursively()
} catch (e: Exception) {
Log.e(ID, "Deletion failed.", e)
}
}
}
}
}

View File

@@ -21,25 +21,28 @@ import android.content.Context;
import android.os.Handler;
import android.os.UserManager;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.settings.UserContentResolverProvider;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserFileManager;
import com.android.systemui.settings.UserFileManagerImpl;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.settings.UserTrackerImpl;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
/**
* Dagger Module for classes found within the com.android.systemui.settings package.
*/
@Module
public abstract class SettingsModule {
public abstract class MultiUserUtilsModule {
@Binds
@SysUISingleton
abstract UserContextProvider bindUserContextProvider(UserTracker tracker);
@@ -62,4 +65,12 @@ public abstract class SettingsModule {
tracker.initialize(startingUser);
return tracker;
}
@Binds
@IntoMap
@ClassKey(UserFileManagerImpl.class)
abstract CoreStartable bindUserFileManagerCoreStartable(UserFileManagerImpl sysui);
@Binds
abstract UserFileManager bindUserFileManager(UserFileManagerImpl impl);
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2022 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.systemui.settings
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import android.os.Environment
import android.os.UserManager
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.isNull
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class UserFileManagerImplTest : SysuiTestCase() {
companion object {
const val TEST_FILE_NAME = "abc.txt"
}
lateinit var userFileManager: UserFileManagerImpl
lateinit var backgroundExecutor: FakeExecutor
@Mock
lateinit var userManager: UserManager
@Mock
lateinit var broadcastDispatcher: BroadcastDispatcher
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
backgroundExecutor = FakeExecutor(FakeSystemClock())
userFileManager = UserFileManagerImpl(context, userManager,
broadcastDispatcher, backgroundExecutor)
}
@Test
fun testGetFile() {
assertThat(userFileManager.getFile(TEST_FILE_NAME, 0).path)
.isEqualTo("${context.filesDir}/$TEST_FILE_NAME")
assertThat(userFileManager.getFile(TEST_FILE_NAME, 11).path)
.isEqualTo("${context.filesDir}/${UserFileManagerImpl.ID}/11/files/$TEST_FILE_NAME")
}
@Test
fun testGetSharedPreferences() {
assertThat(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0))
.isNotEqualTo(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11))
}
@Test
fun testUserFileManagerStart() {
val userFileManager = spy(userFileManager)
userFileManager.start()
verify(userFileManager).clearDeletedUserData()
verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java),
any(IntentFilter::class.java),
any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull())
}
@Test
fun testClearDeletedUserData() {
val dir = Environment.buildPath(
context.filesDir,
UserFileManagerImpl.ID,
"11",
"files"
)
dir.mkdirs()
val file = Environment.buildPath(
context.filesDir,
UserFileManagerImpl.ID,
"11",
"files",
TEST_FILE_NAME
)
val secondaryUserDir = Environment.buildPath(
context.filesDir,
UserFileManagerImpl.ID,
"11",
)
file.createNewFile()
assertThat(secondaryUserDir.exists()).isTrue()
assertThat(file.exists()).isTrue()
userFileManager.clearDeletedUserData()
assertThat(backgroundExecutor.runAllReady()).isGreaterThan(0)
verify(userManager).aliveUsers
assertThat(secondaryUserDir.exists()).isFalse()
assertThat(file.exists()).isFalse()
dir.deleteRecursively()
}
}