diff options
author | 2022-06-21 08:17:14 -0700 | |
---|---|---|
committer | 2022-07-12 11:39:43 -0700 | |
commit | 42af219be98f6c90fe315685ac34517b5c5a0cd8 (patch) | |
tree | 24358d7f0551ba97cd48432dcdfb6b2843c557cd | |
parent | 91079f446edae5e82a17cf7b439fb39d335cb74f (diff) |
[MultiUser] Add file access wrapper.
Adds a class that handles files access for multiple users. Will work on
integration in follow-up CL.
Test: Add unit tests.
Change-Id: Ia0af0d18970a8376edcccbe0f59e3d21e55c4f42
-rw-r--r-- | packages/SystemUI/docs/user-file-manager.md | 13 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt | 5 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java | 4 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/settings/UserFileManager.kt | 39 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt | 144 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java (renamed from packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java) | 17 | ||||
-rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt | 120 |
7 files changed, 335 insertions, 7 deletions
diff --git a/packages/SystemUI/docs/user-file-manager.md b/packages/SystemUI/docs/user-file-manager.md new file mode 100644 index 000000000000..64f1694af50a --- /dev/null +++ b/packages/SystemUI/docs/user-file-manager.md @@ -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. + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index a9f340854689..6db3e82a77b0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -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 -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 137e28888728..c8747fcf16fb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManager.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManager.kt new file mode 100644 index 000000000000..aa218dbfdd43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManager.kt @@ -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 +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt new file mode 100644 index 000000000000..8c8f54fe9c3d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt @@ -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) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java index 7084d3ffc9ff..2f62e44ba4c4 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java +++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt new file mode 100644 index 000000000000..73226fa0b12c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt @@ -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() + } +} |