1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.settings
18 
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.content.SharedPreferences
24 import android.os.Environment
25 import android.os.UserHandle
26 import android.os.UserManager
27 import android.util.Log
28 import androidx.annotation.VisibleForTesting
29 import com.android.systemui.CoreStartable
30 import com.android.systemui.broadcast.BroadcastDispatcher
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.util.concurrency.DelayableExecutor
34 import java.io.File
35 import java.io.FilenameFilter
36 import javax.inject.Inject
37 
38 /**
39  * Implementation for retrieving file paths for file storage of system and secondary users. For
40  * non-system users, files will be prepended by a special prefix containing the user id.
41  */
42 @SysUISingleton
43 class UserFileManagerImpl
44 @Inject
45 constructor(
46     private val context: Context,
47     val userManager: UserManager,
48     val broadcastDispatcher: BroadcastDispatcher,
49     @Background val backgroundExecutor: DelayableExecutor
50 ) : UserFileManager, CoreStartable {
51     companion object {
52         private const val PREFIX = "__USER_"
53         private const val TAG = "UserFileManagerImpl"
54         const val ROOT_DIR = "UserFileManager"
55         const val FILES = "files"
56         const val SHARED_PREFS = "shared_prefs"
57 
58         /**
59          * Returns a File object with a relative path, built from the userId for non-system users
60          */
61         fun createFile(fileName: String, userId: Int): File {
62             return if (isSystemUser(userId)) {
63                 File(fileName)
64             } else {
65                 File(getFilePrefix(userId) + fileName)
66             }
67         }
68 
69         fun createLegacyFile(context: Context, dir: String, fileName: String, userId: Int): File? {
70             return if (isSystemUser(userId)) {
71                 null
72             } else {
73                 return Environment.buildPath(
74                     context.filesDir,
75                     ROOT_DIR,
76                     userId.toString(),
77                     dir,
78                     fileName
79                 )
80             }
81         }
82 
83         fun getFilePrefix(userId: Int): String {
84             return PREFIX + userId.toString() + "_"
85         }
86 
87         /** Returns `true` if the given user ID is that for the system user. */
88         private fun isSystemUser(userId: Int): Boolean {
89             return UserHandle(userId).isSystem
90         }
91     }
92 
93     private val broadcastReceiver =
94         object : BroadcastReceiver() {
95             /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */
96             override fun onReceive(context: Context, intent: Intent) {
97                 if (intent.action == Intent.ACTION_USER_REMOVED) {
98                     clearDeletedUserData()
99                 }
100             }
101         }
102 
103     /** Poll for user-specific directories to delete upon start up. */
104     override fun start() {
105         clearDeletedUserData()
106         val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) }
107         broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
108     }
109 
110     /**
111      * Return the file based on current user. Files for all users will exist in [context.filesDir],
112      * but non system user files will be prepended with [getFilePrefix].
113      */
114     override fun getFile(fileName: String, userId: Int): File {
115         val file = File(context.filesDir, createFile(fileName, userId).path)
116         createLegacyFile(context, FILES, fileName, userId)?.run { migrate(file, this) }
117         return file
118     }
119 
120     /**
121      * Get shared preferences from user. Files for all users will exist in the shared_prefs dir, but
122      * non system user files will be prepended with [getFilePrefix].
123      */
124     override fun getSharedPreferences(
125         fileName: String,
126         @Context.PreferencesMode mode: Int,
127         userId: Int
128     ): SharedPreferences {
129         val file = createFile(fileName, userId)
130         createLegacyFile(context, SHARED_PREFS, "$fileName.xml", userId)?.run {
131             val path = Environment.buildPath(context.dataDir, SHARED_PREFS, "${file.path}.xml")
132             migrate(path, this)
133         }
134         return context.getSharedPreferences(file.path, mode)
135     }
136 
137     /** Remove files for deleted users. */
138     @VisibleForTesting
139     internal fun clearDeletedUserData() {
140         backgroundExecutor.execute {
141             deleteFiles(context.filesDir)
142             deleteFiles(File(context.dataDir, SHARED_PREFS))
143         }
144     }
145 
146     private fun migrate(dest: File, source: File) {
147         if (source.exists()) {
148             try {
149                 val parent = source.getParentFile()
150                 source.renameTo(dest)
151 
152                 deleteParentDirsIfEmpty(parent)
153             } catch (e: Exception) {
154                 Log.e(TAG, "Failed to rename and delete ${source.path}", e)
155             }
156         }
157     }
158 
159     private fun deleteParentDirsIfEmpty(dir: File?) {
160         if (dir != null && dir.listFiles().size == 0) {
161             val priorParent = dir.parentFile
162             val isRoot = dir.name == ROOT_DIR
163             dir.delete()
164 
165             if (!isRoot) {
166                 deleteParentDirsIfEmpty(priorParent)
167             }
168         }
169     }
170 
171     private fun deleteFiles(parent: File) {
172         val aliveUserFilePrefix = userManager.aliveUsers.map { getFilePrefix(it.id) }
173         val filesToDelete =
174             parent.listFiles(
175                 FilenameFilter { _, name ->
176                     name.startsWith(PREFIX) &&
177                         aliveUserFilePrefix.filter { name.startsWith(it) }.isEmpty()
178                 }
179             )
180 
181         // This can happen in test environments
182         if (filesToDelete == null) {
183             Log.i(TAG, "Empty directory: ${parent.path}")
184         } else {
185             filesToDelete.forEach { file ->
186                 Log.i(TAG, "Deleting file: ${file.path}")
187                 try {
188                     file.delete()
189                 } catch (e: Exception) {
190                     Log.e(TAG, "Deletion failed.", e)
191                 }
192             }
193         }
194     }
195 }
196