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 
17 package com.android.ondevicepersonalization.services.download.mdd;
18 
19 import static android.content.pm.PackageManager.GET_META_DATA;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.net.Uri;
26 import android.os.SystemProperties;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.odp.module.common.PackageUtils;
30 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
31 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
32 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
33 import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
34 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
35 
36 import com.google.android.libraries.mobiledatadownload.AddFileGroupRequest;
37 import com.google.android.libraries.mobiledatadownload.FileGroupPopulator;
38 import com.google.android.libraries.mobiledatadownload.GetFileGroupsByFilterRequest;
39 import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
40 import com.google.android.libraries.mobiledatadownload.RemoveFileGroupRequest;
41 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
42 import com.google.common.util.concurrent.FluentFuture;
43 import com.google.common.util.concurrent.Futures;
44 import com.google.common.util.concurrent.ListenableFuture;
45 import com.google.mobiledatadownload.ClientConfigProto;
46 import com.google.mobiledatadownload.DownloadConfigProto.DataFile;
47 import com.google.mobiledatadownload.DownloadConfigProto.DataFile.ChecksumType;
48 import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup;
49 import com.google.mobiledatadownload.DownloadConfigProto.DownloadConditions;
50 import com.google.mobiledatadownload.DownloadConfigProto.DownloadConditions.DeviceNetworkPolicy;
51 
52 import java.util.ArrayList;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Set;
56 
57 /**
58  * FileGroupPopulator to add FileGroups for ODP onboarded packages
59  */
60 public class OnDevicePersonalizationFileGroupPopulator implements FileGroupPopulator {
61     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
62     private static final String TAG = "OnDevicePersonalizationFileGroupPopulator";
63 
64     private final Context mContext;
65 
66     // Immediately delete stale files during maintenance once a newer file has been downloaded.
67     private static final long STALE_LIFETIME_SECS = 0;
68 
69     // Set files to expire after 2 days.
70     private static final long EXPIRATION_TIME_SECS = 172800;
71     private static final String OVERRIDE_DOWNLOAD_URL_PACKAGE =
72             "debug.ondevicepersonalization.override_download_url_package";
73     private static final String OVERRIDE_DOWNLOAD_URL =
74             "debug.ondevicepersonalization.override_download_url";
75 
OnDevicePersonalizationFileGroupPopulator(Context context)76     public OnDevicePersonalizationFileGroupPopulator(Context context) {
77         this.mContext = context;
78     }
79 
80     /**
81      * A helper function to create a DataFilegroup.
82      */
createDataFileGroup( String groupName, String ownerPackage, String[] fileId, int[] byteSize, String[] checksum, ChecksumType[] checksumType, String[] url, DeviceNetworkPolicy deviceNetworkPolicy)83     public static DataFileGroup createDataFileGroup(
84             String groupName,
85             String ownerPackage,
86             String[] fileId,
87             int[] byteSize,
88             String[] checksum,
89             ChecksumType[] checksumType,
90             String[] url,
91             DeviceNetworkPolicy deviceNetworkPolicy) {
92         if (fileId.length != byteSize.length
93                 || fileId.length != checksum.length
94                 || fileId.length != url.length
95                 || checksumType.length != fileId.length) {
96             throw new IllegalArgumentException();
97         }
98 
99         DataFileGroup.Builder dataFileGroupBuilder =
100                 DataFileGroup.newBuilder()
101                         .setGroupName(groupName)
102                         .setOwnerPackage(ownerPackage)
103                         .setStaleLifetimeSecs(STALE_LIFETIME_SECS)
104                         .setExpirationDate(
105                                 (System.currentTimeMillis() / 1000) + EXPIRATION_TIME_SECS)
106                         .setDownloadConditions(
107                                 DownloadConditions.newBuilder().setDeviceNetworkPolicy(
108                                         deviceNetworkPolicy));
109 
110         for (int i = 0; i < fileId.length; ++i) {
111             DataFile file =
112                     DataFile.newBuilder()
113                             .setFileId(fileId[i])
114                             .setByteSize(byteSize[i])
115                             .setChecksum(checksum[i])
116                             .setChecksumType(checksumType[i])
117                             .setUrlToDownload(url[i])
118                             .build();
119             dataFileGroupBuilder.addFile(file);
120         }
121 
122         return dataFileGroupBuilder.build();
123     }
124 
125     /**
126      * Creates the fileGroup name based off the package's name and cert.
127      *
128      * @param packageName Name of the package owning the fileGroup
129      * @param context     Context of the calling service/application
130      * @return The created fileGroup name.
131      */
createPackageFileGroupName(String packageName, Context context)132     public static String createPackageFileGroupName(String packageName, Context context) throws
133             PackageManager.NameNotFoundException {
134         return packageName + "_" + PackageUtils.getCertDigest(context, packageName);
135     }
136 
137     /**
138      * Creates the MDD download URL for the given package
139      *
140      * @param packageName PackageName of the package owning the fileGroup
141      * @param context     Context of the calling service/application
142      * @return The created MDD URL for the package.
143      */
144     @VisibleForTesting
createDownloadUrl(String packageName, Context context)145     public static String createDownloadUrl(String packageName, Context context) throws
146             PackageManager.NameNotFoundException {
147         String baseURL = AppManifestConfigHelper.getDownloadUrlFromOdpSettings(
148                 context, packageName);
149 
150         // Check for override manifest url property, if package is debuggable
151         if (PackageUtils.isPackageDebuggable(context, packageName)) {
152             if (SystemProperties.get(OVERRIDE_DOWNLOAD_URL_PACKAGE, "").equals(packageName)) {
153                 String overrideManifestUrl = SystemProperties.get(OVERRIDE_DOWNLOAD_URL, "");
154                 if (!overrideManifestUrl.isEmpty()) {
155                     sLogger.d(TAG + ": Overriding baseURL for package "
156                             + packageName + " to " + overrideManifestUrl);
157                     baseURL = overrideManifestUrl;
158                 }
159             }
160         }
161 
162         if (baseURL == null || baseURL.isEmpty()) {
163             throw new IllegalArgumentException("Failed to retrieve base download URL");
164         }
165 
166         Uri uri = Uri.parse(baseURL);
167 
168         // Enforce URI scheme
169         if (OnDevicePersonalizationLocalFileDownloader.isLocalOdpUri(uri)) {
170             if (!PackageUtils.isPackageDebuggable(context, packageName)) {
171                 throw new IllegalArgumentException("Local urls are only valid "
172                         + "for debuggable packages: " + baseURL);
173             }
174         } else if (!baseURL.startsWith("https")) {
175             throw new IllegalArgumentException("File url is not secure: " + baseURL);
176         }
177 
178         return addDownloadUrlQueryParameters(uri, packageName, context);
179     }
180 
181     /**
182      * Adds query parameters to the download URL.
183      */
addDownloadUrlQueryParameters(Uri uri, String packageName, Context context)184     private static String addDownloadUrlQueryParameters(Uri uri, String packageName,
185             Context context)
186             throws PackageManager.NameNotFoundException {
187         String serviceClass = AppManifestConfigHelper.getServiceNameFromOdpSettings(
188                 context, packageName);
189         ComponentName service = ComponentName.createRelative(packageName, serviceClass);
190         long syncToken = OnDevicePersonalizationVendorDataDao.getInstance(context, service,
191                 PackageUtils.getCertDigest(context, packageName)).getSyncToken();
192         if (syncToken != -1) {
193             uri = uri.buildUpon().appendQueryParameter("syncToken",
194                     String.valueOf(syncToken)).build();
195         }
196         // TODO(b/267177135) Add user data query parameters here
197         return uri.toString();
198     }
199 
200     @Override
refreshFileGroups(MobileDataDownload mobileDataDownload)201     public ListenableFuture<Void> refreshFileGroups(MobileDataDownload mobileDataDownload) {
202         GetFileGroupsByFilterRequest request =
203                 GetFileGroupsByFilterRequest.newBuilder().setIncludeAllGroups(true).build();
204         return FluentFuture.from(mobileDataDownload.getFileGroupsByFilter(request))
205                 .transformAsync(fileGroupList -> {
206                     Set<String> fileGroupsToRemove = new HashSet<>();
207                     for (ClientConfigProto.ClientFileGroup fileGroup : fileGroupList) {
208                         fileGroupsToRemove.add(fileGroup.getGroupName());
209                     }
210                     List<ListenableFuture<Boolean>> mFutures = new ArrayList<>();
211                     for (PackageInfo packageInfo : mContext.getPackageManager()
212                             .getInstalledPackages(
213                                     PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
214                         String packageName = packageInfo.packageName;
215                         if (AppManifestConfigHelper.manifestContainsOdpSettings(
216                                 mContext, packageName)) {
217                             if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
218                                 sLogger.d(TAG + ": service %s has ODP manifest, "
219                                         + "but not enrolled", packageName);
220                                 continue;
221                             }
222                             sLogger.d(TAG + ": service %s has ODP manifest and is enrolled",
223                                     packageName);
224                             try {
225                                 String groupName = createPackageFileGroupName(
226                                         packageName,
227                                         mContext);
228                                 fileGroupsToRemove.remove(groupName);
229                                 String ownerPackage = mContext.getPackageName();
230                                 String fileId = groupName;
231                                 int byteSize = 0;
232                                 String checksum = "";
233                                 ChecksumType checksumType = ChecksumType.NONE;
234                                 String downloadUrl = createDownloadUrl(packageName,
235                                         mContext);
236                                 DeviceNetworkPolicy deviceNetworkPolicy =
237                                         DeviceNetworkPolicy.DOWNLOAD_ONLY_ON_WIFI;
238                                 DataFileGroup dataFileGroup = createDataFileGroup(
239                                         groupName,
240                                         ownerPackage,
241                                         new String[]{fileId},
242                                         new int[]{byteSize},
243                                         new String[]{checksum},
244                                         new ChecksumType[]{checksumType},
245                                         new String[]{downloadUrl},
246                                         deviceNetworkPolicy);
247                                 mFutures.add(mobileDataDownload.addFileGroup(
248                                         AddFileGroupRequest.newBuilder().setDataFileGroup(
249                                                 dataFileGroup).build()));
250                             } catch (Exception e) {
251                                 sLogger.e(TAG + ": Failed to create file group for "
252                                         + packageName, e);
253                             }
254                         }
255                     }
256 
257                     for (String group : fileGroupsToRemove) {
258                         sLogger.d(TAG + ": Removing file group: " + group);
259                         mFutures.add(mobileDataDownload.removeFileGroup(
260                                 RemoveFileGroupRequest.newBuilder().setGroupName(group).build()));
261                     }
262 
263                     return PropagatedFutures.transform(
264                             Futures.successfulAsList(mFutures),
265                             result -> {
266                                 if (result.contains(null)) {
267                                     sLogger.d(TAG + ": Failed to add or remove a file group");
268                                 } else {
269                                     sLogger.d(TAG + ": Successfully updated all file groups");
270                                 }
271                                 return null;
272                             },
273                             OnDevicePersonalizationExecutors.getBackgroundExecutor()
274                     );
275                 }, OnDevicePersonalizationExecutors.getBackgroundExecutor());
276     }
277 }
278