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:
13
packages/SystemUI/docs/user-file-manager.md
Normal file
13
packages/SystemUI/docs/user-file-manager.md
Normal 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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user