/* -*- 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.util.NativeJSObject; 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; private View mTitlebar; // Must only be accessed from the UI thread. 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; } 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, NativeJSObject message) { if (event == null) { return; } if (event.equals("Webapps:Postinstall")) { String origin = message.optString("origin", null); 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; } }