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