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.content.Context; 26 import android.content.res.Resources; 27 import android.content.res.loader.ResourcesLoader; 28 import android.content.res.loader.ResourcesProvider; 29 import android.net.Uri; 30 import android.os.Build; 31 import android.os.ParcelFileDescriptor; 32 import android.util.ArrayMap; 33 34 import androidx.annotation.NonNull; 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.io.IOException; 50 import java.util.Map; 51 import java.util.concurrent.ExecutionException; 52 53 /** 54 * Manages OTA (over the air) Resources downloaded from MDD. This allows device to use updated OTA 55 * resources. Currently only strings are supported. 56 */ 57 // TODO(b/269798827): Enable for R. 58 @RequiresApi(Build.VERSION_CODES.S) 59 public class OTAResourcesManager { 60 // this value needs to be updated if bundled resources are updated 61 private static final long BUNDLED_RESOURCES_VERSION = 0; 62 private static final long NO_OTA_RESOURCES_VERSION = -1; 63 private static final String FILE_GROUP_NAME = "ui-ota-strings"; 64 private static final String DOWNLOADED_OTA_FILE_ID = "resources.arsc"; 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(context); 88 89 // check if there are OTA Resources 90 if (downloadedOTAFiles == null || downloadedOTAFiles.size() <= 0) { 91 return; 92 } 93 // get OTA strings file 94 ClientFile resourcesFile = downloadedOTAFiles.get(DOWNLOADED_OTA_FILE_ID); 95 if (resourcesFile == null) { 96 LogUtil.d("No OTA file"); 97 return; 98 } 99 if (!resourcesFile.hasFileUri()) { 100 ErrorLogUtil.e( 101 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DOWNLOADED_OTA_FILE_ERROR, 102 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX, 103 OTAResourcesManager.class.getSimpleName(), 104 new Object() {}.getClass().getEnclosingMethod().getName()); 105 return; 106 } 107 File f = new File(context.getDataDir() + Uri.parse(resourcesFile.getFileUri()).getPath()); 108 LogUtil.d("got this file:" + resourcesFile.getFileUri()); 109 // Clear previous ResourceProvider and add new one created from arsc file 110 OTAResourcesLoader.clearProviders(); 111 try { 112 ParcelFileDescriptor fd = ParcelFileDescriptor.open(f, MODE_READ_ONLY); 113 OTAResourcesLoader.addProvider(ResourcesProvider.loadFromTable(fd, null)); 114 fd.close(); 115 } catch (IOException e) { 116 LogUtil.e("Exception while trying to add ResourcesProvider:" + e); 117 ErrorLogUtil.e( 118 e, 119 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 120 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 121 OTAResourcesLoader.clearProviders(); 122 } catch (Exception e) { 123 LogUtil.e("Caught exception while adding providers: " + e.getMessage()); 124 ErrorLogUtil.e( 125 e, 126 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RESOURCES_PROVIDER_ADD_ERROR, 127 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 128 OTAResourcesLoader.clearProviders(); 129 } 130 } 131 132 /** 133 * This function populates metadata files to a map. 134 * 135 * @param context {@link Context} 136 * @return A {@link Map} containing downloaded fileId mapped to ClientFile or null if no 137 * downloaded files found. 138 */ getDownloadedFiles(@onNull Context context)139 static @Nullable Map<String, ClientFile> getDownloadedFiles(@NonNull Context context) { 140 LogUtil.d("getDownloadedFiles called."); 141 MobileDataDownload mobileDataDownload = 142 MobileDataDownloadFactory.getMdd(context, FlagsFactory.getFlags()); 143 GetFileGroupRequest getFileGroupRequest = 144 GetFileGroupRequest.newBuilder().setGroupName(FILE_GROUP_NAME).build(); 145 ClientFileGroup fileGroup; 146 try { 147 // TODO(b/242908564):We potentially cannot do callback here since we need to get the OTA 148 // strings before we create the UI, as the UI needs the updated strings if they exist. 149 fileGroup = mobileDataDownload.getFileGroup(getFileGroupRequest).get(); 150 } catch (ExecutionException | InterruptedException e) { 151 LogUtil.e(e, "Unable to load MDD file group for " + FILE_GROUP_NAME); 152 ErrorLogUtil.e( 153 e, 154 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE, 155 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX); 156 return null; 157 } 158 159 if (fileGroup == null || fileGroup.getStatus() != ClientFileGroup.Status.DOWNLOADED) { 160 return null; 161 } 162 LogUtil.d("found fileGroup: " + fileGroup); 163 Map<String, ClientFile> downloadedFiles = new ArrayMap<>(); 164 if (fileGroup != null) { 165 LogUtil.d("Populating downloadFiles map for " + FILE_GROUP_NAME); 166 for (ClientFile file : fileGroup.getFileList()) { 167 downloadedFiles.put(file.getFileId(), file); 168 } 169 LogUtil.d("setting fileGroup version for " + FILE_GROUP_NAME); 170 sOTAResourcesVersion = fileGroup.getBuildId(); 171 } 172 return downloadedFiles; 173 } 174 } 175