blob: 311bac4653800ab2e664424e4bce3a5e49c1888c [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.ext.services.common;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.DeviceConfig;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.util.function.ToIntBiFunction;
/**
* Handles the BootCompleted initialization for ExtServices APK on T+.
* <p>
* The BootCompleted receiver deletes files created by the AdServices code on S- that persist on
* disk after an OTA to T+. Once these files are deleted, this receiver disables itself.
* <p>
* Since this receiver disables itself after the first run, it will not be re-run after any code
* changes to this class. In order to re-enable this receiver and run the updated code, the simplest
* way is to rename the class every upon every module release that changes the code. Also, in order
* to protect against accidental name re-use, the {@code testReceiverDoesNotReuseClassNames} unit
* test tracking used names should be updated upon each rename as well.
*/
public class AdServicesFilesCleanupBootCompleteReceiver extends BroadcastReceiver {
private static final String TAG = "extservices";
private static final String KEY_RECEIVER_ENABLED =
"extservices_adservices_data_cleanup_enabled";
// All files created by the AdServices code within ExtServices should have this prefix.
private static final String ADSERVICES_PREFIX = "adservices";
@TargetApi(Build.VERSION_CODES.TIRAMISU) // Receiver disabled in manifest for S- devices
@SuppressWarnings("ReturnValueIgnored") // Intentionally ignoring return value of Log.d/Log.e
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "AdServices files cleanup receiver received BOOT_COMPLETED broadcast for user "
+ context.getUser().getIdentifier());
// Check if the feature flag is enabled, otherwise exit without doing anything.
if (!isReceiverEnabled()) {
Log.d(TAG, "AdServices files cleanup receiver not enabled in config, exiting");
return;
}
try {
// Look through and delete any files in the data dir that have the `adservices` prefix
boolean success = deleteAdServicesFiles(context.getDataDir());
// Log as `d` or `e` depending on success or failure.
ToIntBiFunction<String, String> function = success ? Log::d : Log::e;
function.applyAsInt(TAG,
"AdServices files cleanup receiver data deletion success: " + success);
} finally {
unregisterSelf(context);
}
}
private void unregisterSelf(Context context) {
context.getPackageManager().setComponentEnabledSetting(
new ComponentName(context, this.getClass()),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
/* flags= */ 0);
Log.d(TAG, "Disabled AdServices files cleanup receiver");
}
@VisibleForTesting
public boolean isReceiverEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ADSERVICES,
/* name= */ KEY_RECEIVER_ENABLED,
/* defaultValue= */ true);
}
/**
* Recursively delete all files with a prefix of "adservices" from the specified directory.
* <p>
* Note: It expects the input File object to be a directory and not a regular file. Also,
* it only deletes the contents of the input directory, and not the directory itself, even if
* the name of the directory starts with the prefix.
*
* @param currentDirectory the directory to scan for files
* @return {@code true} if all adservices files were successfully deleted; else {@code false}.
*/
@VisibleForTesting
public boolean deleteAdServicesFiles(File currentDirectory) {
if (currentDirectory == null) {
Log.d(TAG, "Argument passed to deleteAdServicesFiles is null");
return true;
}
try {
if (!currentDirectory.isDirectory()) {
Log.d(TAG, "Argument passed to deleteAdServicesFiles is not a directory");
return true;
}
boolean allSuccess = true;
File[] files = currentDirectory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
// Delete ALL data if the directory name starts with the adservices prefix.
// Otherwise, delete any file in the subtree that starts with the prefix.
if (doesFileNameStartWithPrefix(file)) {
// Directory starting with adservices, so delete everything inside it.
allSuccess = deleteAllData(file) && allSuccess;
} else {
// Directory but not starting with adservices, so only delete adservices
// files.
allSuccess = deleteAdServicesFiles(file) && allSuccess;
}
} else if (doesFileNameStartWithPrefix(file)) {
allSuccess = safeDelete(file) && allSuccess;
}
}
return allSuccess;
} catch (RuntimeException e) {
Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e);
return false;
}
}
private boolean doesFileNameStartWithPrefix(File file) {
// Do a case-insensitive comparison
return ADSERVICES_PREFIX.regionMatches(
/* ignoreCase= */ true,
/* toOffset= */ 0,
file.getName(),
/* ooffset= */ 0,
/* len= */ ADSERVICES_PREFIX.length());
}
private boolean deleteAllData(File currentDirectory) {
if (currentDirectory == null) {
Log.d(TAG, "Argument passed to deleteAllData is null");
return true;
}
try {
if (!currentDirectory.isDirectory()) {
Log.d(TAG, "Argument passed to deleteAllData is not a directory");
return true;
}
boolean allSuccess = true;
for (File file : currentDirectory.listFiles()) {
allSuccess = (file.isDirectory() ? deleteAllData(file) : safeDelete(file))
&& allSuccess;
}
// If deleting the entire subdirectory has been successful, then (and only then) delete
// the current directory.
allSuccess = allSuccess && safeDelete(currentDirectory);
return allSuccess;
} catch (RuntimeException e) {
Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e);
return false;
}
}
private boolean safeDelete(File file) {
try {
return file.delete();
} catch (RuntimeException e) {
String message = String.format(
"AdServices files cleanup receiver: Error deleting %s - %s", file.getName(),
e.getMessage());
Log.e(TAG, message, e);
return false;
}
}
}