pt 1 in reviving the android build (copied from pm 28a1, wish me luck)

This commit is contained in:
wuggy 2026-04-08 15:43:25 -07:00
commit d7788a6d6d
4249 changed files with 468189 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mozilla.gecko.distribution;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.mozilla.gecko.GeckoSharedPrefs;
import java.lang.ref.WeakReference;
/**
* A distribution ready callback that will store the distribution ID to profile-specific shared preferences.
*/
public class DistributionStoreCallback implements Distribution.ReadyCallback {
private static final String LOGTAG = "Gecko" + DistributionStoreCallback.class.getSimpleName();
public static final String PREF_DISTRIBUTION_ID = "distribution.id";
private final WeakReference<Context> contextReference;
private final String profileName;
public DistributionStoreCallback(final Context context, final String profileName) {
this.contextReference = new WeakReference<>(context);
this.profileName = profileName;
}
public void distributionNotFound() { /* nothing to do here */ }
@Override
public void distributionFound(final Distribution distribution) {
storeDistribution(distribution);
}
@Override
public void distributionArrivedLate(final Distribution distribution) {
storeDistribution(distribution);
}
private void storeDistribution(final Distribution distribution) {
final Context context = contextReference.get();
if (context == null) {
Log.w(LOGTAG, "Context is no longer alive, could retrieve shared prefs to store distribution");
return;
}
// While the distribution preferences are per install and not per profile, it's okay to use the
// profile-specific prefs because:
// 1) We don't really support mulitple profiles for end-users
// 2) The TelemetryUploadService already accesses profile-specific shared prefs so this keeps things simple.
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(context, profileName);
final Distribution.DistributionDescriptor desc = distribution.getDescriptor();
if (desc != null) {
sharedPrefs.edit().putString(PREF_DISTRIBUTION_ID, desc.id).apply();
}
}
}

View file

@ -0,0 +1,322 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.distribution;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.db.BrowserContract;
import java.util.HashSet;
import java.util.Set;
/**
* A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers
* will be transformed so that they do not overlap with the ids from the local database.
*
* Bookmarks in folder:
* content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId}
* Icon of bookmark:
* content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId}
*/
public class PartnerBookmarksProviderProxy extends ContentProvider {
/**
* The contract between the partner bookmarks provider and applications. Contains the definition
* for the supported URIs and columns.
*/
public static class PartnerContract {
public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_FOLDER = 2;
public static final int PARENT_ROOT_ID = 0;
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String FAVICON = "favicon";
public static final String TOUCHICON = "touchicon";
public static final String PARENT = "parent";
}
private static final String AUTHORITY_PREFIX = ".partnerbookmarks";
private static final int URI_MATCH_BOOKMARKS = 1000;
private static final int URI_MATCH_ICON = 1001;
private static final int URI_MATCH_BOOKMARK = 1002;
private static final String PREF_DELETED_PARTNER_BOOKMARKS = "distribution.partner.bookmark.deleted";
/**
* Cursor wrapper for filtering empty folders.
*/
private static class FilteredCursor extends CursorWrapper {
private HashSet<Integer> emptyFolderPositions;
private int count;
public FilteredCursor(PartnerBookmarksProviderProxy proxy, Cursor cursor) {
super(cursor);
emptyFolderPositions = new HashSet<>();
count = cursor.getCount();
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE));
if (type == BrowserContract.Bookmarks.TYPE_FOLDER && proxy.isFolderEmpty(id)) {
// We do not support deleting folders. So at least hide partner folders that are
// empty because all bookmarks inside it are deleted/hidden.
// Note that this will still show folders with empty folders in them. But multi-level
// partner bookmarks are very unlikely.
count--;
emptyFolderPositions.add(i);
}
}
}
@Override
public int getCount() {
return count;
}
@Override
public boolean moveToPosition(int position) {
final Cursor cursor = getWrappedCursor();
final int actualCount = cursor.getCount();
// Find the next position pointing to a bookmark or a non-empty folder
while (position < actualCount && emptyFolderPositions.contains(position)) {
position++;
}
return position < actualCount && cursor.moveToPosition(position);
}
}
private static String getAuthority(Context context) {
return context.getPackageName() + AUTHORITY_PREFIX;
}
public static Uri getUriForBookmarks(Context context, long folderId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmarks")
.appendPath(String.valueOf(folderId))
.build();
}
public static Uri getUriForIcon(Context context, long bookmarkId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("icons")
.appendPath(String.valueOf(bookmarkId))
.build();
}
public static Uri getUriForBookmark(Context context, long bookmarkId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmark")
.appendPath(String.valueOf(bookmarkId))
.build();
}
private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@Override
public boolean onCreate() {
String authority = getAuthority(assertAndGetContext());
uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS);
uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON);
uriMatcher.addURI(authority, "bookmark/*", URI_MATCH_BOOKMARK);
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final Context context = assertAndGetContext();
final int match = uriMatcher.match(uri);
final ContentResolver contentResolver = context.getContentResolver();
switch (match) {
case URI_MATCH_BOOKMARKS:
final long bookmarkId = ContentUris.parseId(uri);
if (bookmarkId == -1) {
throw new IllegalArgumentException("Bookmark id is not a number");
}
final Cursor cursor = getBookmarksInFolder(contentResolver, bookmarkId);
cursor.setNotificationUri(context.getContentResolver(), uri);
return new FilteredCursor(this, cursor);
case URI_MATCH_ICON:
return getIcon(contentResolver, ContentUris.parseId(uri));
default:
throw new UnsupportedOperationException("Unknown URI " + uri.toString());
}
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
final int match = uriMatcher.match(uri);
switch (match) {
case URI_MATCH_BOOKMARK:
rememberRemovedBookmark(ContentUris.parseId(uri));
notifyBookmarkChange();
return 1;
default:
throw new UnsupportedOperationException("Unknown URI " + uri.toString());
}
}
private void notifyBookmarkChange() {
final Context context = assertAndGetContext();
context.getContentResolver().notifyChange(
new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmarks")
.build(),
null);
}
private synchronized void rememberRemovedBookmark(long bookmarkId) {
Set<String> deletedIds = getRemovedBookmarkIds();
deletedIds.add(String.valueOf(bookmarkId));
GeckoSharedPrefs.forProfile(assertAndGetContext())
.edit()
.putStringSet(PREF_DELETED_PARTNER_BOOKMARKS, deletedIds)
.apply();
}
private synchronized Set<String> getRemovedBookmarkIds() {
SharedPreferences preferences = GeckoSharedPrefs.forProfile(assertAndGetContext());
return preferences.getStringSet(PREF_DELETED_PARTNER_BOOKMARKS, new HashSet<String>());
}
private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) {
// Use root folder id or transform negative id into actual (positive) folder id.
final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
? PartnerContract.PARENT_ROOT_ID
: BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
final String removedBookmarkIds = TextUtils.join(",", getRemovedBookmarkIds());
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
// Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID,
PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL,
// Transform parent ids to negative ids as well
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
// Convert types (we use 0-1 and the partner provider 1-2)
"(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
// Use the ID of the entry as GUID
PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
},
PartnerContract.PARENT + " = ?"
// We only want to read bookmarks or folders from the content provider
+ " AND " + BrowserContract.Bookmarks.TYPE + " IN (?,?)"
// Only select entries with non empty title
+ " AND " + BrowserContract.Bookmarks.TITLE + " <> ''"
// Filter all "deleted" ids
+ " AND " + BrowserContract.Combined.BOOKMARK_ID + " NOT IN (" + removedBookmarkIds + ")",
new String[] {
String.valueOf(actualFolderId),
String.valueOf(PartnerContract.TYPE_BOOKMARK),
String.valueOf(PartnerContract.TYPE_FOLDER)
},
// Same order we use in our content provider (without position)
BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
}
private boolean isFolderEmpty(long folderId) {
final Context context = assertAndGetContext();
final Cursor cursor = getBookmarksInFolder(context.getContentResolver(), folderId);
if (cursor == null) {
return true;
}
try {
return cursor.getCount() == 0;
} finally {
cursor.close();
}
}
private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) {
final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId;
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
PartnerContract.TOUCHICON,
PartnerContract.FAVICON
},
PartnerContract.ID + " = ?",
new String[] {
String.valueOf(actualId)
},
null);
}
private Context assertAndGetContext() {
final Context context = super.getContext();
if (context == null) {
throw new AssertionError("Context is null");
}
return context;
}
@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,43 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.distribution;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
/**
* Client for accessing data from Android's "partner browser customizations" content provider.
*/
public class PartnerBrowserCustomizationsClient {
private static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbrowsercustomizations");
private static final Uri HOMEPAGE_URI = CONTENT_URI.buildUpon().path("homepage").build();
private static final String COLUMN_HOMEPAGE = "homepage";
/**
* Returns the partner homepage or null if it could not be read from the content provider.
*/
public static String getHomepage(Context context) {
Cursor cursor = context.getContentResolver().query(
HOMEPAGE_URI, new String[] { COLUMN_HOMEPAGE }, null, null, null);
if (cursor == null) {
return null;
}
try {
if (!cursor.moveToFirst()) {
return null;
}
return cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HOMEPAGE));
} finally {
cursor.close();
}
}
}

View file

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.distribution;
import org.mozilla.gecko.annotation.RobocopTarget;
import android.net.Uri;
import java.net.URLDecoder;
import java.io.UnsupportedEncodingException;
/**
* Encapsulates access to values encoded in the "referrer" extra of an install intent.
*
* This object is immutable.
*
* Example input:
*
* "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name"
*/
@RobocopTarget
public class ReferrerDescriptor {
public final String source;
public final String medium;
public final String term;
public final String content;
public final String campaign;
public ReferrerDescriptor(String referrer) {
if (referrer == null) {
source = null;
medium = null;
term = null;
content = null;
campaign = null;
return;
}
try {
referrer = URLDecoder.decode(referrer, "UTF-8");
} catch (UnsupportedEncodingException e) {
// UTF-8 is always supported
}
final Uri u = new Uri.Builder()
.scheme("http")
.authority("local")
.path("/")
.encodedQuery(referrer).build();
source = u.getQueryParameter("utm_source");
medium = u.getQueryParameter("utm_medium");
term = u.getQueryParameter("utm_term");
content = u.getQueryParameter("utm_content");
campaign = u.getQueryParameter("utm_campaign");
}
@Override
public String toString() {
return "{s: " + source + ", m: " + medium + ", t: " + term + ", c: " + content + ", c: " + campaign + "}";
}
}

View file

@ -0,0 +1,107 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.distribution;
import org.mozilla.gecko.AdjustConstants;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
public class ReferrerReceiver extends BroadcastReceiver {
private static final String LOGTAG = "GeckoReferrerReceiver";
private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
// Sent when we're done.
@RobocopTarget
public static final String ACTION_REFERRER_RECEIVED = "org.mozilla.fennec.REFERRER_RECEIVED";
/**
* If the install intent has this source, it is a Mozilla specific or over
* the air distribution referral. We'll track the campaign ID using
* Mozilla's metrics systems.
*
* If the install intent has a source different than this one, it is a
* referral from an advertising network. We may track these campaigns using
* third-party tracking and metrics systems.
*/
private static final String MOZILLA_UTM_SOURCE = "mozilla";
/**
* If the install intent has this campaign, we'll load the specified distribution.
*/
private static final String DISTRIBUTION_UTM_CAMPAIGN = "distribution";
@Override
public void onReceive(Context context, Intent intent) {
Log.v(LOGTAG, "Received intent " + intent);
if (!ACTION_INSTALL_REFERRER.equals(intent.getAction())) {
// This should never happen.
return;
}
// Track the referrer object for distribution handling.
ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer"));
if (!TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
// Allow the Adjust handler to process the intent.
try {
AdjustConstants.getAdjustHelper().onReceive(context, intent);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception in Adjust's onReceive; ignoring referrer intent.", e);
}
return;
}
if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) {
Distribution.onReceivedReferrer(context, referrer);
// We want Adjust information for OTA distributions as well
try {
AdjustConstants.getAdjustHelper().onReceive(context, intent);
} catch (Exception e) {
Log.e(LOGTAG, "Got exception in Adjust's onReceive for distribution.", e);
}
} else {
Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
// If this is a Mozilla campaign, pass the campaign along to Gecko.
// It'll pretend to be a "playstore" distribution for BLP purposes.
propagateMozillaCampaign(referrer);
}
// Broadcast a secondary, local intent to allow test code to respond.
final Intent receivedIntent = new Intent(ACTION_REFERRER_RECEIVED);
LocalBroadcastManager.getInstance(context).sendBroadcast(receivedIntent);
}
private void propagateMozillaCampaign(ReferrerDescriptor referrer) {
if (referrer.campaign == null) {
return;
}
try {
final JSONObject data = new JSONObject();
data.put("id", "playstore");
data.put("version", referrer.campaign);
String payload = data.toString();
// Try to make sure the prefs are written as a group.
GeckoAppShell.notifyObservers("Campaign:Set", payload);
} catch (JSONException e) {
Log.e(LOGTAG, "Error propagating campaign identifier.", e);
}
}
}