2014-01-09 10:18:55 -08:00
|
|
|
/* -*- 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.webapp;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.IOException;
|
2014-04-16 08:42:15 -07:00
|
|
|
import java.net.URI;
|
2014-01-09 10:18:55 -08:00
|
|
|
|
|
|
|
import org.json.JSONException;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import org.mozilla.gecko.GeckoApp;
|
|
|
|
import org.mozilla.gecko.GeckoAppShell;
|
|
|
|
import org.mozilla.gecko.GeckoEvent;
|
|
|
|
import org.mozilla.gecko.GeckoThread;
|
|
|
|
import org.mozilla.gecko.R;
|
|
|
|
import org.mozilla.gecko.Tab;
|
|
|
|
import org.mozilla.gecko.Tabs;
|
2014-10-28 14:28:31 -07:00
|
|
|
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
|
2014-06-05 19:38:01 -07:00
|
|
|
import org.mozilla.gecko.util.NativeJSObject;
|
2014-01-09 10:18:55 -08:00
|
|
|
import org.mozilla.gecko.webapp.InstallHelper.InstallCallback;
|
|
|
|
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.pm.PackageManager.NameNotFoundException;
|
|
|
|
import android.graphics.Color;
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
import android.graphics.drawable.GradientDrawable;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.os.Bundle;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.view.Display;
|
|
|
|
import android.view.View;
|
|
|
|
import android.view.animation.Animation;
|
|
|
|
import android.view.animation.AnimationUtils;
|
|
|
|
import android.widget.ImageView;
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
2014-02-11 23:41:05 -08:00
|
|
|
public class WebappImpl extends GeckoApp implements InstallCallback {
|
|
|
|
private static final String LOGTAG = "GeckoWebappImpl";
|
2014-01-09 10:18:55 -08:00
|
|
|
|
2014-04-16 08:42:15 -07:00
|
|
|
private URI mOrigin;
|
2014-07-25 20:14:47 -07:00
|
|
|
private TextView mTitlebarText;
|
|
|
|
private View mTitlebar;
|
2014-01-09 10:18:55 -08:00
|
|
|
|
2014-07-28 17:32:59 -07:00
|
|
|
// Must only be accessed from the UI thread.
|
2014-09-08 18:11:51 -07:00
|
|
|
View mSplashscreen;
|
2014-01-09 10:18:55 -08:00
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
private boolean mIsApk = true;
|
2014-01-09 10:18:55 -08:00
|
|
|
private ApkResources mApkResources;
|
2014-03-28 11:27:46 -07:00
|
|
|
private String mManifestUrl;
|
|
|
|
private String mAppName;
|
2014-01-09 10:18:55 -08:00
|
|
|
|
|
|
|
protected int getIndex() { return 0; }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getLayout() { return R.layout.web_app; }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean hasTabsSideBar() { return false; }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCreate(Bundle savedInstance)
|
|
|
|
{
|
|
|
|
|
|
|
|
String action = getIntent().getAction();
|
|
|
|
Bundle extras = getIntent().getExtras();
|
|
|
|
if (extras == null) {
|
|
|
|
extras = savedInstance;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (extras == null) {
|
|
|
|
extras = new Bundle();
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean isInstalled = extras.getBoolean("isInstalled", false);
|
|
|
|
String packageName = extras.getString("packageName");
|
|
|
|
|
|
|
|
if (packageName == null) {
|
2014-03-28 11:27:46 -07:00
|
|
|
Log.w(LOGTAG, "no package name; treating as legacy shortcut");
|
2014-01-09 10:18:55 -08:00
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
mIsApk = false;
|
|
|
|
|
|
|
|
// Shortcut apps are already installed.
|
|
|
|
isInstalled = true;
|
|
|
|
|
|
|
|
Uri data = getIntent().getData();
|
|
|
|
if (data == null) {
|
|
|
|
Log.wtf(LOGTAG, "can't get manifest URL from shortcut data");
|
|
|
|
setResult(RESULT_CANCELED);
|
|
|
|
finish();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
mManifestUrl = data.toString();
|
|
|
|
|
|
|
|
String shortcutName = extras.getString(Intent.EXTRA_SHORTCUT_NAME);
|
|
|
|
mAppName = shortcutName != null ? shortcutName : "Web App";
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
mApkResources = new ApkResources(this, packageName);
|
|
|
|
} catch (NameNotFoundException e) {
|
|
|
|
Log.e(LOGTAG, "Can't find package for webapp " + packageName, e);
|
|
|
|
setResult(RESULT_CANCELED);
|
|
|
|
finish();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
mManifestUrl = mApkResources.getManifestUrl();
|
|
|
|
mAppName = mApkResources.getAppName();
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// start Gecko.
|
|
|
|
super.onCreate(savedInstance);
|
|
|
|
|
|
|
|
mTitlebarText = (TextView)findViewById(R.id.webapp_title);
|
|
|
|
mTitlebar = findViewById(R.id.webapp_titlebar);
|
|
|
|
mSplashscreen = findViewById(R.id.splashscreen);
|
|
|
|
|
2014-03-22 14:26:43 -07:00
|
|
|
Allocator allocator = Allocator.getInstance(this);
|
|
|
|
int index = getIndex();
|
|
|
|
|
|
|
|
// We have to migrate old prefs before getting the origin because origin
|
|
|
|
// is one of the prefs we might migrate.
|
|
|
|
allocator.maybeMigrateOldPrefs(index);
|
|
|
|
|
|
|
|
String origin = allocator.getOrigin(index);
|
2014-01-09 10:18:55 -08:00
|
|
|
boolean isInstallCompleting = (origin == null);
|
|
|
|
|
|
|
|
if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning) || !isInstalled || isInstallCompleting) {
|
|
|
|
// Show the splash screen if we need to start Gecko, or we need to install this.
|
|
|
|
overridePendingTransition(R.anim.grow_fade_in_center, android.R.anim.fade_out);
|
2014-03-28 11:27:46 -07:00
|
|
|
showSplash();
|
2014-01-09 10:18:55 -08:00
|
|
|
} else {
|
|
|
|
mSplashscreen.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isInstalled || isInstallCompleting) {
|
|
|
|
InstallHelper installHelper = new InstallHelper(getApplicationContext(), mApkResources, this);
|
|
|
|
if (!isInstalled) {
|
|
|
|
// start the vanilla install.
|
|
|
|
try {
|
|
|
|
installHelper.startInstall(getDefaultProfileName());
|
|
|
|
} catch (IOException e) {
|
|
|
|
Log.e(LOGTAG, "Couldn't install packaged app", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// an install is already happening, so we should let it complete.
|
|
|
|
Log.i(LOGTAG, "Waiting for existing install to complete");
|
|
|
|
installHelper.registerGeckoListener();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-10-11 17:41:42 -07:00
|
|
|
launchWebapp(origin);
|
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
setTitle(mAppName);
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-10-28 14:28:31 -07:00
|
|
|
protected String getURIFromIntent(SafeIntent intent) {
|
2014-01-09 10:18:55 -08:00
|
|
|
String uri = super.getURIFromIntent(intent);
|
|
|
|
if (uri != null) {
|
|
|
|
return uri;
|
|
|
|
}
|
|
|
|
// This is where we construct the URL from the Intent from the
|
|
|
|
// the synthesized APK.
|
|
|
|
|
|
|
|
// TODO Translate AndroidIntents into WebActivities here.
|
2014-03-28 11:27:46 -07:00
|
|
|
if (mIsApk) {
|
|
|
|
return mApkResources.getManifestUrl();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is a legacy shortcut, then we should have been able to get
|
|
|
|
// the URI from the intent data. Otherwise, we should have been able
|
|
|
|
// to get it from the APK resources. So we should never get here.
|
|
|
|
Log.wtf(LOGTAG, "Couldn't get URI from intent nor APK resources");
|
|
|
|
return null;
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void loadStartupTab(String uri) {
|
2014-04-01 23:32:42 -07:00
|
|
|
// Load a tab so it's available for any code that assumes a tab
|
|
|
|
// before the app tab itself is loaded in BrowserApp._loadWebapp.
|
|
|
|
super.loadStartupTab("about:blank");
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
private void showSplash() {
|
2014-01-09 10:18:55 -08:00
|
|
|
|
|
|
|
// get the favicon dominant color, stored when the app was installed
|
2014-01-24 21:57:13 -08:00
|
|
|
int dominantColor = Allocator.getInstance().getColor(getIndex());
|
2014-01-09 10:18:55 -08:00
|
|
|
|
|
|
|
setBackgroundGradient(dominantColor);
|
|
|
|
|
|
|
|
ImageView image = (ImageView)findViewById(R.id.splashscreen_icon);
|
|
|
|
Drawable d = null;
|
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
if (mIsApk) {
|
2014-01-09 10:18:55 -08:00
|
|
|
Uri uri = mApkResources.getAppIconUri();
|
|
|
|
image.setImageURI(uri);
|
|
|
|
d = image.getDrawable();
|
|
|
|
} else {
|
|
|
|
// look for a logo.png in the profile dir and show it. If we can't find a logo show nothing
|
|
|
|
File profile = getProfile().getDir();
|
|
|
|
File logoFile = new File(profile, "logo.png");
|
|
|
|
if (logoFile.exists()) {
|
|
|
|
d = Drawable.createFromPath(logoFile.getPath());
|
|
|
|
image.setImageDrawable(d);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (d != null) {
|
|
|
|
Animation fadein = AnimationUtils.loadAnimation(this, R.anim.grow_fade_in_center);
|
|
|
|
fadein.setStartOffset(500);
|
|
|
|
fadein.setDuration(1000);
|
|
|
|
image.startAnimation(fadein);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setBackgroundGradient(int dominantColor) {
|
|
|
|
int[] colors = new int[2];
|
|
|
|
// now lighten it, to ensure that the icon stands out in the center
|
|
|
|
float[] f = new float[3];
|
|
|
|
Color.colorToHSV(dominantColor, f);
|
|
|
|
f[2] = Math.min(f[2]*2, 1.0f);
|
|
|
|
colors[0] = Color.HSVToColor(255, f);
|
|
|
|
|
|
|
|
// now generate a second, slightly darker version of the same color
|
|
|
|
f[2] *= 0.75;
|
|
|
|
colors[1] = Color.HSVToColor(255, f);
|
|
|
|
|
|
|
|
// Draw the background gradient
|
|
|
|
GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TL_BR, colors);
|
|
|
|
gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
|
|
|
|
Display display = getWindowManager().getDefaultDisplay();
|
|
|
|
gd.setGradientCenter(0.5f, 0.5f);
|
|
|
|
gd.setGradientRadius(Math.max(display.getWidth()/2, display.getHeight()/2));
|
|
|
|
mSplashscreen.setBackgroundDrawable(gd);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* (non-Javadoc)
|
|
|
|
* @see org.mozilla.gecko.GeckoApp#getDefaultProfileName()
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
protected String getDefaultProfileName() {
|
|
|
|
return "webapp" + getIndex();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected boolean getSessionRestoreState(Bundle savedInstanceState) {
|
|
|
|
// for now webapps never restore your session
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
|
|
|
switch(msg) {
|
|
|
|
case SELECTED:
|
|
|
|
case LOCATION_CHANGE:
|
|
|
|
if (Tabs.getInstance().isSelectedTab(tab)) {
|
|
|
|
final String urlString = tab.getURL();
|
2014-04-01 23:32:42 -07:00
|
|
|
|
|
|
|
// Don't show the titlebar for about:blank, which we load
|
|
|
|
// into the initial tab we create while waiting for the app
|
|
|
|
// to load.
|
|
|
|
if (urlString != null && urlString.equals("about:blank")) {
|
|
|
|
mTitlebar.setVisibility(View.GONE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-04-16 08:42:15 -07:00
|
|
|
final URI uri;
|
2014-01-09 10:18:55 -08:00
|
|
|
|
|
|
|
try {
|
2014-04-16 08:42:15 -07:00
|
|
|
uri = new URI(urlString);
|
|
|
|
} catch (java.net.URISyntaxException ex) {
|
2014-01-09 10:18:55 -08:00
|
|
|
mTitlebarText.setText(urlString);
|
|
|
|
|
|
|
|
// If we can't parse the url, and its an app protocol hide
|
|
|
|
// the titlebar and return, otherwise show the titlebar
|
|
|
|
// and the full url
|
|
|
|
if (urlString != null && !urlString.startsWith("app://")) {
|
|
|
|
mTitlebar.setVisibility(View.VISIBLE);
|
|
|
|
} else {
|
|
|
|
mTitlebar.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-04-16 08:42:15 -07:00
|
|
|
if (mOrigin != null && mOrigin.getHost().equals(uri.getHost())) {
|
2014-01-09 10:18:55 -08:00
|
|
|
mTitlebar.setVisibility(View.GONE);
|
|
|
|
} else {
|
2014-04-16 08:42:15 -07:00
|
|
|
mTitlebarText.setText(uri.getScheme() + "://" + uri.getHost());
|
2014-01-09 10:18:55 -08:00
|
|
|
mTitlebar.setVisibility(View.VISIBLE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case LOADED:
|
|
|
|
hideSplash();
|
|
|
|
break;
|
|
|
|
case START:
|
|
|
|
if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
|
|
|
|
View area = findViewById(R.id.splashscreen_progress);
|
|
|
|
area.setVisibility(View.VISIBLE);
|
|
|
|
Animation fadein = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
|
|
|
|
fadein.setDuration(1000);
|
|
|
|
area.startAnimation(fadein);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
super.onTabChanged(tab, msg, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected void hideSplash() {
|
|
|
|
if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
|
|
|
|
Animation fadeout = AnimationUtils.loadAnimation(this, android.R.anim.fade_out);
|
|
|
|
fadeout.setAnimationListener(new Animation.AnimationListener() {
|
|
|
|
@Override
|
|
|
|
public void onAnimationEnd(Animation animation) {
|
|
|
|
mSplashscreen.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
@Override
|
|
|
|
public void onAnimationRepeat(Animation animation) { }
|
|
|
|
@Override
|
|
|
|
public void onAnimationStart(Animation animation) { }
|
|
|
|
});
|
|
|
|
mSplashscreen.startAnimation(fadeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-06-05 19:38:01 -07:00
|
|
|
public void installCompleted(InstallHelper installHelper, String event, NativeJSObject message) {
|
2014-01-09 10:18:55 -08:00
|
|
|
if (event == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-02-11 23:41:05 -08:00
|
|
|
if (event.equals("Webapps:Postinstall")) {
|
2014-06-05 19:38:01 -07:00
|
|
|
String origin = message.optString("origin", null);
|
2014-03-28 11:27:46 -07:00
|
|
|
launchWebapp(origin);
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void installErrored(InstallHelper installHelper, Exception exception) {
|
|
|
|
Log.e(LOGTAG, "Install errored", exception);
|
|
|
|
}
|
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
private void setOrigin(String origin) {
|
2014-01-09 10:18:55 -08:00
|
|
|
try {
|
2014-04-16 08:42:15 -07:00
|
|
|
mOrigin = new URI(origin);
|
|
|
|
} catch (java.net.URISyntaxException ex) {
|
2014-03-28 11:27:46 -07:00
|
|
|
// If this isn't an app: URL, just settle for not having an origin.
|
2014-01-09 10:18:55 -08:00
|
|
|
if (!origin.startsWith("app://")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-28 11:27:46 -07:00
|
|
|
// If that failed fall back to the origin stored in the shortcut.
|
|
|
|
if (!mIsApk) {
|
|
|
|
Log.i(LOGTAG, "Origin is app: URL; falling back to intent URL");
|
|
|
|
Uri data = getIntent().getData();
|
|
|
|
if (data != null) {
|
|
|
|
try {
|
2014-04-16 08:42:15 -07:00
|
|
|
mOrigin = new URI(data.toString());
|
|
|
|
} catch (java.net.URISyntaxException ex2) {
|
2014-03-28 11:27:46 -07:00
|
|
|
Log.e(LOGTAG, "Unable to parse intent URL: ", ex);
|
|
|
|
}
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-03-28 11:27:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
public void launchWebapp(String origin) {
|
|
|
|
setOrigin(origin);
|
|
|
|
|
2014-01-09 10:18:55 -08:00
|
|
|
try {
|
|
|
|
JSONObject launchObject = new JSONObject();
|
2014-03-28 11:27:46 -07:00
|
|
|
launchObject.putOpt("url", mManifestUrl);
|
|
|
|
launchObject.putOpt("name", mAppName);
|
2014-01-09 10:18:55 -08:00
|
|
|
Log.i(LOGTAG, "Trying to launch: " + launchObject);
|
|
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Webapps:Load", launchObject.toString()));
|
|
|
|
} catch (JSONException e) {
|
|
|
|
Log.e(LOGTAG, "Error populating launch message", e);
|
|
|
|
}
|
|
|
|
}
|
2014-01-31 14:51:24 -08:00
|
|
|
|
|
|
|
@Override
|
|
|
|
protected boolean getIsDebuggable() {
|
2014-03-28 11:27:46 -07:00
|
|
|
if (mIsApk) {
|
|
|
|
return mApkResources.isDebuggable();
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a legacy shortcut, which didn't provide a way to determine
|
|
|
|
// that the app is debuggable, so we say the app is not debuggable.
|
|
|
|
return false;
|
2014-01-31 14:51:24 -08:00
|
|
|
}
|
2014-01-09 10:18:55 -08:00
|
|
|
}
|