1 /* 2 * Copyright (C) 2023 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 android.ext.services.common; 18 19 import android.annotation.TargetApi; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.os.Build; 26 import android.provider.DeviceConfig; 27 import android.util.Log; 28 29 import androidx.annotation.VisibleForTesting; 30 31 import java.io.File; 32 import java.util.function.ToIntBiFunction; 33 34 /** 35 * Handles the BootCompleted initialization for ExtServices APK on T+. 36 * <p> 37 * The BootCompleted receiver deletes files created by the AdServices code on S- that persist on 38 * disk after an OTA to T+. Once these files are deleted, this receiver disables itself. 39 * <p> 40 * Since this receiver disables itself after the first run, it will not be re-run after any code 41 * changes to this class. In order to re-enable this receiver and run the updated code, the simplest 42 * way is to rename the class every upon every module release that changes the code. Also, in order 43 * to protect against accidental name re-use, the {@code testReceiverDoesNotReuseClassNames} unit 44 * test tracking used names should be updated upon each rename as well. 45 */ 46 public class AdServicesFilesCleanupBootCompleteReceiver extends BroadcastReceiver { 47 private static final String TAG = "extservices"; 48 private static final String KEY_RECEIVER_ENABLED = 49 "extservices_adservices_data_cleanup_enabled"; 50 51 // All files created by the AdServices code within ExtServices should have this prefix. 52 private static final String ADSERVICES_PREFIX = "adservices"; 53 54 @TargetApi(Build.VERSION_CODES.TIRAMISU) // Receiver disabled in manifest for S- devices 55 @SuppressWarnings("ReturnValueIgnored") // Intentionally ignoring return value of Log.d/Log.e 56 @Override onReceive(Context context, Intent intent)57 public void onReceive(Context context, Intent intent) { 58 Log.i(TAG, "AdServices files cleanup receiver received BOOT_COMPLETED broadcast for user " 59 + context.getUser().getIdentifier()); 60 61 // Check if the feature flag is enabled, otherwise exit without doing anything. 62 if (!isReceiverEnabled()) { 63 Log.d(TAG, "AdServices files cleanup receiver not enabled in config, exiting"); 64 return; 65 } 66 67 try { 68 // Look through and delete any files in the data dir that have the `adservices` prefix 69 boolean success = deleteAdServicesFiles(context.getDataDir()); 70 71 // Log as `d` or `e` depending on success or failure. 72 ToIntBiFunction<String, String> function = success ? Log::d : Log::e; 73 function.applyAsInt(TAG, 74 "AdServices files cleanup receiver data deletion success: " + success); 75 76 scheduleAppsearchDeleteJob(context); 77 } finally { 78 unregisterSelf(context); 79 } 80 } 81 unregisterSelf(Context context)82 private void unregisterSelf(Context context) { 83 context.getPackageManager().setComponentEnabledSetting( 84 new ComponentName(context, this.getClass()), 85 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 86 /* flags= */ 0); 87 Log.d(TAG, "Disabled AdServices files cleanup receiver"); 88 } 89 90 @VisibleForTesting isReceiverEnabled()91 public boolean isReceiverEnabled() { 92 return DeviceConfig.getBoolean( 93 DeviceConfig.NAMESPACE_ADSERVICES, 94 /* name= */ KEY_RECEIVER_ENABLED, 95 /* defaultValue= */ true); 96 } 97 98 /** 99 * Recursively delete all files with a prefix of "adservices" from the specified directory. 100 * <p> 101 * Note: It expects the input File object to be a directory and not a regular file. Also, 102 * it only deletes the contents of the input directory, and not the directory itself, even if 103 * the name of the directory starts with the prefix. 104 * 105 * @param currentDirectory the directory to scan for files 106 * @return {@code true} if all adservices files were successfully deleted; else {@code false}. 107 */ 108 @VisibleForTesting deleteAdServicesFiles(File currentDirectory)109 public boolean deleteAdServicesFiles(File currentDirectory) { 110 if (currentDirectory == null) { 111 Log.d(TAG, "Argument passed to deleteAdServicesFiles is null"); 112 return true; 113 } 114 115 try { 116 if (!currentDirectory.isDirectory()) { 117 Log.d(TAG, "Argument passed to deleteAdServicesFiles is not a directory"); 118 return true; 119 } 120 121 boolean allSuccess = true; 122 123 File[] files = currentDirectory.listFiles(); 124 for (File file : files) { 125 if (file.isDirectory()) { 126 // Delete ALL data if the directory name starts with the adservices prefix. 127 // Otherwise, delete any file in the subtree that starts with the prefix. 128 if (doesFileNameStartWithPrefix(file)) { 129 // Directory starting with adservices, so delete everything inside it. 130 allSuccess = deleteAllData(file) && allSuccess; 131 } else { 132 // Directory but not starting with adservices, so only delete adservices 133 // files. 134 allSuccess = deleteAdServicesFiles(file) && allSuccess; 135 } 136 } else if (doesFileNameStartWithPrefix(file)) { 137 allSuccess = safeDelete(file) && allSuccess; 138 } 139 } 140 141 return allSuccess; 142 } catch (RuntimeException e) { 143 Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e); 144 return false; 145 } 146 } 147 doesFileNameStartWithPrefix(File file)148 private boolean doesFileNameStartWithPrefix(File file) { 149 // Do a case-insensitive comparison 150 return ADSERVICES_PREFIX.regionMatches( 151 /* ignoreCase= */ true, 152 /* toOffset= */ 0, 153 file.getName(), 154 /* ooffset= */ 0, 155 /* len= */ ADSERVICES_PREFIX.length()); 156 } 157 deleteAllData(File currentDirectory)158 private boolean deleteAllData(File currentDirectory) { 159 if (currentDirectory == null) { 160 Log.d(TAG, "Argument passed to deleteAllData is null"); 161 return true; 162 } 163 164 try { 165 if (!currentDirectory.isDirectory()) { 166 Log.d(TAG, "Argument passed to deleteAllData is not a directory"); 167 return true; 168 } 169 170 boolean allSuccess = true; 171 172 for (File file : currentDirectory.listFiles()) { 173 allSuccess = (file.isDirectory() ? deleteAllData(file) : safeDelete(file)) 174 && allSuccess; 175 } 176 177 // If deleting the entire subdirectory has been successful, then (and only then) delete 178 // the current directory. 179 allSuccess = allSuccess && safeDelete(currentDirectory); 180 181 return allSuccess; 182 } catch (RuntimeException e) { 183 Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e); 184 return false; 185 } 186 } 187 safeDelete(File file)188 private boolean safeDelete(File file) { 189 try { 190 return file.delete(); 191 } catch (RuntimeException e) { 192 String message = String.format( 193 "AdServices files cleanup receiver: Error deleting %s - %s", file.getName(), 194 e.getMessage()); 195 Log.e(TAG, message, e); 196 return false; 197 } 198 } 199 200 /** 201 * Schedules background periodic job AdservicesAppsearchDeleteJob 202 * to delete Appsearch data after OTA and data migration 203 * 204 * @param context the android context 205 **/ 206 @VisibleForTesting scheduleAppsearchDeleteJob(Context context)207 public void scheduleAppsearchDeleteJob(Context context) { 208 AdServicesAppsearchDeleteJob 209 .scheduleAdServicesAppsearchDeletePeriodicJob(context, 210 new AdservicesPhFlags()); 211 } 212 } 213