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