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 }