Bug 1200445 - Expose android native apps trough the navigator.mozApps api r=snorp,ferjm

This commit is contained in:
Fabrice Desré 2015-09-14 09:30:28 -07:00
Родитель c1e44ec580
Коммит 1a84049c64
7 изменённых файлов: 384 добавлений и 46 удалений

123
dom/apps/AndroidUtils.jsm Normal file
Просмотреть файл

@ -0,0 +1,123 @@
/* 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/. */
const { interfaces: Ci, utils: Cu } = Components;
this.EXPORTED_SYMBOLS = ["AndroidUtils"];
Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
"resource://gre/modules/Messaging.jsm");
let appsRegistry = null;
function debug() {
//dump("-*- AndroidUtils " + Array.slice(arguments) + "\n");
}
// Helper functions to manage Android native apps. We keep them in the
// registry with a `kind` equals to "android-native" and we also store
// the package name and class name in the registry.
// Communication with the android side happens through json messages.
this.AndroidUtils = {
init: function(aRegistry) {
appsRegistry = aRegistry;
Services.obs.addObserver(this, "Android:Apps:Installed", false);
Services.obs.addObserver(this, "Android:Apps:Uninstalled", false);
},
uninit: function() {
Services.obs.removeObserver(this, "Android:Apps:Installed");
Services.obs.removeObserver(this, "Android:Apps:Uninstalled");
},
getOriginAndManifestURL: function(aPackageName) {
let origin = "android://" + aPackageName.toLowerCase();
let manifestURL = origin + "/manifest.webapp";
return [origin, manifestURL];
},
getPackageAndClassFromManifestURL: function(aManifestURL) {
debug("getPackageAndClassFromManifestURL " + aManifestURL);
let app = appsRegistry.getAppByManifestURL(aManifestURL);
if (!app) {
debug("No app for " + aManifestURL);
return [];
}
return [app.android_packagename, app.android_classname];
},
buildAndroidAppData: function(aApp) {
// Use the package and class name to get a unique origin.
// We put the version with the normal case as part of the manifest url.
let [origin, manifestURL] =
this.getOriginAndManifestURL(aApp.packagename);
// TODO: Bug 1204557 to improve the icons support.
let manifest = {
name: aApp.name,
icons: { "96": aApp.icon }
}
debug("Origin is " + origin);
let appData = {
app: {
installOrigin: origin,
origin: origin,
manifest: manifest,
manifestURL: manifestURL,
manifestHash: AppsUtils.computeHash(JSON.stringify(manifest)),
appStatus: Ci.nsIPrincipal.APP_STATUS_INSTALLED,
removable: aApp.removable,
android_packagename: aApp.packagename,
android_classname: aApp.classname
},
isBrowser: false,
isPackage: false
};
return appData;
},
installAndroidApps: function() {
return Messaging.sendRequestForResult({ type: "Apps:GetList" }).then(
aApps => {
debug("Got " + aApps.apps.length + " android apps.");
let promises = [];
aApps.apps.forEach(app => {
debug("App is " + app.name + " removable? " + app.removable);
let p = new Promise((aResolveInstall, aRejectInstall) => {
let appData = this.buildAndroidAppData(app);
appsRegistry.confirmInstall(appData, null, aResolveInstall);
});
promises.push(p);
});
// Wait for all apps to be installed.
return Promise.all(promises);
}
).then(appsRegistry._saveApps.bind(appsRegistry));
},
observe: function(aSubject, aTopic, aData) {
let data;
try {
data = JSON.parse(aData);
} catch(e) {
debug(e);
return;
}
if (aTopic == "Android:Apps:Installed") {
let appData = this.buildAndroidAppData(data);
appsRegistry.confirmInstall(appData);
} else if (aTopic == "Android:Apps:Uninstalled") {
let [origin, manifestURL] =
this.getOriginAndManifestURL(data.packagename);
appsRegistry.uninstall(manifestURL);
}
},
}

Просмотреть файл

@ -130,6 +130,10 @@ function _setAppProperties(aObj, aApp) {
aObj.kind = aApp.kind;
aObj.enabled = aApp.enabled !== undefined ? aApp.enabled : true;
aObj.sideloaded = aApp.sideloaded;
#ifdef MOZ_B2GDROID
aObj.android_packagename = aApp.android_packagename;
aObj.android_classname = aApp.android_classname;
#endif
}
this.AppsUtils = {

Просмотреть файл

@ -91,6 +91,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "ImportExport",
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
"resource://gre/modules/Messaging.jsm");
#ifdef MOZ_WIDGET_GONK
XPCOMUtils.defineLazyGetter(this, "libcutils", function() {
Cu.import("resource://gre/modules/systemlibs.js");
@ -195,6 +198,7 @@ this.DOMApplicationRegistry = {
get kPackaged() "packaged",
get kHosted() "hosted",
get kHostedAppcache() "hosted-appcache",
get kAndroid() "android-native",
// Path to the webapps.json file where we store the registry data.
appsFile: null,
@ -259,6 +263,11 @@ this.DOMApplicationRegistry = {
this.getFullAppByManifestURL.bind(this));
MessageBroadcaster.init(this.getAppByManifestURL);
if (AppConstants.MOZ_B2GDROID) {
Cu.import("resource://gre/modules/AndroidUtils.jsm");
AndroidUtils.init(this);
}
},
// loads the current registry, that could be empty on first run.
@ -464,7 +473,9 @@ this.DOMApplicationRegistry = {
}),
appKind: function(aApp, aManifest) {
if (aApp.origin.startsWith("app://")) {
if (aApp.origin.startsWith("android://")) {
return this.kAndroid;
} if (aApp.origin.startsWith("app://")) {
return this.kPackaged;
} else {
// Hosted apps, can be appcached or not.
@ -522,7 +533,10 @@ this.DOMApplicationRegistry = {
// Installs a 3rd party app.
installPreinstalledApp: function installPreinstalledApp(aId) {
#ifdef MOZ_WIDGET_GONK
if (!AppConstants.MOZ_B2GDROID && AppConstants.platform !== "gonk") {
return false;
}
// In some cases, the app might be already installed under a different ID but
// with the same manifestURL. In that case, the only content of the webapp will
// be the id of the old version, which is the one we'll keep.
@ -625,7 +639,6 @@ this.DOMApplicationRegistry = {
zipReader.close();
}
return isPreinstalled;
#endif
},
// For hosted apps, uninstall an app served from http:// if we have
@ -792,6 +805,10 @@ this.DOMApplicationRegistry = {
yield this.installSystemApps();
}
if (AppConstants.MOZ_B2GDROID) {
yield AndroidUtils.installAndroidApps();
}
// At first run, install preloaded apps and set up their permissions.
for (let id in this.webapps) {
let isPreinstalled = this.installPreinstalledApp(id);
@ -804,7 +821,7 @@ this.DOMApplicationRegistry = {
}
// Need to update the persisted list of apps since
// installPreinstalledApp() removes the ones failing to install.
this._saveApps();
yield this._saveApps();
Services.prefs.setBoolPref("dom.apps.reset-permissions", true);
}
@ -1195,6 +1212,9 @@ this.DOMApplicationRegistry = {
Services.obs.removeObserver(this, "xpcom-shutdown");
cpmm = null;
ppmm = null;
if (AppConstants.MOZ_B2GDROID) {
AndroidUtils.uninit();
}
} else if (aTopic == "memory-pressure") {
// Clear the manifest cache on memory pressure.
this._manifestCache = {};
@ -1304,22 +1324,22 @@ this.DOMApplicationRegistry = {
this.registryReady.then( () => {
switch (aMessage.name) {
case "Webapps:Install": {
#ifdef MOZ_WIDGET_ANDROID
Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
#else
this.doInstall(msg, mm);
#endif
if (AppConstants.platform == "android" && !AppConstants.MOZ_B2GDROID) {
Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
} else {
this.doInstall(msg, mm);
}
break;
}
case "Webapps:GetSelf":
this.getSelf(msg, mm);
break;
case "Webapps:Uninstall":
#ifdef MOZ_WIDGET_ANDROID
Services.obs.notifyObservers(mm, "webapps-runtime-uninstall", JSON.stringify(msg));
#else
this.doUninstall(msg, mm);
#endif
if (AppConstants.platform == "android" && !AppConstants.MOZ_B2GDROID) {
Services.obs.notifyObservers(mm, "webapps-runtime-uninstall", JSON.stringify(msg));
} else {
this.doUninstall(msg, mm);
}
break;
case "Webapps:Launch":
this.doLaunch(msg, mm);
@ -1337,11 +1357,11 @@ this.DOMApplicationRegistry = {
this.getNotInstalled(msg, mm);
break;
case "Webapps:InstallPackage": {
#ifdef MOZ_WIDGET_ANDROID
Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
#else
this.doInstallPackage(msg, mm);
#endif
if (AppConstants.platform == "android" && !AppConstants.MOZ_B2GDROID) {
Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
} else {
this.doInstallPackage(msg, mm);
}
break;
}
case "Webapps:Download":
@ -1602,6 +1622,19 @@ this.DOMApplicationRegistry = {
return;
}
// Delegate native android apps launch.
if (this.kAndroid == app.kind) {
debug("Launching android app " + app.origin);
let [packageName, className] =
AndroidUtils.getPackageAndClassFromManifestURL(aManifestURL);
debug(" " + packageName + " " + className);
Messaging.sendRequest({ type: "Apps:Launch",
packagename: packageName,
classname: className });
aOnSuccess();
return;
}
// We have to clone the app object as nsIDOMApplication objects are
// stringified as an empty object. (see bug 830376)
let appClone = AppsUtils.cloneAppObject(app);
@ -2046,8 +2079,9 @@ this.DOMApplicationRegistry = {
}
// If the app is packaged and its manifestURL has an app:// scheme,
// then we can't have an update.
if (app.kind == this.kPackaged && app.manifestURL.startsWith("app://")) {
// or if it's a native Android app then we can't have an update.
if (app.kind == this.kAndroid ||
(app.kind == this.kPackaged && app.manifestURL.startsWith("app://"))) {
sendError("NOT_UPDATABLE");
return;
}
@ -2779,8 +2813,10 @@ this.DOMApplicationRegistry = {
_setupApp: function(aData, aId) {
let app = aData.app;
// app can be uninstalled
app.removable = true;
// app can be uninstalled by default.
if (app.removable === undefined) {
app.removable = true;
}
if (aData.isPackage) {
// Override the origin with the correct id.
@ -2813,7 +2849,8 @@ this.DOMApplicationRegistry = {
appObject.downloading = true;
appObject.downloadSize = aLocaleManifest.size;
appObject.readyToApplyDownload = false;
} else if (appObject.kind == this.kHosted) {
} else if (appObject.kind == this.kHosted ||
appObject.kind == this.kAndroid) {
appObject.installState = "installed";
appObject.downloadAvailable = false;
appObject.downloading = false;
@ -2869,8 +2906,6 @@ this.DOMApplicationRegistry = {
app.appStatus = AppsUtils.getAppManifestStatus(aManifest);
app.removable = true;
// Reuse the app ID if the scheme is "app".
let uri = Services.io.newURI(app.origin, null, null);
if (uri.scheme == "app") {
@ -2960,6 +2995,7 @@ this.DOMApplicationRegistry = {
let appObject = this._cloneApp(aData, app, manifest, jsonManifest, id, localId);
this.webapps[id] = appObject;
this._manifestCache[id] = jsonManifest;
// For package apps, the permissions are not in the mini-manifest, so
// don't update the permissions yet.
@ -4064,12 +4100,24 @@ this.DOMApplicationRegistry = {
try {
aData.app = yield this._getAppWithManifest(aData.manifestURL);
let prefName = "dom.mozApps.auto_confirm_uninstall";
if (Services.prefs.prefHasUserValue(prefName) &&
Services.prefs.getBoolPref(prefName)) {
yield this._uninstallApp(aData.app);
if (this.kAndroid == aData.app.kind) {
debug("Uninstalling android app " + aData.app.origin);
let [packageName, className] =
AndroidUtils.getPackageAndClassFromManifestURL(aData.manifestURL);
Messaging.sendRequest({ type: "Apps:Uninstall",
packagename: packageName,
classname: className });
// We have to wait for Android's uninstall before sending the
// uninstall event, so fake an error here.
response = "Webapps:Uninstall:Return:KO";
} else {
yield this._promptForUninstall(aData);
let prefName = "dom.mozApps.auto_confirm_uninstall";
if (Services.prefs.prefHasUserValue(prefName) &&
Services.prefs.getBoolPref(prefName)) {
yield this._uninstallApp(aData.app);
} else {
yield this._promptForUninstall(aData);
}
}
} catch (error) {
aData.error = error;

Просмотреть файл

@ -42,6 +42,11 @@ EXTRA_JS_MODULES += [
'UserCustomizations.jsm',
]
if CONFIG['MOZ_B2GDROID']:
EXTRA_JS_MODULES += [
'AndroidUtils.jsm',
]
EXTRA_PP_JS_MODULES += [
'AppsUtils.jsm',
'ImportExport.jsm',

Просмотреть файл

@ -5,6 +5,7 @@
ANDROID_MANIFEST_FILE := src/main/AndroidManifest.xml
JAVAFILES := \
src/main/java/org/mozilla/b2gdroid/Apps.java \
src/main/java/org/mozilla/b2gdroid/Launcher.java \
src/main/java/org/mozilla/b2gdroid/ScreenStateObserver.java \
$(NULL)

Просмотреть файл

@ -0,0 +1,168 @@
/* 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.b2gdroid;
import java.io.ByteArrayOutputStream;
import java.util.Iterator;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import org.json.JSONObject;
import org.json.JSONArray;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
class Apps extends BroadcastReceiver
implements GeckoEventListener {
private static final String LOGTAG = "B2G:Apps";
private Context mContext;
Apps(Context context) {
mContext = context;
EventDispatcher.getInstance()
.registerGeckoThreadListener(this,
"Apps:GetList",
"Apps:Launch",
"Apps:Uninstall");
// Observe app installation and removal.
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
mContext.registerReceiver(this, filter);
}
void destroy() {
mContext.unregisterReceiver(this);
EventDispatcher.getInstance()
.unregisterGeckoThreadListener(this,
"Apps:GetList",
"Apps:Launch",
"Apps:Uninstall");
}
JSONObject activityInfoToJson(ActivityInfo info, PackageManager pm) {
JSONObject obj = new JSONObject();
try {
obj.put("name", info.loadLabel(pm).toString());
obj.put("packagename", info.packageName);
obj.put("classname", info.name);
final ApplicationInfo appInfo = info.applicationInfo;
// Pre-installed apps can't be uninstalled.
final boolean removable =
(appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
obj.put("removable", removable);
// For now, create a data: url for the icon, since we need additional
// android:// protocol support for icons. Once it's there we'll do
// something like: obj.put("icon", "android:icon/" + info.packageName);
Drawable d = pm.getApplicationIcon(info.packageName);
Bitmap bitmap = ((BitmapDrawable)d).getBitmap();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
obj.put("icon", "data:image/png;base64," + encoded);
} catch(Exception ex) {
Log.wtf(LOGTAG, "Error building ActivityInfo JSON", ex);
}
return obj;
}
public void handleMessage(String event, JSONObject message) {
Log.w(LOGTAG, "Received " + event);
if ("Apps:GetList".equals(event)) {
JSONObject ret = new JSONObject();
JSONArray array = new JSONArray();
PackageManager pm = mContext.getPackageManager();
final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
final Iterator<ResolveInfo> i = pm.queryIntentActivities(mainIntent, 0).iterator();
try {
while (i.hasNext()) {
ActivityInfo info = i.next().activityInfo;
array.put(activityInfoToJson(info, pm));
}
ret.put("apps", array);
} catch(Exception ex) {
Log.wtf(LOGTAG, "error, making list of apps", ex);
}
EventDispatcher.sendResponse(message, ret);
} else if ("Apps:Launch".equals(event)) {
try {
String className = message.getString("classname");
String packageName = message.getString("packagename");
final Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setClassName(packageName, className);
mContext.startActivity(intent);
} catch(Exception ex) {
Log.wtf(LOGTAG, "Error launching app", ex);
}
} else if ("Apps:Uninstall".equals(event)) {
try {
String packageName = message.getString("packagename");
Uri packageUri = Uri.parse("package:" + packageName);
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
mContext.startActivity(intent);
} catch(Exception ex) {
Log.wtf(LOGTAG, "Error uninstalling app", ex);
}
}
}
@Override
public void onReceive(Context context, Intent intent) {
Log.d(LOGTAG, intent.getAction() + " " + intent.getDataString());
String packageName = intent.getDataString().substring(8);
String action = intent.getAction();
if ("android.intent.action.PACKAGE_ADDED".equals(action)) {
PackageManager pm = mContext.getPackageManager();
Intent launch = pm.getLaunchIntentForPackage(packageName);
if (launch == null) {
Log.d(LOGTAG, "No launchable intent for " + packageName);
return;
}
ActivityInfo info = launch.resolveActivityInfo(pm, 0);
JSONObject obj = activityInfoToJson(info, pm);
GeckoEvent e = GeckoEvent.createBroadcastEvent("Android:Apps:Installed", obj.toString());
GeckoAppShell.sendEventToGecko(e);
} else if ("android.intent.action.PACKAGE_REMOVED".equals(action)) {
JSONObject obj = new JSONObject();
try {
obj.put("packagename", packageName);
} catch(Exception ex) {
Log.wtf(LOGTAG, "Error building PACKAGE_REMOVED JSON", ex);
}
GeckoEvent e = GeckoEvent.createBroadcastEvent("Android:Apps:Uninstalled", obj.toString());
GeckoAppShell.sendEventToGecko(e);
}
}
}

Просмотреть файл

@ -4,9 +4,6 @@
package org.mozilla.b2gdroid;
import java.io.ByteArrayOutputStream;
import java.util.Iterator;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.KeyguardManager;
@ -14,24 +11,12 @@ import android.app.KeyguardManager.KeyguardLock;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.BaseGeckoInterface;
import org.mozilla.gecko.ContactService;
@ -45,6 +30,7 @@ import org.mozilla.gecko.IntentHelper;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.b2gdroid.ScreenStateObserver;
import org.mozilla.b2gdroid.Apps;
public class Launcher extends Activity
implements GeckoEventListener, ContextGetter {
@ -52,6 +38,7 @@ public class Launcher extends Activity
private ContactService mContactService;
private ScreenStateObserver mScreenStateObserver;
private Apps mApps;
/** ContextGetter */
public Context getContext() {
@ -68,6 +55,7 @@ public class Launcher extends Activity
GeckoBatteryManager.getInstance().start(this);
mContactService = new ContactService(EventDispatcher.getInstance(), this);
mApps = new Apps(this);
}
private void hideSplashScreen() {
@ -123,6 +111,7 @@ public class Launcher extends Activity
"Launcher:Ready");
mContactService.destroy();
mApps.destroy();
}
@Override