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