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.media.RingtonePlayer
|
||||||
import com.android.systemui.power.PowerUI
|
import com.android.systemui.power.PowerUI
|
||||||
import com.android.systemui.recents.Recents
|
import com.android.systemui.recents.Recents
|
||||||
|
import com.android.systemui.settings.dagger.MultiUserUtilsModule
|
||||||
import com.android.systemui.shortcut.ShortcutKeyDispatcher
|
import com.android.systemui.shortcut.ShortcutKeyDispatcher
|
||||||
import com.android.systemui.statusbar.notification.InstantAppNotifier
|
import com.android.systemui.statusbar.notification.InstantAppNotifier
|
||||||
import com.android.systemui.statusbar.phone.KeyguardLiftController
|
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.
|
* Collection of {@link CoreStartable}s that should be run on AOSP.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module(includes = [MultiUserUtilsModule::class])
|
||||||
abstract class SystemUICoreStartableModule {
|
abstract class SystemUICoreStartableModule {
|
||||||
/** Inject into AuthController. */
|
/** Inject into AuthController. */
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
|||||||
import com.android.systemui.privacy.PrivacyModule;
|
import com.android.systemui.privacy.PrivacyModule;
|
||||||
import com.android.systemui.recents.Recents;
|
import com.android.systemui.recents.Recents;
|
||||||
import com.android.systemui.screenshot.dagger.ScreenshotModule;
|
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.smartspace.dagger.SmartspaceModule;
|
||||||
import com.android.systemui.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.CommandQueue;
|
||||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
|
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
|
||||||
@@ -127,7 +127,7 @@ import dagger.Provides;
|
|||||||
QsFrameTranslateModule.class,
|
QsFrameTranslateModule.class,
|
||||||
ScreenshotModule.class,
|
ScreenshotModule.class,
|
||||||
SensorModule.class,
|
SensorModule.class,
|
||||||
SettingsModule.class,
|
MultiUserUtilsModule.class,
|
||||||
SettingsUtilModule.class,
|
SettingsUtilModule.class,
|
||||||
SmartRepliesInflationModule.class,
|
SmartRepliesInflationModule.class,
|
||||||
SmartspaceModule.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.Handler;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
|
|
||||||
|
import com.android.systemui.CoreStartable;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
import com.android.systemui.dagger.qualifiers.Background;
|
||||||
import com.android.systemui.dump.DumpManager;
|
import com.android.systemui.dump.DumpManager;
|
||||||
import com.android.systemui.settings.UserContentResolverProvider;
|
import com.android.systemui.settings.UserContentResolverProvider;
|
||||||
import com.android.systemui.settings.UserContextProvider;
|
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.UserTracker;
|
||||||
import com.android.systemui.settings.UserTrackerImpl;
|
import com.android.systemui.settings.UserTrackerImpl;
|
||||||
|
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.ClassKey;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dagger Module for classes found within the com.android.systemui.settings package.
|
* Dagger Module for classes found within the com.android.systemui.settings package.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
public abstract class SettingsModule {
|
public abstract class MultiUserUtilsModule {
|
||||||
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
abstract UserContextProvider bindUserContextProvider(UserTracker tracker);
|
abstract UserContextProvider bindUserContextProvider(UserTracker tracker);
|
||||||
@@ -62,4 +65,12 @@ public abstract class SettingsModule {
|
|||||||
tracker.initialize(startingUser);
|
tracker.initialize(startingUser);
|
||||||
return tracker;
|
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