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 // TODO(b/269798827): Enable for R. 57 @RequiresApi(Build.VERSION_CODES.S) 58 public class OTAResourcesManager { 59 // this value needs to be updated if bundled resources are updated 60 private static final long BUNDLED_RESOURCES_VERSION = 4265; 61 private static final long NO_OTA_RESOURCES_VERSION = -1; 62 private static final String FILE_GROUP_NAME = "ui-ota-strings"; 63 public static final String DOWNLOADED_OTA_FILE_ID = "resources.arsc"; 64 public static final String DOWNLOADED_OTA_APK_ID = "AdServicesOtaResourcesApp.apk"; 65 private static final ResourcesLoader OTAResourcesLoader = new ResourcesLoader(); 66 67 private static long sOTAResourcesVersion = NO_OTA_RESOURCES_VERSION; 68 69 /** 70 * If shouldRefresh, then create a new OTA {@link ResourcesLoader} from ARSC file on device. 71 * Checks if OTA version > bundled version: If true, then add OTAResourcesLoader to the current 72 * context's {@link Resources}. Else, do nothing. 73 * 74 * @param context {@link Context} 75 */ 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 refreshOTAResources(Context context)85 static void refreshOTAResources(Context context) { 86 LogUtil.d("createResourceLoaderFromMDDFiles called."); 87 Map<String, ClientFile> downloadedOTAFiles = getDownloadedFiles(); 88 89 // check if there are OTA Resources 90 if (downloadedOTAFiles == null || downloadedOTAFiles.size() == 0) { 91 return; 92 } 93 // get OTA strings file 94 File resourcesFile = getOtaFile(context, downloadedOTAFiles, DOWNLOADED_OTA_FILE_ID); 95 // get OTA resources apk 96 File resourcesApk = getOtaFile(context, downloadedOTAFiles, DOWNLOADED_OTA_APK_ID); 97 if (resourcesFile == null && resourcesApk == null) { 98 LogUtil.d("No OTA files"); 99 return; 100 } 101 102 // Clear previous ResourceProvider 103 OTAResourcesLoader.clearProviders(); 104 // Add new ResourceProvider created from arsc file 105 if (resourcesFile != null) { 106 try { 107 ParcelFileDescriptor fd = ParcelFileDescriptor.open(resourcesFile, MODE_READ_ONLY); 108 OTAResourcesLoader.addProvider(ResourcesProvider.loadFromTable(fd, null)); 109 fd.close(); 110 } catch (Exception e) { 111 LogUtil.e("Caught exception while adding OTA string provider: " + e.getMessage()); 112 ErrorLogUtil.e( 113 e, 114 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 115 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 116 OTAResourcesLoader.clearProviders(); 117 } 118 } 119 // Add new ResourceProvider created from OTA apk 120 else if (resourcesApk != null) { 121 try { 122 ParcelFileDescriptor fd = ParcelFileDescriptor.open(resourcesApk, MODE_READ_ONLY); 123 OTAResourcesLoader.addProvider(ResourcesProvider.loadFromApk(fd, null)); 124 fd.close(); 125 } catch (Exception e) { 126 LogUtil.e("Caught exception while adding OTA apk provider: " + e.getMessage()); 127 ErrorLogUtil.e( 128 e, 129 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 130 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 131 } 132 } 133 } 134 getOtaFile( Context context, Map<String, ClientFile> otaFilesMap, String fileId)135 private static File getOtaFile( 136 Context context, Map<String, ClientFile> otaFilesMap, String fileId) { 137 // get OTA file 138 ClientFile otaFile = otaFilesMap.get(fileId); 139 if (otaFile == null) { 140 LogUtil.d(fileId + " not found"); 141 return null; 142 } 143 if (!otaFile.hasFileUri()) { 144 ErrorLogUtil.e( 145 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DOWNLOADED_OTA_FILE_ERROR, 146 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 147 return null; 148 } 149 @SuppressLint("NewAdServicesFile") 150 File f = new File(context.getDataDir() + Uri.parse(otaFile.getFileUri()).getPath()); 151 LogUtil.d("got this file:" + otaFile.getFileUri()); 152 return f; 153 } 154 155 /** 156 * This function populates metadata files to a map. 157 * 158 * @param context {@link Context} 159 * @return A {@link Map} containing downloaded fileId mapped to ClientFile or null if no 160 * downloaded files found. 161 */ getDownloadedFiles()162 static @Nullable Map<String, ClientFile> getDownloadedFiles() { 163 LogUtil.d("getDownloadedFiles called."); 164 MobileDataDownload mobileDataDownload = 165 MobileDataDownloadFactory.getMdd(FlagsFactory.getFlags()); 166 GetFileGroupRequest getFileGroupRequest = 167 GetFileGroupRequest.newBuilder().setGroupName(FILE_GROUP_NAME).build(); 168 ClientFileGroup fileGroup; 169 try { 170 // TODO(b/242908564):We potentially cannot do callback here since we need to get the OTA 171 // strings before we create the UI, as the UI needs the updated strings if they exist. 172 fileGroup = mobileDataDownload.getFileGroup(getFileGroupRequest).get(); 173 } catch (ExecutionException | InterruptedException e) { 174 LogUtil.e(e, "Unable to load MDD file group for " + FILE_GROUP_NAME); 175 ErrorLogUtil.e( 176 e, 177 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE, 178 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 179 return null; 180 } 181 182 if (fileGroup == null || fileGroup.getStatus() != ClientFileGroup.Status.DOWNLOADED) { 183 return null; 184 } 185 LogUtil.d("found fileGroup: " + fileGroup); 186 Map<String, ClientFile> downloadedFiles = new ArrayMap<>(); 187 if (fileGroup != null) { 188 LogUtil.d("Populating downloadFiles map for " + FILE_GROUP_NAME); 189 for (ClientFile file : fileGroup.getFileList()) { 190 downloadedFiles.put(file.getFileId(), file); 191 } 192 LogUtil.d("setting fileGroup version for " + FILE_GROUP_NAME); 193 sOTAResourcesVersion = fileGroup.getBuildId(); 194 } 195 return downloadedFiles; 196 } 197 } 198