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