1 /* 2 * Copyright (C) 2022 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 package com.android.adservices.ui; 17 18 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 19 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DOWNLOADED_OTA_FILE_ERROR; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX; 24 25 import android.annotation.SuppressLint; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.content.res.loader.ResourcesLoader; 29 import android.content.res.loader.ResourcesProvider; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.os.ParcelFileDescriptor; 33 import android.util.ArrayMap; 34 35 import androidx.annotation.Nullable; 36 import androidx.annotation.RequiresApi; 37 38 import com.android.adservices.LogUtil; 39 import com.android.adservices.download.MobileDataDownloadFactory; 40 import com.android.adservices.errorlogging.ErrorLogUtil; 41 import com.android.adservices.service.FlagsFactory; 42 43 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest; 44 import com.google.android.libraries.mobiledatadownload.MobileDataDownload; 45 import com.google.mobiledatadownload.ClientConfigProto.ClientFile; 46 import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup; 47 48 import java.io.File; 49 import java.util.Map; 50 import java.util.concurrent.ExecutionException; 51 52 /** 53 * Manages OTA (over the air) Resources downloaded from MDD. This allows device to use updated OTA 54 * resources. Currently only strings are supported. 55 */ 56 @RequiresApi(Build.VERSION_CODES.S) 57 public class OTAResourcesManager { 58 // this value needs to be updated if bundled resources are updated 59 private static final long BUNDLED_RESOURCES_VERSION = 4265; 60 private static final long NO_OTA_RESOURCES_VERSION = -1; 61 private static final String FILE_GROUP_NAME = "ui-ota-strings"; 62 public static final String DOWNLOADED_OTA_FILE_ID = "resources.arsc"; 63 public static final String DOWNLOADED_OTA_APK_ID = "AdServicesOtaResourcesApp.apk"; 64 private static final ResourcesLoader OTAResourcesLoader = new ResourcesLoader(); 65 66 private static long sOTAResourcesVersion = NO_OTA_RESOURCES_VERSION; 67 68 /** 69 * If shouldRefresh, then create a new OTA {@link ResourcesLoader} from ARSC file on device. 70 * Checks if OTA version > bundled version: If true, then add OTAResourcesLoader to the current 71 * context's {@link Resources}. Else, do nothing. 72 * 73 * @param context {@link Context} 74 */ 75 @SuppressWarnings("AvoidStaticContext") // UX class applyOTAResources(Context context, boolean shouldRefresh)76 public static void applyOTAResources(Context context, boolean shouldRefresh) { 77 if (shouldRefresh || sOTAResourcesVersion == NO_OTA_RESOURCES_VERSION) { 78 refreshOTAResources(context.getApplicationContext()); 79 } 80 if (sOTAResourcesVersion > BUNDLED_RESOURCES_VERSION) { 81 context.getApplicationContext().getResources().addLoaders(OTAResourcesLoader); 82 } 83 } 84 85 @SuppressWarnings("AvoidStaticContext") // UX class refreshOTAResources(Context context)86 static void refreshOTAResources(Context context) { 87 LogUtil.d("createResourceLoaderFromMDDFiles called."); 88 Map<String, ClientFile> downloadedOTAFiles = getDownloadedFiles(); 89 90 // check if there are OTA Resources 91 if (downloadedOTAFiles == null || downloadedOTAFiles.size() == 0) { 92 return; 93 } 94 // get OTA strings file 95 File resourcesFile = getOtaFile(context, downloadedOTAFiles, DOWNLOADED_OTA_FILE_ID); 96 // get OTA resources apk 97 File resourcesApk = getOtaFile(context, downloadedOTAFiles, DOWNLOADED_OTA_APK_ID); 98 if (resourcesFile == null && resourcesApk == null) { 99 LogUtil.d("No OTA files"); 100 return; 101 } 102 103 // Clear previous ResourceProvider 104 OTAResourcesLoader.clearProviders(); 105 // Add new ResourceProvider created from arsc file 106 if (resourcesFile != null) { 107 try { 108 ParcelFileDescriptor fd = ParcelFileDescriptor.open(resourcesFile, MODE_READ_ONLY); 109 OTAResourcesLoader.addProvider(ResourcesProvider.loadFromTable(fd, null)); 110 fd.close(); 111 } catch (Exception e) { 112 LogUtil.e("Caught exception while adding OTA string provider: " + e.getMessage()); 113 ErrorLogUtil.e( 114 e, 115 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 116 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 117 OTAResourcesLoader.clearProviders(); 118 } 119 } 120 // Add new ResourceProvider created from OTA apk 121 else if (resourcesApk != null) { 122 try { 123 ParcelFileDescriptor fd = ParcelFileDescriptor.open(resourcesApk, MODE_READ_ONLY); 124 OTAResourcesLoader.addProvider(ResourcesProvider.loadFromApk(fd, null)); 125 fd.close(); 126 } catch (Exception e) { 127 LogUtil.e("Caught exception while adding OTA apk provider: " + e.getMessage()); 128 ErrorLogUtil.e( 129 e, 130 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 131 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 132 } 133 } 134 } 135 136 @SuppressWarnings("AvoidStaticContext") // UX class getOtaFile( Context context, Map<String, ClientFile> otaFilesMap, String fileId)137 private static File getOtaFile( 138 Context context, Map<String, ClientFile> otaFilesMap, String fileId) { 139 // get OTA file 140 ClientFile otaFile = otaFilesMap.get(fileId); 141 if (otaFile == null) { 142 LogUtil.d(fileId + " not found"); 143 return null; 144 } 145 if (!otaFile.hasFileUri()) { 146 ErrorLogUtil.e( 147 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DOWNLOADED_OTA_FILE_ERROR, 148 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 149 return null; 150 } 151 @SuppressLint("NewAdServicesFile") 152 File f = new File(context.getDataDir() + Uri.parse(otaFile.getFileUri()).getPath()); 153 LogUtil.d("got this file:" + otaFile.getFileUri()); 154 return f; 155 } 156 157 /** 158 * This function populates metadata files to a map. 159 * 160 * @return A {@link Map} containing downloaded fileId mapped to ClientFile or null if no 161 * downloaded files found. 162 */ getDownloadedFiles()163 static @Nullable Map<String, ClientFile> getDownloadedFiles() { 164 LogUtil.d("getDownloadedFiles called."); 165 MobileDataDownload mobileDataDownload = 166 MobileDataDownloadFactory.getMdd(FlagsFactory.getFlags()); 167 GetFileGroupRequest getFileGroupRequest = 168 GetFileGroupRequest.newBuilder().setGroupName(FILE_GROUP_NAME).build(); 169 ClientFileGroup fileGroup; 170 try { 171 // TODO(b/242908564):We potentially cannot do callback here since we need to get the OTA 172 // strings before we create the UI, as the UI needs the updated strings if they exist. 173 fileGroup = mobileDataDownload.getFileGroup(getFileGroupRequest).get(); 174 } catch (ExecutionException | InterruptedException e) { 175 LogUtil.e(e, "Unable to load MDD file group for " + FILE_GROUP_NAME); 176 ErrorLogUtil.e( 177 e, 178 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE, 179 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 180 return null; 181 } 182 183 if (fileGroup == null || fileGroup.getStatus() != ClientFileGroup.Status.DOWNLOADED) { 184 return null; 185 } 186 LogUtil.d("found fileGroup: " + fileGroup); 187 Map<String, ClientFile> downloadedFiles = new ArrayMap<>(); 188 if (fileGroup != null) { 189 LogUtil.d("Populating downloadFiles map for " + FILE_GROUP_NAME); 190 for (ClientFile file : fileGroup.getFileList()) { 191 downloadedFiles.put(file.getFileId(), file); 192 } 193 LogUtil.d("setting fileGroup version for " + FILE_GROUP_NAME); 194 sOTAResourcesVersion = fileGroup.getBuildId(); 195 } 196 return downloadedFiles; 197 } 198 } 199