1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.httpflags;
6 
7 import static com.google.common.truth.Truth.assertThat;
8 
9 import android.content.Context;
10 import android.content.ContextWrapper;
11 import android.content.Intent;
12 import android.content.pm.ApplicationInfo;
13 import android.content.pm.PackageManager;
14 import android.content.pm.ResolveInfo;
15 import android.content.pm.ServiceInfo;
16 import android.os.Build;
17 
18 import androidx.annotation.Nullable;
19 
20 import org.chromium.base.test.util.PackageManagerWrapper;
21 import org.chromium.net.ContextInterceptor;
22 
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.util.UUID;
27 
28 /**
29  * A {@link ContextInterceptor} that makes the intercepted Context advertise the presence (or
30  * absence) of an HTTP flags file.
31  *
32  * @see org.chromium.net.httpflags.HttpFlagsLoader
33  */
34 public final class HttpFlagsInterceptor implements ContextInterceptor, AutoCloseable {
35     private static final String FLAGS_PROVIDER_PACKAGE_NAME =
36             "org.chromium.net.httpflags.HttpFlagsInterceptor.FAKE_PROVIDER_PACKAGE";
37 
38     @Nullable private final Flags mFlagsFileContents;
39     private File mDataDir;
40 
41     /** @param flagsFileContents the contents of the flags file, or null to simulate a missing file. */
HttpFlagsInterceptor(@ullable Flags flagsFileContents)42     public HttpFlagsInterceptor(@Nullable Flags flagsFileContents) {
43         mFlagsFileContents = flagsFileContents;
44     }
45 
46     @Override
interceptContext(Context context)47     public Context interceptContext(Context context) {
48         return new HttpFlagsContextWrapper(context);
49     }
50 
51     private final class HttpFlagsContextWrapper extends ContextWrapper {
HttpFlagsContextWrapper(Context context)52         HttpFlagsContextWrapper(Context context) {
53             super(context);
54         }
55 
56         @Override
getPackageManager()57         public PackageManager getPackageManager() {
58             return new PackageManagerWrapper(super.getPackageManager()) {
59                 @Override
60                 public ResolveInfo resolveService(Intent intent, int flags) {
61                     if (!intent.getAction()
62                             .equals(HttpFlagsLoader.FLAGS_FILE_PROVIDER_INTENT_ACTION)) {
63                         return super.resolveService(intent, flags);
64                     }
65 
66                     assertThat(flags).isEqualTo(MATCH_SYSTEM_ONLY);
67 
68                     if (mFlagsFileContents == null) return null;
69                     createFlagsFile(getBaseContext());
70 
71                     ApplicationInfo applicationInfo = new ApplicationInfo();
72                     applicationInfo.packageName = FLAGS_PROVIDER_PACKAGE_NAME;
73                     if (Build.VERSION.SDK_INT >= 24) {
74                         applicationInfo.deviceProtectedDataDir = mDataDir.getAbsolutePath();
75                     } else {
76                         applicationInfo.dataDir = mDataDir.getAbsolutePath();
77                     }
78 
79                     ResolveInfo resolveInfo = new ResolveInfo();
80                     resolveInfo.serviceInfo = new ServiceInfo();
81                     resolveInfo.serviceInfo.applicationInfo = applicationInfo;
82                     return resolveInfo;
83                 }
84             };
85         }
86     }
87 
88     private void createFlagsFile(Context context) {
89         if (mDataDir != null) return;
90         mDataDir =
91                 context.getDir(
92                         "org.chromium.net.httpflags.FakeFlagsFileDataDir."
93                                 // Ensure different instances can't interfere with each other (e.g.
94                                 // when running multiple tests).
95                                 + UUID.randomUUID(),
96                         Context.MODE_PRIVATE);
97 
98         File flagsFile = getFlagsFile();
99         if (!flagsFile.getParentFile().mkdir()) {
100             throw new RuntimeException("Unable to create flags dir");
101         }
102         try {
103             if (!flagsFile.createNewFile()) throw new RuntimeException("File already exists");
104             try (final FileOutputStream fileOutputStream = new FileOutputStream(flagsFile)) {
105                 mFlagsFileContents.writeDelimitedTo(fileOutputStream);
106             }
107         } catch (RuntimeException | IOException exception) {
108             throw new RuntimeException(
109                     "Failed to write fake HTTP flags file " + flagsFile, exception);
110         }
111     }
112 
113     @Override
114     public void close() {
115         if (mDataDir == null) return;
116 
117         File flagsFile = getFlagsFile();
118         if (!flagsFile.delete()) {
119             throw new RuntimeException("Failed to delete fake HTTP flags file " + flagsFile);
120         }
121         File flagsDir = flagsFile.getParentFile();
122         if (!flagsDir.delete()) {
123             throw new RuntimeException("Failed to delete fake HTTP flags dir " + flagsDir);
124         }
125         if (!mDataDir.delete()) {
126             throw new RuntimeException("Failed to delete fake HTTP flags data dir " + mDataDir);
127         }
128         mDataDir = null;
129     }
130 
131     private File getFlagsFile() {
132         return new File(
133                 new File(mDataDir, HttpFlagsLoader.FLAGS_FILE_DIR_NAME),
134                 HttpFlagsLoader.FLAGS_FILE_NAME);
135     }
136 }
137