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