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