Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.webapp;
8 import java.io.File;
9 import java.io.IOException;
10 import java.net.URI;
12 import org.json.JSONException;
13 import org.json.JSONObject;
14 import org.mozilla.gecko.GeckoApp;
15 import org.mozilla.gecko.GeckoAppShell;
16 import org.mozilla.gecko.GeckoEvent;
17 import org.mozilla.gecko.GeckoThread;
18 import org.mozilla.gecko.R;
19 import org.mozilla.gecko.Tab;
20 import org.mozilla.gecko.Tabs;
21 import org.mozilla.gecko.webapp.InstallHelper.InstallCallback;
23 import android.content.Intent;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.graphics.Color;
26 import android.graphics.drawable.Drawable;
27 import android.graphics.drawable.GradientDrawable;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.util.Log;
31 import android.view.Display;
32 import android.view.View;
33 import android.view.animation.Animation;
34 import android.view.animation.AnimationUtils;
35 import android.widget.ImageView;
36 import android.widget.TextView;
38 public class WebappImpl extends GeckoApp implements InstallCallback {
39 private static final String LOGTAG = "GeckoWebappImpl";
41 private URI mOrigin;
42 private TextView mTitlebarText = null;
43 private View mTitlebar = null;
45 private View mSplashscreen;
47 private boolean mIsApk = true;
48 private ApkResources mApkResources;
49 private String mManifestUrl;
50 private String mAppName;
52 protected int getIndex() { return 0; }
54 @Override
55 public int getLayout() { return R.layout.web_app; }
57 @Override
58 public boolean hasTabsSideBar() { return false; }
60 @Override
61 public void onCreate(Bundle savedInstance)
62 {
64 String action = getIntent().getAction();
65 Bundle extras = getIntent().getExtras();
66 if (extras == null) {
67 extras = savedInstance;
68 }
70 if (extras == null) {
71 extras = new Bundle();
72 }
74 boolean isInstalled = extras.getBoolean("isInstalled", false);
75 String packageName = extras.getString("packageName");
77 if (packageName == null) {
78 Log.w(LOGTAG, "no package name; treating as legacy shortcut");
80 mIsApk = false;
82 // Shortcut apps are already installed.
83 isInstalled = true;
85 Uri data = getIntent().getData();
86 if (data == null) {
87 Log.wtf(LOGTAG, "can't get manifest URL from shortcut data");
88 setResult(RESULT_CANCELED);
89 finish();
90 return;
91 }
92 mManifestUrl = data.toString();
94 String shortcutName = extras.getString(Intent.EXTRA_SHORTCUT_NAME);
95 mAppName = shortcutName != null ? shortcutName : "Web App";
96 } else {
97 try {
98 mApkResources = new ApkResources(this, packageName);
99 } catch (NameNotFoundException e) {
100 Log.e(LOGTAG, "Can't find package for webapp " + packageName, e);
101 setResult(RESULT_CANCELED);
102 finish();
103 return;
104 }
106 mManifestUrl = mApkResources.getManifestUrl();
107 mAppName = mApkResources.getAppName();
108 }
110 // start Gecko.
111 super.onCreate(savedInstance);
113 mTitlebarText = (TextView)findViewById(R.id.webapp_title);
114 mTitlebar = findViewById(R.id.webapp_titlebar);
115 mSplashscreen = findViewById(R.id.splashscreen);
117 Allocator allocator = Allocator.getInstance(this);
118 int index = getIndex();
120 // We have to migrate old prefs before getting the origin because origin
121 // is one of the prefs we might migrate.
122 allocator.maybeMigrateOldPrefs(index);
124 String origin = allocator.getOrigin(index);
125 boolean isInstallCompleting = (origin == null);
127 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning) || !isInstalled || isInstallCompleting) {
128 // Show the splash screen if we need to start Gecko, or we need to install this.
129 overridePendingTransition(R.anim.grow_fade_in_center, android.R.anim.fade_out);
130 showSplash();
131 } else {
132 mSplashscreen.setVisibility(View.GONE);
133 }
135 if (!isInstalled || isInstallCompleting) {
136 InstallHelper installHelper = new InstallHelper(getApplicationContext(), mApkResources, this);
137 if (!isInstalled) {
138 // start the vanilla install.
139 try {
140 installHelper.startInstall(getDefaultProfileName());
141 } catch (IOException e) {
142 Log.e(LOGTAG, "Couldn't install packaged app", e);
143 }
144 } else {
145 // an install is already happening, so we should let it complete.
146 Log.i(LOGTAG, "Waiting for existing install to complete");
147 installHelper.registerGeckoListener();
148 }
149 return;
150 } else {
151 launchWebapp(origin);
152 }
154 setTitle(mAppName);
155 }
157 @Override
158 protected String getURIFromIntent(Intent intent) {
159 String uri = super.getURIFromIntent(intent);
160 if (uri != null) {
161 return uri;
162 }
163 // This is where we construct the URL from the Intent from the
164 // the synthesized APK.
166 // TODO Translate AndroidIntents into WebActivities here.
167 if (mIsApk) {
168 return mApkResources.getManifestUrl();
169 }
171 // If this is a legacy shortcut, then we should have been able to get
172 // the URI from the intent data. Otherwise, we should have been able
173 // to get it from the APK resources. So we should never get here.
174 Log.wtf(LOGTAG, "Couldn't get URI from intent nor APK resources");
175 return null;
176 }
178 @Override
179 protected void loadStartupTab(String uri) {
180 // Load a tab so it's available for any code that assumes a tab
181 // before the app tab itself is loaded in BrowserApp._loadWebapp.
182 super.loadStartupTab("about:blank");
183 }
185 private void showSplash() {
187 // get the favicon dominant color, stored when the app was installed
188 int dominantColor = Allocator.getInstance().getColor(getIndex());
190 setBackgroundGradient(dominantColor);
192 ImageView image = (ImageView)findViewById(R.id.splashscreen_icon);
193 Drawable d = null;
195 if (mIsApk) {
196 Uri uri = mApkResources.getAppIconUri();
197 image.setImageURI(uri);
198 d = image.getDrawable();
199 } else {
200 // look for a logo.png in the profile dir and show it. If we can't find a logo show nothing
201 File profile = getProfile().getDir();
202 File logoFile = new File(profile, "logo.png");
203 if (logoFile.exists()) {
204 d = Drawable.createFromPath(logoFile.getPath());
205 image.setImageDrawable(d);
206 }
207 }
209 if (d != null) {
210 Animation fadein = AnimationUtils.loadAnimation(this, R.anim.grow_fade_in_center);
211 fadein.setStartOffset(500);
212 fadein.setDuration(1000);
213 image.startAnimation(fadein);
214 }
215 }
217 public void setBackgroundGradient(int dominantColor) {
218 int[] colors = new int[2];
219 // now lighten it, to ensure that the icon stands out in the center
220 float[] f = new float[3];
221 Color.colorToHSV(dominantColor, f);
222 f[2] = Math.min(f[2]*2, 1.0f);
223 colors[0] = Color.HSVToColor(255, f);
225 // now generate a second, slightly darker version of the same color
226 f[2] *= 0.75;
227 colors[1] = Color.HSVToColor(255, f);
229 // Draw the background gradient
230 GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TL_BR, colors);
231 gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
232 Display display = getWindowManager().getDefaultDisplay();
233 gd.setGradientCenter(0.5f, 0.5f);
234 gd.setGradientRadius(Math.max(display.getWidth()/2, display.getHeight()/2));
235 mSplashscreen.setBackgroundDrawable(gd);
236 }
238 /* (non-Javadoc)
239 * @see org.mozilla.gecko.GeckoApp#getDefaultProfileName()
240 */
241 @Override
242 protected String getDefaultProfileName() {
243 return "webapp" + getIndex();
244 }
246 @Override
247 protected boolean getSessionRestoreState(Bundle savedInstanceState) {
248 // for now webapps never restore your session
249 return false;
250 }
252 @Override
253 public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
254 switch(msg) {
255 case SELECTED:
256 case LOCATION_CHANGE:
257 if (Tabs.getInstance().isSelectedTab(tab)) {
258 final String urlString = tab.getURL();
260 // Don't show the titlebar for about:blank, which we load
261 // into the initial tab we create while waiting for the app
262 // to load.
263 if (urlString != null && urlString.equals("about:blank")) {
264 mTitlebar.setVisibility(View.GONE);
265 return;
266 }
268 final URI uri;
270 try {
271 uri = new URI(urlString);
272 } catch (java.net.URISyntaxException ex) {
273 mTitlebarText.setText(urlString);
275 // If we can't parse the url, and its an app protocol hide
276 // the titlebar and return, otherwise show the titlebar
277 // and the full url
278 if (urlString != null && !urlString.startsWith("app://")) {
279 mTitlebar.setVisibility(View.VISIBLE);
280 } else {
281 mTitlebar.setVisibility(View.GONE);
282 }
283 return;
284 }
286 if (mOrigin != null && mOrigin.getHost().equals(uri.getHost())) {
287 mTitlebar.setVisibility(View.GONE);
288 } else {
289 mTitlebarText.setText(uri.getScheme() + "://" + uri.getHost());
290 mTitlebar.setVisibility(View.VISIBLE);
291 }
292 }
293 break;
294 case LOADED:
295 hideSplash();
296 break;
297 case START:
298 if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
299 View area = findViewById(R.id.splashscreen_progress);
300 area.setVisibility(View.VISIBLE);
301 Animation fadein = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
302 fadein.setDuration(1000);
303 area.startAnimation(fadein);
304 }
305 break;
306 }
307 super.onTabChanged(tab, msg, data);
308 }
310 protected void hideSplash() {
311 if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
312 Animation fadeout = AnimationUtils.loadAnimation(this, android.R.anim.fade_out);
313 fadeout.setAnimationListener(new Animation.AnimationListener() {
314 @Override
315 public void onAnimationEnd(Animation animation) {
316 mSplashscreen.setVisibility(View.GONE);
317 }
318 @Override
319 public void onAnimationRepeat(Animation animation) { }
320 @Override
321 public void onAnimationStart(Animation animation) { }
322 });
323 mSplashscreen.startAnimation(fadeout);
324 }
325 }
327 @Override
328 public void installCompleted(InstallHelper installHelper, String event, JSONObject message) {
329 if (event == null) {
330 return;
331 }
333 if (event.equals("Webapps:Postinstall")) {
334 String origin = message.optString("origin");
335 launchWebapp(origin);
336 }
337 }
339 @Override
340 public void installErrored(InstallHelper installHelper, Exception exception) {
341 Log.e(LOGTAG, "Install errored", exception);
342 }
344 private void setOrigin(String origin) {
345 try {
346 mOrigin = new URI(origin);
347 } catch (java.net.URISyntaxException ex) {
348 // If this isn't an app: URL, just settle for not having an origin.
349 if (!origin.startsWith("app://")) {
350 return;
351 }
353 // If that failed fall back to the origin stored in the shortcut.
354 if (!mIsApk) {
355 Log.i(LOGTAG, "Origin is app: URL; falling back to intent URL");
356 Uri data = getIntent().getData();
357 if (data != null) {
358 try {
359 mOrigin = new URI(data.toString());
360 } catch (java.net.URISyntaxException ex2) {
361 Log.e(LOGTAG, "Unable to parse intent URL: ", ex);
362 }
363 }
364 }
365 }
366 }
368 public void launchWebapp(String origin) {
369 setOrigin(origin);
371 try {
372 JSONObject launchObject = new JSONObject();
373 launchObject.putOpt("url", mManifestUrl);
374 launchObject.putOpt("name", mAppName);
375 Log.i(LOGTAG, "Trying to launch: " + launchObject);
376 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Webapps:Load", launchObject.toString()));
377 } catch (JSONException e) {
378 Log.e(LOGTAG, "Error populating launch message", e);
379 }
380 }
382 @Override
383 protected boolean getIsDebuggable() {
384 if (mIsApk) {
385 return mApkResources.isDebuggable();
386 }
388 // This is a legacy shortcut, which didn't provide a way to determine
389 // that the app is debuggable, so we say the app is not debuggable.
390 return false;
391 }
392 }