diff -r 000000000000 -r 6474c204b198 mobile/android/base/webapp/WebappImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/webapp/WebappImpl.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,392 @@ +/* -*- 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; +import java.net.URI; + +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; +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; + +public class WebappImpl extends GeckoApp implements InstallCallback { + private static final String LOGTAG = "GeckoWebappImpl"; + + private URI mOrigin; + private TextView mTitlebarText = null; + private View mTitlebar = null; + + private View mSplashscreen; + + private boolean mIsApk = true; + private ApkResources mApkResources; + private String mManifestUrl; + private String mAppName; + + 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) { + Log.w(LOGTAG, "no package name; treating as legacy shortcut"); + + 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(); + } + + // start Gecko. + super.onCreate(savedInstance); + + mTitlebarText = (TextView)findViewById(R.id.webapp_title); + mTitlebar = findViewById(R.id.webapp_titlebar); + mSplashscreen = findViewById(R.id.splashscreen); + + 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); + 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); + showSplash(); + } 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; + } else { + launchWebapp(origin); + } + + setTitle(mAppName); + } + + @Override + protected String getURIFromIntent(Intent intent) { + 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. + 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; + } + + @Override + protected void loadStartupTab(String uri) { + // 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"); + } + + private void showSplash() { + + // get the favicon dominant color, stored when the app was installed + int dominantColor = Allocator.getInstance().getColor(getIndex()); + + setBackgroundGradient(dominantColor); + + ImageView image = (ImageView)findViewById(R.id.splashscreen_icon); + Drawable d = null; + + if (mIsApk) { + 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(); + + // 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; + } + + final URI uri; + + try { + uri = new URI(urlString); + } catch (java.net.URISyntaxException ex) { + 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; + } + + if (mOrigin != null && mOrigin.getHost().equals(uri.getHost())) { + mTitlebar.setVisibility(View.GONE); + } else { + mTitlebarText.setText(uri.getScheme() + "://" + uri.getHost()); + 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 + public void installCompleted(InstallHelper installHelper, String event, JSONObject message) { + if (event == null) { + return; + } + + if (event.equals("Webapps:Postinstall")) { + String origin = message.optString("origin"); + launchWebapp(origin); + } + } + + @Override + public void installErrored(InstallHelper installHelper, Exception exception) { + Log.e(LOGTAG, "Install errored", exception); + } + + private void setOrigin(String origin) { + try { + mOrigin = new URI(origin); + } catch (java.net.URISyntaxException ex) { + // If this isn't an app: URL, just settle for not having an origin. + if (!origin.startsWith("app://")) { + return; + } + + // 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 { + mOrigin = new URI(data.toString()); + } catch (java.net.URISyntaxException ex2) { + Log.e(LOGTAG, "Unable to parse intent URL: ", ex); + } + } + } + } + } + + public void launchWebapp(String origin) { + setOrigin(origin); + + try { + JSONObject launchObject = new JSONObject(); + launchObject.putOpt("url", mManifestUrl); + launchObject.putOpt("name", mAppName); + 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); + } + } + + @Override + protected boolean getIsDebuggable() { + 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; + } +}