1 /*
2  * Copyright (C) 2020 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.controls.controller
18 
19 import android.app.job.JobInfo
20 import android.app.job.JobParameters
21 import android.app.job.JobService
22 import android.content.ComponentName
23 import android.content.Context
24 import android.os.PersistableBundle
25 import com.android.internal.annotations.VisibleForTesting
26 import com.android.systemui.backup.BackupHelper
27 import com.android.systemui.settings.UserFileManagerImpl
28 import java.io.File
29 import java.util.concurrent.Executor
30 import java.util.concurrent.TimeUnit
31 
32 /**
33  * Class to track the auxiliary persistence of controls.
34  *
35  * This file is a copy of the `controls_favorites.xml` file restored from a back up. It is used to
36  * keep track of controls that were restored but its corresponding app has not been installed yet.
37  */
38 class AuxiliaryPersistenceWrapper
39 @VisibleForTesting
40 internal constructor(wrapper: ControlsFavoritePersistenceWrapper) {
41 
42     constructor(
43         file: File,
44         executor: Executor
45     ) : this(ControlsFavoritePersistenceWrapper(file, executor))
46 
47     companion object {
48         const val AUXILIARY_FILE_NAME = "aux_controls_favorites.xml"
49     }
50 
51     private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper
52 
53     /** Access the current list of favorites as tracked by the auxiliary file */
54     var favorites: List<StructureInfo> = emptyList()
55         private set
56 
57     init {
58         initialize()
59     }
60 
61     /**
62      * Change the file that this class is tracking.
63      *
64      * This will reset [favorites].
65      */
changeFilenull66     fun changeFile(file: File) {
67         persistenceWrapper.changeFileAndBackupManager(file, null)
68         initialize()
69     }
70 
71     /**
72      * Initialize the list of favorites to the content of the auxiliary file. If the file does not
73      * exist, it will be initialized to an empty list.
74      */
initializenull75     fun initialize() {
76         favorites =
77             if (persistenceWrapper.fileExists) {
78                 persistenceWrapper.readFavorites()
79             } else {
80                 emptyList()
81             }
82     }
83 
84     /**
85      * Gets the list of favorite controls as persisted in the auxiliary file for a given component.
86      *
87      * When the favorites for that application are returned, they will be removed from the auxiliary
88      * file immediately, so they won't be retrieved again.
89      *
90      * @param componentName the name of the service that provided the controls
91      * @return a list of structures with favorites
92      */
getCachedFavoritesAndRemoveFornull93     fun getCachedFavoritesAndRemoveFor(componentName: ComponentName): List<StructureInfo> {
94         if (!persistenceWrapper.fileExists) {
95             return emptyList()
96         }
97         val (comp, noComp) = favorites.partition { it.componentName == componentName }
98         return comp.also {
99             favorites = noComp
100             if (favorites.isNotEmpty()) {
101                 persistenceWrapper.storeFavorites(noComp)
102             } else {
103                 persistenceWrapper.deleteFile()
104             }
105         }
106     }
107 
108     /** [JobService] to delete the auxiliary file after a week. */
109     class DeletionJobService : JobService() {
110         companion object {
111             @VisibleForTesting internal val DELETE_FILE_JOB_ID = 1000
112             @VisibleForTesting internal val USER = "USER"
113             private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
getJobForContextnull114             fun getJobForContext(context: Context, targetUserId: Int): JobInfo {
115                 val jobId = DELETE_FILE_JOB_ID + context.userId
116                 val componentName = ComponentName(context, DeletionJobService::class.java)
117                 val bundle = PersistableBundle().also { it.putInt(USER, targetUserId) }
118                 return JobInfo.Builder(jobId, componentName)
119                     .setMinimumLatency(WEEK_IN_MILLIS)
120                     .setPersisted(true)
121                     .setExtras(bundle)
122                     .build()
123             }
124         }
125 
126         @VisibleForTesting
attachContextnull127         fun attachContext(context: Context) {
128             attachBaseContext(context)
129         }
130 
onStartJobnull131         override fun onStartJob(params: JobParameters): Boolean {
132             val userId = params.getExtras()?.getInt(USER, 0) ?: 0
133             synchronized(BackupHelper.controlsDataLock) {
134                 val file =
135                     UserFileManagerImpl.createFile(
136                         userId = userId,
137                         fileName = AUXILIARY_FILE_NAME,
138                     )
139                 baseContext.deleteFile(file.getPath())
140             }
141             return false
142         }
143 
onStopJobnull144         override fun onStopJob(params: JobParameters?): Boolean {
145             return true // reschedule and try again if the job was stopped without completing
146         }
147     }
148 }
149