|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko.webapp; |
|
7 |
|
8 import java.io.File; |
|
9 import java.io.IOException; |
|
10 import java.net.URI; |
|
11 |
|
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; |
|
22 |
|
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; |
|
37 |
|
38 public class WebappImpl extends GeckoApp implements InstallCallback { |
|
39 private static final String LOGTAG = "GeckoWebappImpl"; |
|
40 |
|
41 private URI mOrigin; |
|
42 private TextView mTitlebarText = null; |
|
43 private View mTitlebar = null; |
|
44 |
|
45 private View mSplashscreen; |
|
46 |
|
47 private boolean mIsApk = true; |
|
48 private ApkResources mApkResources; |
|
49 private String mManifestUrl; |
|
50 private String mAppName; |
|
51 |
|
52 protected int getIndex() { return 0; } |
|
53 |
|
54 @Override |
|
55 public int getLayout() { return R.layout.web_app; } |
|
56 |
|
57 @Override |
|
58 public boolean hasTabsSideBar() { return false; } |
|
59 |
|
60 @Override |
|
61 public void onCreate(Bundle savedInstance) |
|
62 { |
|
63 |
|
64 String action = getIntent().getAction(); |
|
65 Bundle extras = getIntent().getExtras(); |
|
66 if (extras == null) { |
|
67 extras = savedInstance; |
|
68 } |
|
69 |
|
70 if (extras == null) { |
|
71 extras = new Bundle(); |
|
72 } |
|
73 |
|
74 boolean isInstalled = extras.getBoolean("isInstalled", false); |
|
75 String packageName = extras.getString("packageName"); |
|
76 |
|
77 if (packageName == null) { |
|
78 Log.w(LOGTAG, "no package name; treating as legacy shortcut"); |
|
79 |
|
80 mIsApk = false; |
|
81 |
|
82 // Shortcut apps are already installed. |
|
83 isInstalled = true; |
|
84 |
|
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(); |
|
93 |
|
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 } |
|
105 |
|
106 mManifestUrl = mApkResources.getManifestUrl(); |
|
107 mAppName = mApkResources.getAppName(); |
|
108 } |
|
109 |
|
110 // start Gecko. |
|
111 super.onCreate(savedInstance); |
|
112 |
|
113 mTitlebarText = (TextView)findViewById(R.id.webapp_title); |
|
114 mTitlebar = findViewById(R.id.webapp_titlebar); |
|
115 mSplashscreen = findViewById(R.id.splashscreen); |
|
116 |
|
117 Allocator allocator = Allocator.getInstance(this); |
|
118 int index = getIndex(); |
|
119 |
|
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); |
|
123 |
|
124 String origin = allocator.getOrigin(index); |
|
125 boolean isInstallCompleting = (origin == null); |
|
126 |
|
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 } |
|
134 |
|
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 } |
|
153 |
|
154 setTitle(mAppName); |
|
155 } |
|
156 |
|
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. |
|
165 |
|
166 // TODO Translate AndroidIntents into WebActivities here. |
|
167 if (mIsApk) { |
|
168 return mApkResources.getManifestUrl(); |
|
169 } |
|
170 |
|
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 } |
|
177 |
|
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 } |
|
184 |
|
185 private void showSplash() { |
|
186 |
|
187 // get the favicon dominant color, stored when the app was installed |
|
188 int dominantColor = Allocator.getInstance().getColor(getIndex()); |
|
189 |
|
190 setBackgroundGradient(dominantColor); |
|
191 |
|
192 ImageView image = (ImageView)findViewById(R.id.splashscreen_icon); |
|
193 Drawable d = null; |
|
194 |
|
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 } |
|
208 |
|
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 } |
|
216 |
|
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); |
|
224 |
|
225 // now generate a second, slightly darker version of the same color |
|
226 f[2] *= 0.75; |
|
227 colors[1] = Color.HSVToColor(255, f); |
|
228 |
|
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 } |
|
237 |
|
238 /* (non-Javadoc) |
|
239 * @see org.mozilla.gecko.GeckoApp#getDefaultProfileName() |
|
240 */ |
|
241 @Override |
|
242 protected String getDefaultProfileName() { |
|
243 return "webapp" + getIndex(); |
|
244 } |
|
245 |
|
246 @Override |
|
247 protected boolean getSessionRestoreState(Bundle savedInstanceState) { |
|
248 // for now webapps never restore your session |
|
249 return false; |
|
250 } |
|
251 |
|
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(); |
|
259 |
|
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 } |
|
267 |
|
268 final URI uri; |
|
269 |
|
270 try { |
|
271 uri = new URI(urlString); |
|
272 } catch (java.net.URISyntaxException ex) { |
|
273 mTitlebarText.setText(urlString); |
|
274 |
|
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 } |
|
285 |
|
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 } |
|
309 |
|
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 } |
|
326 |
|
327 @Override |
|
328 public void installCompleted(InstallHelper installHelper, String event, JSONObject message) { |
|
329 if (event == null) { |
|
330 return; |
|
331 } |
|
332 |
|
333 if (event.equals("Webapps:Postinstall")) { |
|
334 String origin = message.optString("origin"); |
|
335 launchWebapp(origin); |
|
336 } |
|
337 } |
|
338 |
|
339 @Override |
|
340 public void installErrored(InstallHelper installHelper, Exception exception) { |
|
341 Log.e(LOGTAG, "Install errored", exception); |
|
342 } |
|
343 |
|
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 } |
|
352 |
|
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 } |
|
367 |
|
368 public void launchWebapp(String origin) { |
|
369 setOrigin(origin); |
|
370 |
|
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 } |
|
381 |
|
382 @Override |
|
383 protected boolean getIsDebuggable() { |
|
384 if (mIsApk) { |
|
385 return mApkResources.isDebuggable(); |
|
386 } |
|
387 |
|
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 } |