diff --git a/configure.in b/configure.in index 03115d4b1ab..ffcfedf9d27 100644 --- a/configure.in +++ b/configure.in @@ -8540,6 +8540,7 @@ AC_SUBST(MOZ_DISABLE_GECKOVIEW) AC_SUBST(MOZ_ANDROID_GCM) AC_SUBST(MOZ_ANDROID_GECKOLIBS_AAR) AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY) +AC_SUBST(MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) AC_SUBST(MOZ_ANDROID_MLS_STUMBLER) AC_SUBST(MOZ_ANDROID_DOWNLOADS_INTEGRATION) AC_SUBST(MOZ_ANDROID_APPLICATION_CLASS) diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index fc6bf1ab621..8deece60627 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -886,9 +886,13 @@ ANDROID_ASSETS_DIRS += [ ] if CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY']: - ANDROID_ASSETS_DIRS += [ - '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets', - ] + # If you change this, also change its equivalent in mobile/android/bouncer. + if not CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']: + # If we are packaging the bouncer, it will have the distribution, so don't put + # it in the main APK as well. + ANDROID_ASSETS_DIRS += [ + '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets', + ] # We do not expose MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN here because that # would leak the value to build logs. Instead we expose the token quietly where diff --git a/mobile/android/bouncer/AndroidManifest.xml.in b/mobile/android/bouncer/AndroidManifest.xml.in new file mode 100644 index 00000000000..34345a2b6b6 --- /dev/null +++ b/mobile/android/bouncer/AndroidManifest.xml.in @@ -0,0 +1,54 @@ +#filter substitution + + + + + +#include ../base/FennecManifest_permissions.xml.in + + +#else + android:debuggable="false"> +#endif + + + + + + + + + + + + diff --git a/mobile/android/bouncer/Makefile.in b/mobile/android/bouncer/Makefile.in new file mode 100644 index 00000000000..efc6841a04a --- /dev/null +++ b/mobile/android/bouncer/Makefile.in @@ -0,0 +1,25 @@ +# 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/. + +include $(topsrcdir)/config/config.mk + +JAVAFILES := \ + java/org/mozilla/bouncer/BouncerService.java \ + java/org/mozilla/gecko/BrowserApp.java \ + $(NULL) + +ANDROID_EXTRA_JARS := \ + $(NULL) + +PP_TARGETS += manifest +manifest := $(srcdir)/AndroidManifest.xml.in +manifest_TARGET := export +# Special 'cuz they are set in mobile/android/defs.mk. +manifest_FLAGS += \ + -DMOZ_ANDROID_SHARED_ID="$(MOZ_ANDROID_SHARED_ID)" \ + -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE)" \ + -DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE)" \ + $(NULL) + +libs:: $(ANDROID_APK_NAME).apk diff --git a/mobile/android/bouncer/assets/example_asset.txt b/mobile/android/bouncer/assets/example_asset.txt new file mode 100644 index 00000000000..34338f983ea --- /dev/null +++ b/mobile/android/bouncer/assets/example_asset.txt @@ -0,0 +1 @@ +This is an example asset. diff --git a/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java b/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java new file mode 100644 index 00000000000..b33d1a9ca5b --- /dev/null +++ b/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java @@ -0,0 +1,129 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.bouncer; + +import android.app.IntentService; +import android.content.Intent; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class BouncerService extends IntentService { + + private static final String LOGTAG = "GeckoBouncerService"; + + public BouncerService() { + super("BouncerService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + final byte[] buffer = new byte[8192]; + + Log.d(LOGTAG, "Preparing to copy distribution files"); + + final List files; + try { + files = getFiles("distribution"); + } catch (IOException e) { + Log.e(LOGTAG, "Error getting distribution files from assets/distribution/**", e); + return; + } + + InputStream in = null; + for (String path : files) { + try { + Log.d(LOGTAG, "Copying distribution file: " + path); + + in = getAssets().open(path); + + final File outFile = getDataFile(path); + writeStream(in, outFile, buffer); + } catch (IOException e) { + Log.e(LOGTAG, "Error opening distribution input stream from assets", e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + Log.e(LOGTAG, "Error closing distribution input stream", e); + } + } + } + } + } + + /** + * Recursively traverse a directory to list paths to all files. + * + * @param path Directory to traverse. + * @return List of all files in given directory. + * @throws IOException + */ + private List getFiles(String path) throws IOException { + List paths = new ArrayList<>(); + getFiles(path, paths); + return paths; + } + + /** + * Recursively traverse a directory to list paths to all files. + * + * @param path Directory to traverse. + * @param acc Accumulator of paths seen. + * @throws IOException + */ + private void getFiles(String path, List acc) throws IOException { + final String[] list = getAssets().list(path); + if (list.length > 0) { + // We're a directory -- recurse. + for (final String file : list) { + getFiles(path + "/" + file, acc); + } + } else { + // We're a file -- accumulate. + acc.add(path); + } + } + + private String getDataDir() { + return getApplicationInfo().dataDir; + } + + private File getDataFile(final String path) { + File outFile = new File(getDataDir(), path); + File dir = outFile.getParentFile(); + + if (dir != null && !dir.exists()) { + Log.d(LOGTAG, "Creating " + dir.getAbsolutePath()); + if (!dir.mkdirs()) { + Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); + return null; + } + } + + return outFile; + } + + private void writeStream(InputStream fileStream, File outFile, byte[] buffer) + throws IOException { + final OutputStream outStream = new FileOutputStream(outFile); + try { + int count; + while ((count = fileStream.read(buffer)) > 0) { + outStream.write(buffer, 0, count); + } + } finally { + outStream.close(); + } + } +} diff --git a/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java new file mode 100644 index 00000000000..8a462822f5c --- /dev/null +++ b/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java @@ -0,0 +1,46 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import org.mozilla.bouncer.BouncerService; + +/** + * Bouncer activity version of BrowserApp. + * + * This class has the same name as org.mozilla.gecko.BrowserApp to preserve + * shortcuts created by the bouncer app. + */ +public class BrowserApp extends Activity { + private static final String LOGTAG = "GeckoBouncerActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // This races distribution installation against the Play Store killing our process to + // install the update. We'll live with it. To do better, consider using an Intent to + // notify when the service has completed. + startService(new Intent(this, BouncerService.class)); + + final String appPackageName = Uri.encode(getPackageName()); + final Uri uri = Uri.parse("market://details?id=" + appPackageName); + Log.i(LOGTAG, "Lanching activity with URL: " + uri.toString()); + + // It might be more correct to catch failure in case the Play Store isn't installed. The + // fallback action is to open the Play Store website... but doing so may offer Firefox as + // browser (since even the bouncer offers to view URLs), which will be very confusing. + // Therefore, we don't try to be fancy here, and we just fail (silently). + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + + finish(); + } +} diff --git a/mobile/android/bouncer/moz.build b/mobile/android/bouncer/moz.build new file mode 100644 index 00000000000..07b2ba01d0a --- /dev/null +++ b/mobile/android/bouncer/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DEFINES['ANDROID_VERSION_CODE'] = '1' + +for var in ('ANDROID_PACKAGE_NAME', + 'MOZ_ANDROID_BROWSER_INTENT_CLASS', + 'MOZ_APP_DISPLAYNAME', + 'MOZ_APP_VERSION'): + DEFINES[var] = CONFIG[var] + +ANDROID_APK_NAME = 'bouncer' +ANDROID_APK_PACKAGE = CONFIG['ANDROID_PACKAGE_NAME'] + +# Putting branding earlier allows branders to override default resources. +ANDROID_RES_DIRS += [ + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res', # For the icon. + 'res', +] + +ANDROID_ASSETS_DIRS += [ + 'assets', +] + +if CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY']: + # If you change this, also change its equivalent in mobile/android/base. + ANDROID_ASSETS_DIRS += [ + '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets', + ] diff --git a/mobile/android/bouncer/res/drawable-v21/logo.xml b/mobile/android/bouncer/res/drawable-v21/logo.xml new file mode 100644 index 00000000000..568cbec00e0 --- /dev/null +++ b/mobile/android/bouncer/res/drawable-v21/logo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/mobile/android/bouncer/res/drawable/logo.xml b/mobile/android/bouncer/res/drawable/logo.xml new file mode 100644 index 00000000000..e188f80dce5 --- /dev/null +++ b/mobile/android/bouncer/res/drawable/logo.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh index 8995965c4a9..4156bff85d4 100644 --- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -93,6 +93,9 @@ MOZ_ANDROID_MLS_STUMBLER=1 # Enable adding to the system downloads list. MOZ_ANDROID_DOWNLOADS_INTEGRATION=1 +# Build and package the install bouncer APK by default. +MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER=1 + # Use the low-memory GC tuning. export JS_GC_SMALL_CHUNK_SIZE=1 diff --git a/mobile/android/moz.build b/mobile/android/moz.build index 1aab30e6be7..049293ed682 100644 --- a/mobile/android/moz.build +++ b/mobile/android/moz.build @@ -26,6 +26,9 @@ DIRS += [ 'geckoview_library', ] +if CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']: + DIRS += ['bouncer'] # No ordering implied with respect to base. + DIRS += ['../../xulrunner/tools/redit'] TEST_DIRS += [ diff --git a/toolkit/mozapps/installer/upload-files.mk b/toolkit/mozapps/installer/upload-files.mk index abafb8673a3..7dad5707a15 100644 --- a/toolkit/mozapps/installer/upload-files.mk +++ b/toolkit/mozapps/installer/upload-files.mk @@ -337,6 +337,16 @@ else INNER_ROBOCOP_PACKAGE=echo 'Testing is disabled - No Android Robocop for you' endif +ifdef MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER +UPLOAD_EXTRA_FILES += bouncer.apk + +# Package and release sign the install bouncer APK. +INNER_INSTALL_BOUNCER_PACKAGE=\ + $(call RELEASE_SIGN_ANDROID_APK,$(topobjdir)/mobile/android/bouncer/bouncer-unsigned-unaligned.apk,$(ABS_DIST)/bouncer.apk) +else +INNER_INSTALL_BOUNCER_PACKAGE=echo 'Install bouncer is disabled - No trampolines for you' +endif # MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER + # Create geckoview_library/geckoview_{assets,library}.zip for third-party GeckoView consumers. ifdef NIGHTLY_BUILD ifndef MOZ_DISABLE_GECKOVIEW @@ -478,6 +488,7 @@ INNER_MAKE_PACKAGE = \ (echo "*** Error: The R.txt that was built and the R.txt that is being packaged are not the same. Rebuild mobile/android/base and re-package." && exit 1)) && \ $(INNER_MAKE_APK) && \ $(INNER_ROBOCOP_PACKAGE) && \ + $(INNER_INSTALL_BOUNCER_PACKAGE) && \ $(INNER_MAKE_GECKOLIBS_AAR) && \ $(INNER_MAKE_GECKOVIEW_LIBRARY) endif