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 com.android.internal.annotations.VisibleForTesting
25 import com.android.systemui.backup.BackupHelper
26 import java.io.File
27 import java.util.concurrent.Executor
28 import java.util.concurrent.TimeUnit
29 
30 /**
31  * Class to track the auxiliary persistence of controls.
32  *
33  * This file is a copy of the `controls_favorites.xml` file restored from a back up. It is used to
34  * keep track of controls that were restored but its corresponding app has not been installed yet.
35  */
36 class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
37     wrapper: ControlsFavoritePersistenceWrapper
38 ) {
39 
40     constructor(
41         file: File,
42         executor: Executor
43     ): this(ControlsFavoritePersistenceWrapper(file, executor))
44 
45     companion object {
46         const val AUXILIARY_FILE_NAME = "aux_controls_favorites.xml"
47     }
48 
49     private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper
50 
51     /**
52      * Access the current list of favorites as tracked by the auxiliary file
53      */
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      */
66     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      */
75     fun initialize() {
76         favorites = if (persistenceWrapper.fileExists) {
77             persistenceWrapper.readFavorites()
78         } else {
79             emptyList()
80         }
81     }
82 
83     /**
84      * Gets the list of favorite controls as persisted in the auxiliary file for a given component.
85      *
86      * When the favorites for that application are returned, they will be removed from the
87      * auxiliary file immediately, so they won't be retrieved again.
88      * @param componentName the name of the service that provided the controls
89      * @return a list of structures with favorites
90      */
91     fun getCachedFavoritesAndRemoveFor(componentName: ComponentName): List<StructureInfo> {
92         if (!persistenceWrapper.fileExists) {
93             return emptyList()
94         }
95         val (comp, noComp) = favorites.partition { it.componentName == componentName }
96         return comp.also {
97             favorites = noComp
98             if (favorites.isNotEmpty()) {
99                 persistenceWrapper.storeFavorites(noComp)
100             } else {
101                 persistenceWrapper.deleteFile()
102             }
103         }
104     }
105 
106     /**
107      * [JobService] to delete the auxiliary file after a week.
108      */
109     class DeletionJobService : JobService() {
110         companion object {
111             @VisibleForTesting
112             internal val DELETE_FILE_JOB_ID = 1000
113             private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
114             fun getJobForContext(context: Context): JobInfo {
115                 val jobId = DELETE_FILE_JOB_ID + context.userId
116                 val componentName = ComponentName(context, DeletionJobService::class.java)
117                 return JobInfo.Builder(jobId, componentName)
118                     .setMinimumLatency(WEEK_IN_MILLIS)
119                     .setPersisted(true)
120                     .build()
121             }
122         }
123 
124         @VisibleForTesting
125         fun attachContext(context: Context) {
126             attachBaseContext(context)
127         }
128 
129         override fun onStartJob(params: JobParameters): Boolean {
130             synchronized(BackupHelper.controlsDataLock) {
131                 baseContext.deleteFile(AUXILIARY_FILE_NAME)
132             }
133             return false
134         }
135 
136         override fun onStopJob(params: JobParameters?): Boolean {
137             return true // reschedule and try again if the job was stopped without completing
138         }
139     }
140 }