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.internal.content.om;
18 
19 import static android.content.Context.MODE_PRIVATE;
20 import static android.content.om.OverlayManagerTransaction.Request.BUNDLE_FABRICATED_OVERLAY;
21 import static android.content.om.OverlayManagerTransaction.Request.TYPE_REGISTER_FABRICATED;
22 import static android.content.om.OverlayManagerTransaction.Request.TYPE_UNREGISTER_FABRICATED;
23 
24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
25 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
26 import static com.android.internal.content.om.OverlayConfig.DEFAULT_PRIORITY;
27 
28 import android.annotation.NonNull;
29 import android.annotation.NonUiContext;
30 import android.content.Context;
31 import android.content.om.OverlayIdentifier;
32 import android.content.om.OverlayInfo;
33 import android.content.om.OverlayManagerTransaction;
34 import android.content.om.OverlayManagerTransaction.Request;
35 import android.content.pm.ApplicationInfo;
36 import android.content.pm.PackageManager;
37 import android.content.pm.parsing.FrameworkParsingPackageUtils;
38 import android.os.FabricatedOverlayInfo;
39 import android.os.FabricatedOverlayInternal;
40 import android.os.FabricatedOverlayInternalEntry;
41 import android.os.FileUtils;
42 import android.os.Process;
43 import android.os.UserHandle;
44 import android.text.TextUtils;
45 import android.util.Log;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.util.Preconditions;
49 
50 import java.io.File;
51 import java.io.IOException;
52 import java.nio.file.FileVisitResult;
53 import java.nio.file.Files;
54 import java.nio.file.Path;
55 import java.nio.file.SimpleFileVisitor;
56 import java.nio.file.attribute.BasicFileAttributes;
57 import java.util.ArrayList;
58 import java.util.Iterator;
59 import java.util.List;
60 import java.util.Objects;
61 
62 /**
63  * This class provides the functionalities of registering an overlay, unregistering an overlay, and
64  * getting the list of overlays information.
65  */
66 public class OverlayManagerImpl {
67     private static final String TAG = "OverlayManagerImpl";
68     private static final boolean DEBUG = false;
69 
70     private static final String FRRO_EXTENSION = ".frro";
71 
72     private static final String IDMAP_EXTENSION = ".idmap";
73 
74     @VisibleForTesting(visibility = PRIVATE)
75     public static final String SELF_TARGET = ".self_target";
76 
77     @NonNull
78     private final Context mContext;
79     private Path mBasePath;
80 
81     /**
82      * Init a OverlayManagerImpl by the context.
83      *
84      * @param context the context to create overlay environment
85      */
86     @VisibleForTesting(visibility = PACKAGE)
OverlayManagerImpl(@onNull Context context)87     public OverlayManagerImpl(@NonNull Context context) {
88         mContext = Objects.requireNonNull(context);
89 
90         if (!Process.myUserHandle().equals(context.getUser())) {
91             throw new SecurityException("Self-Targeting doesn't support multiple user now!");
92         }
93     }
94 
cleanExpiredOverlays(Path selfTargetingBasePath, Path folderForCurrentBaseApk)95     private static void cleanExpiredOverlays(Path selfTargetingBasePath,
96             Path folderForCurrentBaseApk) {
97         try {
98             final String currentBaseFolder = folderForCurrentBaseApk.toString();
99             final String selfTargetingDir = selfTargetingBasePath.getFileName().toString();
100             Files.walkFileTree(
101                     selfTargetingBasePath,
102                     new SimpleFileVisitor<>() {
103                         @Override
104                         public FileVisitResult preVisitDirectory(Path dir,
105                                                                  BasicFileAttributes attrs)
106                                 throws IOException {
107                             final String fileName = dir.getFileName().toString();
108                             return fileName.equals(currentBaseFolder)
109                                     ? FileVisitResult.SKIP_SUBTREE
110                                     : super.preVisitDirectory(dir, attrs);
111                         }
112 
113                         @Override
114                         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
115                                 throws IOException {
116                             if (!file.toFile().delete()) {
117                                 Log.w(TAG, "Failed to delete file " + file);
118                             }
119                             return super.visitFile(file, attrs);
120                         }
121 
122                         @Override
123                         public FileVisitResult postVisitDirectory(Path dir, IOException exc)
124                                 throws IOException {
125                             final String fileName = dir.getFileName().toString();
126                             if (!fileName.equals(currentBaseFolder)
127                                     && !fileName.equals(selfTargetingDir)) {
128                                 if (!dir.toFile().delete()) {
129                                     Log.w(TAG, "Failed to delete dir " + dir);
130                                 }
131                             }
132                             return super.postVisitDirectory(dir, exc);
133                         }
134                     });
135         } catch (IOException e) {
136             Log.w(TAG, "Unknown fail " + e);
137         }
138     }
139 
140     /** Ensure the base dir for self-targeting is valid. */
141     @VisibleForTesting
142     @NonUiContext
ensureBaseDir()143     public void ensureBaseDir() {
144         final String baseApkPath = mContext.getApplicationInfo().getBaseCodePath();
145         final Path baseApkFolderName = Path.of(baseApkPath).getParent().getFileName();
146         final File selfTargetingBaseFile = mContext.getDir(SELF_TARGET, MODE_PRIVATE);
147         Preconditions.checkArgument(
148                 selfTargetingBaseFile.isDirectory()
149                         && selfTargetingBaseFile.exists()
150                         && selfTargetingBaseFile.canWrite()
151                         && selfTargetingBaseFile.canRead()
152                         && selfTargetingBaseFile.canExecute(),
153                 "Can't work for this context");
154         cleanExpiredOverlays(selfTargetingBaseFile.toPath(), baseApkFolderName);
155 
156         final File baseFile = new File(selfTargetingBaseFile, baseApkFolderName.toString());
157         if (!baseFile.exists()) {
158             if (!baseFile.mkdirs()) {
159                 Log.w(TAG, "Failed to create directory " + baseFile);
160             }
161 
162             // It fails to create frro and idmap files without this setting.
163             FileUtils.setPermissions(
164                     baseFile,
165                     FileUtils.S_IRWXU,
166                     -1 /* uid unchanged */,
167                     -1 /* gid unchanged */);
168         }
169         Preconditions.checkArgument(
170                 baseFile.isDirectory()
171                         && baseFile.exists()
172                         && baseFile.canWrite()
173                         && baseFile.canRead()
174                         && baseFile.canExecute(), // 'list' capability
175                 "Can't create a workspace for this context");
176 
177         mBasePath = baseFile.toPath();
178     }
179 
isSameWithTargetSignature(final String targetPackage)180     private boolean isSameWithTargetSignature(final String targetPackage) {
181         final PackageManager packageManager = mContext.getPackageManager();
182         final String packageName = mContext.getPackageName();
183         if (TextUtils.equals(packageName, targetPackage)) {
184             return true;
185         }
186         return packageManager.checkSignatures(packageName, targetPackage)
187                 == PackageManager.SIGNATURE_MATCH;
188     }
189 
190     /**
191      * Check if the overlay name is valid or not.
192      *
193      * @param name the non-check overlay name
194      * @return the valid overlay name
195      */
checkOverlayNameValid(@onNull String name)196     public static String checkOverlayNameValid(@NonNull String name) {
197         final String overlayName =
198                 Preconditions.checkStringNotEmpty(
199                         name, "overlayName should be neither empty nor null string");
200         final String checkOverlayNameResult =
201                 FrameworkParsingPackageUtils.validateName(
202                         overlayName, false /* requireSeparator */, true /* requireFilename */);
203         Preconditions.checkArgument(
204                 checkOverlayNameResult == null,
205                 TextUtils.formatSimple(
206                         "Invalid overlayName \"%s\". The check result is %s.",
207                         overlayName, checkOverlayNameResult));
208         return overlayName;
209     }
210 
checkPackageName(@onNull String packageName)211     private void checkPackageName(@NonNull String packageName) {
212         Preconditions.checkStringNotEmpty(packageName);
213         Preconditions.checkArgument(
214                 TextUtils.equals(mContext.getPackageName(), packageName),
215                 TextUtils.formatSimple(
216                         "UID %d doesn't own the package %s", Process.myUid(), packageName));
217     }
218 
219     /**
220      * Save FabricatedOverlay instance as frro and idmap files.
221      *
222      * <p>In order to fill the overlayable policy, it's necessary to collect the information from
223      * app. And then, the information is passed to native layer to fill the overlayable policy
224      *
225      * @param overlayInternal the FabricatedOverlayInternal to be saved.
226      */
227     @NonUiContext
registerFabricatedOverlay(@onNull FabricatedOverlayInternal overlayInternal)228     public void registerFabricatedOverlay(@NonNull FabricatedOverlayInternal overlayInternal)
229             throws IOException, PackageManager.NameNotFoundException {
230         ensureBaseDir();
231         Objects.requireNonNull(overlayInternal);
232         final List<FabricatedOverlayInternalEntry> entryList =
233                 Objects.requireNonNull(overlayInternal.entries);
234         Preconditions.checkArgument(!entryList.isEmpty(), "overlay entries shouldn't be empty");
235         final String overlayName = checkOverlayNameValid(overlayInternal.overlayName);
236         checkPackageName(overlayInternal.packageName);
237         checkPackageName(overlayInternal.targetPackageName);
238         Preconditions.checkStringNotEmpty(
239                 overlayInternal.targetOverlayable,
240                 "Target overlayable should be neither null nor empty string.");
241 
242         final ApplicationInfo applicationInfo = mContext.getApplicationInfo();
243         final String targetPackage = Preconditions.checkStringNotEmpty(
244                 applicationInfo.getBaseCodePath());
245         final Path frroPath = mBasePath.resolve(overlayName + FRRO_EXTENSION);
246         final Path idmapPath = mBasePath.resolve(overlayName + IDMAP_EXTENSION);
247 
248         createFrroFile(frroPath.toString(), overlayInternal);
249         try {
250             createIdmapFile(
251                     targetPackage,
252                     frroPath.toString(),
253                     idmapPath.toString(),
254                     overlayName,
255                     applicationInfo.isSystemApp() || applicationInfo.isSystemExt() /* isSystem */,
256                     applicationInfo.isVendor(),
257                     applicationInfo.isProduct(),
258                     isSameWithTargetSignature(overlayInternal.targetPackageName),
259                     applicationInfo.isOdm(),
260                     applicationInfo.isOem());
261         } catch (IOException e) {
262             if (!frroPath.toFile().delete()) {
263                 Log.w(TAG, "Failed to delete file " + frroPath);
264             }
265             throw e;
266         }
267     }
268 
269     /**
270      * Remove the overlay with the specific name
271      *
272      * @param overlayName the specific name
273      */
274     @NonUiContext
unregisterFabricatedOverlay(@onNull String overlayName)275     public void unregisterFabricatedOverlay(@NonNull String overlayName) {
276         ensureBaseDir();
277         checkOverlayNameValid(overlayName);
278         final Path frroPath = mBasePath.resolve(overlayName + FRRO_EXTENSION);
279         final Path idmapPath = mBasePath.resolve(overlayName + IDMAP_EXTENSION);
280 
281         if (!frroPath.toFile().delete()) {
282             Log.w(TAG, "Failed to delete file " + frroPath);
283         }
284         if (!idmapPath.toFile().delete()) {
285             Log.w(TAG, "Failed to delete file " + idmapPath);
286         }
287     }
288 
289     /**
290      * Commit the overlay manager transaction
291      *
292      * @param transaction the overlay manager transaction
293      */
294     @NonUiContext
commit(@onNull OverlayManagerTransaction transaction)295     public void commit(@NonNull OverlayManagerTransaction transaction)
296             throws PackageManager.NameNotFoundException, IOException {
297         Objects.requireNonNull(transaction);
298 
299         for (Iterator<Request> it = transaction.getRequests(); it.hasNext(); ) {
300             final Request request = it.next();
301             if (request.type == TYPE_REGISTER_FABRICATED) {
302                 final FabricatedOverlayInternal fabricatedOverlayInternal =
303                         Objects.requireNonNull(
304                                 request.extras.getParcelable(
305                                         BUNDLE_FABRICATED_OVERLAY,
306                                         FabricatedOverlayInternal.class));
307 
308                 // populate the mandatory data
309                 if (TextUtils.isEmpty(fabricatedOverlayInternal.packageName)) {
310                     fabricatedOverlayInternal.packageName = mContext.getPackageName();
311                 } else {
312                     if (!TextUtils.equals(
313                             fabricatedOverlayInternal.packageName, mContext.getPackageName())) {
314                         throw new IllegalArgumentException("Unknown package name in transaction");
315                     }
316                 }
317 
318                 registerFabricatedOverlay(fabricatedOverlayInternal);
319             } else if (request.type == TYPE_UNREGISTER_FABRICATED) {
320                 final OverlayIdentifier overlayIdentifier = Objects.requireNonNull(request.overlay);
321                 unregisterFabricatedOverlay(overlayIdentifier.getOverlayName());
322             } else {
323                 throw new IllegalArgumentException("Unknown request in transaction " + request);
324             }
325         }
326     }
327 
328     /**
329      * Get the list of overlays information for the target package name.
330      *
331      * @param targetPackage the target package name
332      * @return the list of overlays information.
333      */
334     @NonNull
getOverlayInfosForTarget(@onNull String targetPackage)335     public List<OverlayInfo> getOverlayInfosForTarget(@NonNull String targetPackage) {
336         ensureBaseDir();
337 
338         final File base = mBasePath.toFile();
339         final File[] frroFiles = base.listFiles((dir, name) -> {
340             if (!name.endsWith(FRRO_EXTENSION)) {
341                 return false;
342             }
343 
344             final String idmapFileName = name.substring(0, name.length() - FRRO_EXTENSION.length())
345                     + IDMAP_EXTENSION;
346             final File idmapFile = new File(dir, idmapFileName);
347             return idmapFile.exists();
348         });
349 
350         final ArrayList<OverlayInfo> overlayInfos = new ArrayList<>();
351         for (File file : frroFiles) {
352             final FabricatedOverlayInfo fabricatedOverlayInfo;
353             try {
354                 fabricatedOverlayInfo = getFabricatedOverlayInfo(file.getAbsolutePath());
355             } catch (IOException e) {
356                 Log.w(TAG, "can't load " + file);
357                 continue;
358             }
359             if (!TextUtils.equals(targetPackage, fabricatedOverlayInfo.targetPackageName)) {
360                 continue;
361             }
362             if (DEBUG) {
363                 Log.i(TAG, "load " + file);
364             }
365 
366             final OverlayInfo overlayInfo =
367                     new OverlayInfo(
368                             fabricatedOverlayInfo.packageName,
369                             fabricatedOverlayInfo.overlayName,
370                             fabricatedOverlayInfo.targetPackageName,
371                             fabricatedOverlayInfo.targetOverlayable,
372                             null,
373                             file.getAbsolutePath(),
374                             OverlayInfo.STATE_ENABLED,
375                             UserHandle.myUserId(),
376                             DEFAULT_PRIORITY,
377                             true /* isMutable */,
378                             true /* isFabricated */);
379             overlayInfos.add(overlayInfo);
380         }
381         return overlayInfos;
382     }
383 
createFrroFile( @onNull String frroFile, @NonNull FabricatedOverlayInternal fabricatedOverlayInternal)384     private static native void createFrroFile(
385             @NonNull String frroFile, @NonNull FabricatedOverlayInternal fabricatedOverlayInternal)
386             throws IOException;
387 
createIdmapFile( @onNull String targetPath, @NonNull String overlayPath, @NonNull String idmapPath, @NonNull String overlayName, boolean isSystem, boolean isVendor, boolean isProduct, boolean isSameWithTargetSignature, boolean isOdm, boolean isOem)388     private static native void createIdmapFile(
389             @NonNull String targetPath,
390             @NonNull String overlayPath,
391             @NonNull String idmapPath,
392             @NonNull String overlayName,
393             boolean isSystem,
394             boolean isVendor,
395             boolean isProduct,
396             boolean isSameWithTargetSignature,
397             boolean isOdm,
398             boolean isOem)
399             throws IOException;
400 
getFabricatedOverlayInfo( @onNull String overlayPath)401     private static native FabricatedOverlayInfo getFabricatedOverlayInfo(
402             @NonNull String overlayPath) throws IOException;
403 }
404