package cc.cleartrust.bundlepixel.harness; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.webkit.ConsoleMessage; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; /** * Bundle Pixel harness: a real app with a real Android System WebView. * It loads six ad setups the way apps and ad SDKs do, so the bundle-pixel tag * can be checked in a genuine app environment. Results land on the test bench. */ public class MainActivity extends Activity { static final String TAG = "BPHarness"; static final String DEFAULT_SERVER = "__DEFAULT_SERVER__"; static final String[] MODES = {"plain", "url", "mraid3-env", "mraid3-getenv", "mraid2-macro", "xo"}; static final long STEP_MS = 4500; WebView web; EditText server; TextView status; Button runBtn; final Handler ui = new Handler(Looper.getMainLooper()); String runId, base, device; int idx; @Override protected void onCreate(Bundle state) { super.onCreate(state); SharedPreferences prefs = getSharedPreferences("bp", MODE_PRIVATE); device = Build.MANUFACTURER + " " + Build.MODEL + " · Android " + Build.VERSION.RELEASE; LinearLayout root = new LinearLayout(this); root.setOrientation(LinearLayout.VERTICAL); int pad = (int) (12 * getResources().getDisplayMetrics().density); root.setPadding(pad, pad, pad, 0); LinearLayout bar = new LinearLayout(this); bar.setOrientation(LinearLayout.HORIZONTAL); server = new EditText(this); server.setSingleLine(true); server.setTextSize(13); server.setImeOptions(EditorInfo.IME_ACTION_GO); server.setHint("Test bench URL"); server.setText(prefs.getString("server", DEFAULT_SERVER)); bar.addView(server, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); runBtn = new Button(this); runBtn.setText("Run all"); runBtn.setOnClickListener(v -> runAll()); bar.addView(runBtn); root.addView(bar); status = new TextView(this); status.setTextSize(12); status.setText(getPackageName() + "\n" + device + "\nWebView " + webViewVersion()); root.addView(status); web = new WebView(this); WebSettings s = web.getSettings(); s.setJavaScriptEnabled(true); s.setDomStorageEnabled(true); WebView.setWebContentsDebuggingEnabled(true); web.setWebViewClient(new WebViewClient()); web.setWebChromeClient(new WebChromeClient() { @Override public boolean onConsoleMessage(ConsoleMessage m) { Log.d(TAG, m.messageLevel() + " " + m.message() + " @" + m.sourceId() + ":" + m.lineNumber()); return true; } }); root.addView(web, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); setContentView(root); handle(getIntent()); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handle(intent); } // adb shell am start -n cc.cleartrust.bundlepixel.harness/.MainActivity --es server http://10.0.2.2:5420 --ez autorun true void handle(Intent intent) { if (intent == null) return; String s = intent.getStringExtra("server"); if (s != null && !s.isEmpty()) server.setText(s); if (intent.getBooleanExtra("autorun", false)) ui.postDelayed(this::runAll, 800); } String webViewVersion() { try { if (Build.VERSION.SDK_INT >= 26 && WebView.getCurrentWebViewPackage() != null) return WebView.getCurrentWebViewPackage().packageName + " " + WebView.getCurrentWebViewPackage().versionName; } catch (Exception ignored) { } return "(unknown)"; } void runAll() { base = server.getText().toString().trim().replaceAll("/+$", ""); getSharedPreferences("bp", MODE_PRIVATE).edit().putString("server", base).apply(); runId = "a" + Long.toString(System.currentTimeMillis(), 36); idx = 0; runBtn.setEnabled(false); ui.removeCallbacksAndMessages(null); next(); } void next() { if (idx >= MODES.length) { status.setText("Done · run " + runId + " · results below and on the test bench"); web.loadUrl(base + "/real/report.html?run=" + runId); runBtn.setEnabled(true); Log.i(TAG, "RUN_DONE " + runId); return; } String mode = MODES[idx++]; status.setText("Running " + idx + "/" + MODES.length + ": " + mode + " · run " + runId); Log.i(TAG, "MODE " + mode + " run " + runId); load(mode); ui.postDelayed(this::next, STEP_MS); } static String enc(String v) { try { return URLEncoder.encode(v, "UTF-8"); } catch (Exception e) { return v; } } void load(final String mode) { final String pkg = getPackageName(); final String q = "mode=" + mode + "&run=" + runId + "&dev=" + enc(device) + "&hp=" + enc(pkg) + "&plat=android"; switch (mode) { case "plain": case "xo": web.loadUrl(base + "/real/page.html?" + q); return; case "url": web.loadUrl(base + "/real/page.html?" + q + "&bundle=" + enc(pkg)); return; default: // Like an ad SDK: fetch the ad markup, wrap it with the MRAID bridge, load it with a base URL. new Thread(() -> { String html; try { html = fetch(base + "/real/creative.html?" + q); } catch (Exception e) { html = "
could not fetch creative: " + e + "
"; } if (mode.equals("mraid2-macro")) html = html.replace("%%APP_BUNDLE%%", pkg); String version = mode.startsWith("mraid2") ? "2.0" : "3.0"; String page = "" + "" + (mode.equals("mraid3-env") ? "" : "") + "" + html + ""; ui.post(() -> web.loadDataWithBaseURL(base + "/real/sdk", page, "text/html", "utf-8", null)); }).start(); } } static String fetch(String url) throws Exception { HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection(); c.setConnectTimeout(8000); c.setReadTimeout(8000); try (InputStream in = c.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) > 0) out.write(buf, 0, n); return out.toString("UTF-8"); } finally { c.disconnect(); } } }