diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml
index 456316a..7ec9d99 100644
--- a/demo-app/src/main/AndroidManifest.xml
+++ b/demo-app/src/main/AndroidManifest.xml
@@ -19,7 +19,8 @@
-
+ * This is not invoked on the UI thread. + * + * @param message The message. + */ + @JavascriptInterface + @WorkerThread + public void dispatch(@Nullable final String message) { + JSONObject action; + try { + action = new JSONObject(message); + } catch (JSONException e) { + if (BuildConfig.DEBUG) { + Log.e(App.TAG, "Failed to parse serialized action", e); + } + + return; + } + + String type; + try { + type = action.getString("type"); + } catch (JSONException e) { + if (BuildConfig.DEBUG) { + Log.e(App.TAG, "The action does not have a type", e); + } + + return; + } + + switch (type) { + case "APP_LOADED": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(APP_LOADED)); + break; + } + case "APP_STARTED": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(APP_STARTED)); + break; + } + case "CLOSE_PAGE_REQUESTED": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(CLOSE_PAGE_REQUESTED)); + break; + } + case "LOGGED_IN": { + try { + String payload = action.getString("payload"); + Zapic.setPlayerId(payload); + } catch (JSONException ignored) { + Zapic.setPlayerId(null); + } + + break; + } + case "LOGIN": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(LOGIN)); + break; + } + case "LOGOUT": { + Zapic.setPlayerId(null); + this.mHandler.sendMessage(this.mHandler.obtainMessage(LOGOUT)); + break; + } + case "PAGE_READY": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(SHOW_PAGE)); + break; + } + case "SHOW_BANNER": { + this.mHandler.sendMessage(this.mHandler.obtainMessage(SHOW_BANNER)); + break; + } + default: { + break; + } + } + } + } + + /** + * A utility that overrides the default {@link WebView} behaviors. + * + * @author Kyle Dodson + * @since 1.0.0 + */ + final class AppWebViewClient extends WebViewClient { +// TODO: Handle errors loading the initial HTTP content. +// @MainThread +// @Override +// @RequiresApi(Build.VERSION_CODES.M) +// public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { +// // TODO: Transition to an offline page. +// } + +// TODO: Handle errors loading the initial HTTP content. +// @MainThread +// @Override +// public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { +// // TODO: Transition to an offline page. +// } + + @MainThread + @Override + @RequiresApi(Build.VERSION_CODES.O) + public boolean onRenderProcessGone(@NonNull final WebView view, @NonNull final RenderProcessGoneDetail detail) { + if (BuildConfig.DEBUG) { + final boolean crashed = detail.didCrash(); + if (crashed) { + Log.e(App.TAG, "The WebView has crashed"); + } else { + Log.e(App.TAG, "The WebView has been stopped to reclaim memory"); + } + } + + App.this.mLoaded = false; + App.this.mReady = false; + App.this.mStarted = false; + App.this.mInteractionListener.onStateChanged(State.NOT_LOADED); + return true; + } + + @MainThread + @Override + @RequiresApi(Build.VERSION_CODES.N) + public boolean shouldOverrideUrlLoading(@Nullable final WebView view, @Nullable final WebResourceRequest request) { + if (view == null || request == null) { + return false; + } + + Uri url = request.getUrl(); + return url != null && this.overrideUrlLoading(view, request.getUrl()); + } + + @MainThread + @Override + public boolean shouldOverrideUrlLoading(@Nullable final WebView view, @Nullable final String url) { + return !(view == null || url == null) && this.overrideUrlLoading(view, Uri.parse(url)); + } + + /** + * Intercepts {@link WebView} navigation events and, optionally, overrides the navigation + * behavior. + *
+ * This currently overrides navigation events to {@code market://*} URLs by opening the Google
+ * Play Store app.
+ *
+ * @param view The {@link WebView}.
+ * @param url The target {@link Uri} of the navigation event.
+ * @return {@code true} if the navigation behavior was overridden; {@code false} if the
+ * navigation behavior was not overridden.
+ */
+ @MainThread
+ private boolean overrideUrlLoading(@NonNull final WebView view, @NonNull final Uri url) {
+ final String scheme = url.getScheme();
+ if (scheme != null && scheme.equalsIgnoreCase("market")) {
+ try {
+ // Create a Google Play Store app intent.
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(url);
+
+ // Open the Google Play Store app.
+ final Context context = view.getContext();
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException ignored) {
+ }
+
+ // Prevent the WebView from navigating to an invalid URL.
+ return true;
+ }
+
+ final String host = url.getHost();
+ if (host != null && (host.equalsIgnoreCase("itunes.apple.com") || host.toLowerCase().endsWith(".itunes.apple.com"))) {
+ if (scheme != null && (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
+ // Create a web browser app intent.
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(url);
+
+ // Open the web browser app.
+ final Context context = view.getContext();
+ context.startActivity(intent);
+ }
+
+ // Prevent the WebView from navigating to an external URL.
+ return true;
+ }
+
+ if (host != null && (host.equalsIgnoreCase("play.google.com") || host.toLowerCase().endsWith(".play.google.com"))) {
+ if (scheme != null && (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
+ // Create a web browser app intent.
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(url);
+
+ // Open the web browser app.
+ final Context context = view.getContext();
+ context.startActivity(intent);
+ }
+
+ // Prevent the WebView from navigating to an external URL.
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * The Zapic JavaScript application URL.
+ */
+ @NonNull
+ private static final String APP_URL = "https://app.zapic.net";
+
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "App";
+
+ /**
+ * The variable name of the {@link AppJavascriptInterface} instance.
+ */
+ @NonNull
+ private static final String VARIABLE_NAME = "androidWebView";
+
+ /**
+ * The currently executing asynchronous task.
+ */
+ @Nullable
+ private AsyncTask mAsyncTask;
+
+ /**
+ * The Android application's cache directory.
+ */
+ @Nullable
+ private File mCacheDir;
+
+ /**
+ * A value that indicates whether the Android device has network connectivity.
+ */
+ private boolean mConnected;
+
+ /**
+ * The interaction events listener.
+ */
+ @NonNull
+ private final InteractionListener mInteractionListener;
+
+ /**
+ * A value that indicates whether the Zapic JavaScript application has been loaded.
+ */
+ private boolean mLoaded;
+
+ /**
+ * A value that indicates whether the Zapic JavaScript application page is ready.
+ */
+ private boolean mReady;
+
+ /**
+ * A value that indicates whether the Zapic JavaScript application has been started.
+ */
+ private boolean mStarted;
+
+ /**
+ * The {@link WebView}.
+ */
+ @Nullable
+ private WebView mWebView;
+
+ /**
+ * Creates a new {@link App} instance
+ *
+ * @param interactionListener The interaction event listener.
+ */
+ App(@NonNull final InteractionListener interactionListener) {
+ this.mAsyncTask = null;
+ this.mCacheDir = null;
+ this.mConnected = false;
+ this.mInteractionListener = interactionListener;
+ this.mLoaded = false;
+ this.mReady = false;
+ this.mStarted = false;
+ this.mWebView = null;
+ }
+
+ /**
+ * Gets a value that indicates whether the Android device has network connectivity.
+ *
+ * @return {@code true} if the Android device has network connectivity; {@code false} if the
+ * Android device does not have network connectivity.
+ */
+ @CheckResult
+ @MainThread
+ boolean getConnected() {
+ return this.mConnected;
+ }
+
+ /**
+ * Gets the current state.
+ *
+ * @return The current state.
+ */
+ @CheckResult
+ @MainThread
+ State getState() {
+ if (this.mReady) {
+ return State.READY;
+ } else if (this.mStarted) {
+ return State.STARTED;
+ } else if (this.mLoaded) {
+ return State.LOADED;
+ } else {
+ return State.NOT_LOADED;
+ }
+ }
+
+ /**
+ * Gets the {@link WebView}.
+ *
+ * @return The {@link WebView}.
+ */
+ @CheckResult
+ @MainThread
+ @Nullable
+ WebView getWebView() {
+ return this.mWebView;
+ }
+
+ @MainThread
+ void loadWebView(@NonNull final AppSource appSource) {
+ this.mAsyncTask = null;
+
+ if (this.mWebView != null) {
+ this.mWebView.loadDataWithBaseURL(App.APP_URL, appSource.getHtml(), "text/html", "utf-8", App.APP_URL);
+ }
+ }
+
+ @MainThread
+ void loadWebViewCancelled() {
+ this.mAsyncTask = null;
+ }
+
+ @Override
+ public void onConnected() {
+ if (this.mConnected) {
+ return;
+ }
+
+ this.mConnected = true;
+ if (this.mAsyncTask == null && this.mCacheDir != null && this.getState() == State.NOT_LOADED) {
+ // Start an asynchronous task to get the web client application.
+ AppSourceAsyncTask asyncTask = new AppSourceAsyncTask(this, App.APP_URL, this.mCacheDir);
+ this.mAsyncTask = asyncTask;
+ asyncTask.execute();
+ }
+ }
+
+ @Override
+ public void onDisconnected() {
+ if (!this.mConnected) {
+ return;
+ }
+
+ this.mConnected = false;
+ }
+
+ @MainThread
+ @SuppressLint("SetJavaScriptEnabled")
+ void start(@NonNull final Context context) {
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // This enables WebView debugging for all WebViews in the current process. Changes to
+ // this value are accepted only before the WebView process is created.
+ WebView.setWebContentsDebuggingEnabled(true);
+ }
+
+ // We use the application context to ensure we don't leak an activity.
+ this.mWebView = new WebView(context.getApplicationContext());
+ this.mWebView.addJavascriptInterface(this.new AppJavascriptInterface(), App.VARIABLE_NAME);
+ this.mWebView.getSettings().setAllowContentAccess(false);
+ this.mWebView.getSettings().setAllowFileAccess(false);
+ this.mWebView.getSettings().setAllowFileAccessFromFileURLs(false);
+ this.mWebView.getSettings().setAllowUniversalAccessFromFileURLs(false);
+ this.mWebView.getSettings().setDomStorageEnabled(true);
+ this.mWebView.getSettings().setGeolocationEnabled(false);
+ this.mWebView.getSettings().setJavaScriptEnabled(true);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ this.mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+ }
+ this.mWebView.getSettings().setSaveFormData(false);
+ this.mWebView.getSettings().setSupportZoom(false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this.mWebView.setRendererPriorityPolicy(WebView.RENDERER_PRIORITY_WAIVED, false);
+ }
+ this.mWebView.setWebViewClient(this.new AppWebViewClient());
+
+ // Get the Android application's cache directory.
+ this.mCacheDir = context.getCacheDir();
+
+ // Start an asynchronous task to get the web client application.
+ AppSourceAsyncTask asyncTask = new AppSourceAsyncTask(this, App.APP_URL, this.mCacheDir);
+ this.mAsyncTask = asyncTask;
+ asyncTask.execute();
+ }
+
+ @MainThread
+ void stop() {
+ if (this.mAsyncTask != null) {
+ this.mAsyncTask.cancel(true);
+ this.mAsyncTask = null;
+ }
+
+ this.mLoaded = false;
+ this.mReady = false;
+ this.mStarted = false;
+ this.mInteractionListener.onStateChanged(State.NOT_LOADED);
+
+ if (this.mWebView != null) {
+ final ViewParent parent = this.mWebView.getParent();
+ if (parent instanceof ViewManager) {
+ ((ViewManager)parent).removeView(this.mWebView);
+ }
+
+ this.mWebView.destroy();
+ this.mWebView = null;
+ }
+ }
+
+ @MainThread
+ void submitEvent(@NonNull final JSONObject event) {}
+}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/AppJavaBridge.java b/zapic/src/main/java/com/zapic/android/sdk/AppJavaBridge.java
deleted file mode 100644
index 694f4b9..0000000
--- a/zapic/src/main/java/com/zapic/android/sdk/AppJavaBridge.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package com.zapic.android.sdk;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import android.webkit.WebView;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-
-/**
- * A communication bridge that dispatches messages from the Android application's Java context to
- * the web client application's JavaScript context (running in an Android
- * {@see android.webkit.WebView}).
- *
- * @author Kyle Dodson
- * @since 1.0.0
- */
-final class AppJavaBridge {
- /**
- * The tag used to identify log entries.
- */
- @NonNull
- private static final String TAG = "AppJavaBridge";
-
- /**
- * The handler that sends messages on the UI thread.
- */
- @NonNull
- private final Handler handler;
-
- /**
- * The synchronization lock for {@see queue}, {@see queueMessageScheduled}, and {@see webView}.
- */
- @NonNull
- private final Object lock;
-
- /**
- * The queue of actions.
- */
- @NonNull
- private final ArrayList
- * This is not invoked on the UI thread.
- *
- * @param message The message.
- */
- @JavascriptInterface
- public void dispatch(@Nullable final String message) {
- JSONObject action;
- try {
- action = new JSONObject(message);
- } catch (JSONException e) {
- Log.e(TAG, "Failed to parse serialized action", e);
- return;
- }
-
- String type;
- try {
- type = action.getString("type");
- } catch (JSONException e) {
- Log.e(TAG, "The action does not have a type", e);
- return;
- }
-
- switch (type) {
- case "APP_LOADED":
- case "LOGIN":
- case "LOGGED_IN":
- case "SHOW_BANNER":
- case "APP_STARTED":
- case "CLOSE_PAGE_REQUESTED":
- Log.d(TAG, message);
- break;
- default:
- Log.e(TAG, String.format("The action type is invalid: %s", type));
- break;
- }
- }
-}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/AppManager.java b/zapic/src/main/java/com/zapic/android/sdk/AppManager.java
deleted file mode 100644
index 622d197..0000000
--- a/zapic/src/main/java/com/zapic/android/sdk/AppManager.java
+++ /dev/null
@@ -1,340 +0,0 @@
-package com.zapic.android.sdk;
-
-import android.annotation.SuppressLint;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.support.annotation.AnyThread;
-import android.support.annotation.CheckResult;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.util.Log;
-import android.webkit.RenderProcessGoneDetail;
-import android.webkit.WebResourceRequest;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import java.io.File;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-final class AppManager {
- private enum State {
- UNLOADED,
- LOADED
- }
-
- @NonNull
- private static final String JAVASCRIPT_VARIABLE_NAME = "androidWebView";
-
- @NonNull
- private static final String TAG = "AppManager";
-
- /**
- * The web client URL.
- */
- @NonNull
- private static final String WEB_CLIENT_URL = "https://app.zapic.net";
-
- private static final Object instanceLock = new Object();
-
- private static WeakReference
- * This currently only overrides navigation events to {@code market://*} URLs by opening the
- * the Google Play Store app.
- *
- * @param view The {@see android.webkit.WebView}.
- * @param url The {@see Uri} of the navigation event.
- * @return {@code true} if the navigation behavior was overridden; {@code false} if the
- * navigation behavior was not overridden.
- */
- private static boolean overrideUrlLoading(@NonNull final WebView view, @NonNull final Uri url) {
- final String scheme = url.getScheme();
- if (scheme != null && scheme.equalsIgnoreCase("market")) {
- try {
- // Create a Google Play Store app intent.
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(url);
-
- // Open the Google Play Store app.
- Context context = view.getContext();
- context.startActivity(intent);
- } catch (ActivityNotFoundException ignored) {
- }
-
- // Prevent the WebView from navigating to an invalid URL.
- return true;
- }
-
- return false;
- }
-
- @MainThread
- void setOffline() {
- if (this.mOffline) {
- return;
- }
-
- // Cancel any asynchronous task.
- if (this.mAsyncTask != null) {
- this.mAsyncTask.cancel(true);
- this.mAsyncTask = null;
- }
-
- // Notify listeners.
- this.mOffline = true;
- for (StateChangedListener listener : this.mListeners) {
- listener.onOffline();
- }
- }
-
- @MainThread
- void setOnline() {
- if (!this.mOffline) {
- return;
- }
-
- // Notify listeners.
- this.mOffline = false;
- for (StateChangedListener listener : this.mListeners) {
- listener.onOnline();
- }
-
- // Start an asynchronous task to get the web client application.
- if (this.mState == State.UNLOADED && this.mCacheDir != null) {
- AppSourceAsyncTask asyncTask = new AppSourceAsyncTask(this, AppManager.WEB_CLIENT_URL, this.mCacheDir);
- this.mAsyncTask = asyncTask;
- asyncTask.execute();
- }
- }
-
- interface StateChangedListener {
- @MainThread
- void onAppStarted();
-
- @MainThread
- void onOffline();
-
- @MainThread
- void onOnline();
- }
-
- @MainThread
- void addStateChangedListener(@NonNull final StateChangedListener listener) {
- if (!this.mListeners.contains(listener)) {
- this.mListeners.add(listener);
- }
- }
-
- @MainThread
- void removeStateChangedListener(@NonNull final StateChangedListener listener) {
- this.mListeners.remove(listener);
- }
-}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java
index 8094331..5b5e8f0 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java
@@ -1,22 +1,27 @@
package com.zapic.android.sdk;
+import android.app.Activity;
import android.app.Fragment;
+import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.CheckResult;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.transition.Slide;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewManager;
+import android.view.ViewParent;
import android.webkit.WebView;
import android.widget.FrameLayout;
/**
- * A {@link Fragment} that renders the web client application page.
+ * A {@link Fragment} that presents the Zapic JavaScript application page.
*
- * Use the {@link AppPageFragment#createInstance} factory method to create instances of this
- * fragment.
+ * Use the {@link #createInstance()} factory method to create instances of this fragment.
*
* @author Kyle Dodson
* @since 1.0.0
@@ -29,50 +34,91 @@ public final class AppPageFragment extends Fragment {
private static final String TAG = "AppPageFragment";
/**
- * Creates a new instance.
+ * The {@link WebView}.
*/
+ @Nullable
+ private WebView mWebView;
+
+ /**
+ * Creates a new {@link AppPageFragment} instance.
+ */
+ @MainThread
public AppPageFragment() {
+ this.mWebView = null;
}
/**
- * Creates a new instance of the {@link AppPageFragment} class.
+ * Creates a new {@link LoadingPageFragment} instance.
*
- * @return The new instance of the {@link AppPageFragment} class.
+ * @return The new {@link LoadingPageFragment} instance.
*/
+ @CheckResult
+ @MainThread
@NonNull
public static AppPageFragment createInstance() {
return new AppPageFragment();
}
+ @MainThread
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onActivityCreated");
+ }
+
+ super.onActivityCreated(savedInstanceState);
+
+ if (this.mWebView == null) {
+ final Activity activity = this.getActivity();
+ if (activity instanceof ZapicActivity) {
+ final ZapicActivity zapicActivity = (ZapicActivity) activity;
+ this.mWebView = zapicActivity.getWebView();
+ assert this.mWebView != null : "mWebView == null";
+
+ final FrameLayout.LayoutParams webViewLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+ this.mWebView.setLayoutParams(webViewLayoutParams);
+
+ View view = this.getView();
+ assert view != null : "view == null";
+
+ final FrameLayout frameLayout = view.findViewById(R.id.fragment_page_app_container);
+ frameLayout.addView(this.mWebView);
+ }
+ }
+ }
+
@MainThread
@Override
public void onCreate(final Bundle savedInstanceState) {
- Log.d(TAG, "onCreate");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+
super.onCreate(savedInstanceState);
- // This "recycles" the fragment instance when configuration changes occur allowing instance
- // variables to be retained. Note that the fragment is detached from the old activity and
- // then attached to the new activity.
- this.setRetainInstance(true);
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ final Slide slide = new Slide();
+ slide.setDuration(500);
+ this.setEnterTransition(slide);
+ }
}
@MainThread
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
- Log.d(TAG, "onCreateView");
- final View view = inflater.inflate(R.layout.fragment_page_app, container, false);
-
- final AppManager appManager = AppManager.getInstance();
- final WebView webView = appManager.getWebView();
- if (webView == null) {
- return view;
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreateView");
}
- final FrameLayout.LayoutParams webViewLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
- webView.setLayoutParams(webViewLayoutParams);
+ View view = inflater.inflate(R.layout.fragment_page_app, container, false);
+
+ if (this.mWebView != null) {
+ final FrameLayout.LayoutParams webViewLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+ this.mWebView.setLayoutParams(webViewLayoutParams);
- final FrameLayout frameLayout = view.findViewById(R.id.fragment_page_app_container);
- frameLayout.addView(webView);
+ final FrameLayout frameLayout = view.findViewById(R.id.fragment_page_app_container);
+ frameLayout.addView(this.mWebView);
+ }
return view;
}
@@ -80,12 +126,17 @@ public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup
@MainThread
@Override
public void onDestroyView() {
- Log.d(TAG, "onDestroyView");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onDestroyView");
+ }
+
+ if (this.mWebView != null) {
+ ViewParent parent = this.mWebView.getParent();
+ if (parent instanceof ViewManager) {
+ ((ViewManager) parent).removeView(this.mWebView);
+ }
- final AppManager appManager = AppManager.getInstance();
- final WebView webView = appManager.getWebView();
- if (webView != null) {
- ((ViewManager)webView.getParent()).removeView(webView);
+ this.mWebView = null;
}
super.onDestroyView();
diff --git a/zapic/src/main/java/com/zapic/android/sdk/AppSource.java b/zapic/src/main/java/com/zapic/android/sdk/AppSource.java
index a42a3f2..37a1d33 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/AppSource.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/AppSource.java
@@ -64,7 +64,7 @@ String getHtml() {
* Gets the Last-Modified date and time in milliseconds since January 1, 1970.
*
* @return The Last-Modified date and time in milliseconds since January 1, 1970 or {@code 0}
- * if one was not returned in the HTTP response.
+ * if one was not returned in the HTTP response.
*/
long getLastModified() {
return this.lastModified;
diff --git a/zapic/src/main/java/com/zapic/android/sdk/AppSourceAsyncTask.java b/zapic/src/main/java/com/zapic/android/sdk/AppSourceAsyncTask.java
index 5349308..9a4dd3d 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/AppSourceAsyncTask.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/AppSourceAsyncTask.java
@@ -32,7 +32,7 @@ final class AppSourceAsyncTask extends AsyncTask
+ * This normally indicates the device gained network connectivity. However, it may also indicate
+ * data saver has been disabled.
+ */
+ @MainThread
+ void onConnected();
+
+ /**
+ * Called when the application loses network connectivity.
+ *
+ * This normally indicates the device lost network connectivity. However, it may also indicate
+ * data saver has been enabled.
+ */
+ @MainThread
+ void onDisconnected();
+}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityManagerUtilities.java b/zapic/src/main/java/com/zapic/android/sdk/ConnectivityManagerUtilities.java
new file mode 100644
index 0000000..6906625
--- /dev/null
+++ b/zapic/src/main/java/com/zapic/android/sdk/ConnectivityManagerUtilities.java
@@ -0,0 +1,73 @@
+package com.zapic.android.sdk;
+
+import android.app.Activity;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.AnyThread;
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+
+/**
+ * Provides {@link ConnectivityManager} utility methods.
+ *
+ * @author Kyle Dodson
+ * @since 1.0.0
+ */
+final class ConnectivityManagerUtilities {
+ /**
+ * Prevents creating a new {@link ConnectivityManagerUtilities} instance.
+ */
+ @AnyThread
+ private ConnectivityManagerUtilities() {
+ }
+
+ /**
+ * Gets a value indicating whether the application has network connectivity.
+ *
+ * This normally indicates whether the device has network connectivity. However, it may also
+ * indicate whether data saver has been enabled.
+ *
+ * @param connectivityManager The Android device's connectivity manager.
+ * @return {@code true} if the Android device is connected to a network; {@code false} if the
+ * Android device is not connected to a network.
+ */
+ @AnyThread
+ @CheckResult
+ static boolean isConnected(@NonNull ConnectivityManager connectivityManager) {
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /**
+ * Registers and returns a {@link ConnectivityBroadcastReceiver} using the specified activity's
+ * context. This should be unregistered using {@link #unregisterConnectivityBroadcastReceiver}.
+ *
+ * @param activity The activity.
+ * @param listener The network connectivity listener.
+ * @return The {@link ConnectivityBroadcastReceiver}.
+ * @see #unregisterConnectivityBroadcastReceiver
+ */
+ @AnyThread
+ @NonNull
+ static ConnectivityBroadcastReceiver registerConnectivityBroadcastReceiver(@NonNull final Activity activity, @NonNull final ConnectivityListener listener) {
+ final IntentFilter connectivityFilter = new IntentFilter();
+ connectivityFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+
+ final ConnectivityBroadcastReceiver broadcastReceiver = new ConnectivityBroadcastReceiver(listener);
+ activity.registerReceiver(broadcastReceiver, connectivityFilter);
+
+ return broadcastReceiver;
+ }
+
+ /**
+ * Unregisters a {@link ConnectivityBroadcastReceiver} using the specified activity's context.
+ *
+ * @param activity The activity.
+ * @param broadcastReceiver The {@link ConnectivityBroadcastReceiver}.
+ */
+ @AnyThread
+ static void unregisterConnectivityBroadcastReceiver(@NonNull final Activity activity, @NonNull final ConnectivityBroadcastReceiver broadcastReceiver) {
+ activity.unregisterReceiver(broadcastReceiver);
+ }
+}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityReceiver.java b/zapic/src/main/java/com/zapic/android/sdk/ConnectivityReceiver.java
deleted file mode 100644
index 496f737..0000000
--- a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityReceiver.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.zapic.android.sdk;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.support.annotation.NonNull;
-
-final class ConnectivityReceiver extends BroadcastReceiver {
- @NonNull
- private final AppManager mAppManager;
-
- ConnectivityReceiver(@NonNull final AppManager appManager) {
- this.mAppManager = appManager;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivityManager == null) {
- return;
- }
-
- final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
- if (networkInfo != null && networkInfo.isConnected()) {
- this.mAppManager.setOnline();
- } else {
- this.mAppManager.setOffline();
- }
- }
-}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java
index 41c65b8..2df44f6 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java
@@ -2,27 +2,50 @@
import android.app.Fragment;
import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.CheckResult;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.transition.Slide;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
/**
- * A {@link Fragment} that renders a loading page.
+ * A {@link Fragment} that presents a loading page.
*
- * Use the {@link LoadingPageFragment#createInstance} factory method to create instances of this
- * fragment.
+ * Use the {@link #createInstance()} factory method to create instances of this fragment.
*
- * Activities that include this fragment must implement the
- * {@link LoadingPageFragment.InteractionListener} interface.
+ * Activities that present this fragment must implement the {@link InteractionListener} interface.
*
* @author Kyle Dodson
* @since 1.0.0
*/
public final class LoadingPageFragment extends Fragment {
+ /**
+ * Handles {@link LoadingPageFragment} interaction events.
+ *
+ * @author Kyle Dodson
+ * @since 1.0.0
+ */
+ public interface InteractionListener {
+ /**
+ * Closes the page.
+ */
+ @MainThread
+ void close();
+ }
+
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "LoadingPageFragment";
+
/**
* The interaction listener.
*/
@@ -30,39 +53,85 @@ public final class LoadingPageFragment extends Fragment {
private InteractionListener mListener;
/**
- * Creates a new instance.
+ * Creates a new {@link LoadingPageFragment} instance.
*/
+ @MainThread
public LoadingPageFragment() {
}
/**
- * Creates a new instance of the {@link LoadingPageFragment} class.
+ * Creates a new {@link LoadingPageFragment} instance.
*
- * @return The new instance of the {@link LoadingPageFragment} class.
+ * @return The new {@link LoadingPageFragment} instance.
*/
+ @CheckResult
+ @MainThread
@NonNull
public static LoadingPageFragment createInstance() {
return new LoadingPageFragment();
}
+ @MainThread
@Override
public void onAttach(final Context context) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onAttach");
+ }
+
super.onAttach(context);
if (context instanceof InteractionListener) {
- this.mListener = (InteractionListener)context;
+ this.mListener = (InteractionListener) context;
} else {
throw new RuntimeException(String.format("%s must implement InteractionListener", context.toString()));
}
}
+ @MainThread
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+
+ super.onCreate(savedInstanceState);
+
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ final Slide slide = new Slide();
+ slide.setDuration(500);
+ this.setEnterTransition(slide);
+ }
+ }
+
+ @MainThread
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreateView");
+ }
+
return inflater.inflate(R.layout.fragment_page_loading, container, false);
}
+ @MainThread
+ @Override
+ public void onDetach() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onDetach");
+ }
+
+ super.onDetach();
+
+ this.mListener = null;
+ }
+
+ @MainThread
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onViewCreated");
+ }
+
super.onViewCreated(view, savedInstanceState);
ImageButton closeButton = view.findViewById(R.id.fragment_page_loading_close);
@@ -71,27 +140,9 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
public void onClick(View v) {
InteractionListener listener = LoadingPageFragment.this.mListener;
if (listener != null) {
- listener.onClose();
+ listener.close();
}
}
});
}
-
- @Override
- public void onDetach() {
- super.onDetach();
-
- this.mListener = null;
- }
-
- /**
- * Activities that include {@link LoadingPageFragment} must implement this interface to handle
- * events.
- */
- public interface InteractionListener {
- /**
- * Called when the user requests that the page be closed.
- */
- void onClose();
- }
}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java
index a1724c4..c112997 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java
@@ -2,27 +2,50 @@
import android.app.Fragment;
import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.CheckResult;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.transition.Slide;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
/**
- * A {@link Fragment} that renders an offline page.
+ * A {@link Fragment} that presents an offline page.
*
- * Use the {@link OfflinePageFragment#createInstance} factory method to create instances of this
- * fragment.
+ * Use the {@link #createInstance()} factory method to create instances of this fragment.
*
- * Activities that include this fragment must implement the
- * {@link OfflinePageFragment.InteractionListener} interface.
+ * Activities that present this fragment must implement the {@link InteractionListener} interface.
*
* @author Kyle Dodson
* @since 1.0.0
*/
public final class OfflinePageFragment extends Fragment {
+ /**
+ * Handles {@link OfflinePageFragment} interaction events.
+ *
+ * @author Kyle Dodson
+ * @since 1.0.0
+ */
+ public interface InteractionListener {
+ /**
+ * Closes the page.
+ */
+ @MainThread
+ void close();
+ }
+
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "OfflinePageFragment";
+
/**
* The interaction listener.
*/
@@ -30,39 +53,85 @@ public final class OfflinePageFragment extends Fragment {
private InteractionListener mListener;
/**
- * Creates a new instance.
+ * Creates a new {@link OfflinePageFragment} instance.
*/
+ @MainThread
public OfflinePageFragment() {
}
/**
- * Creates a new instance of the {@link OfflinePageFragment} class.
+ * Creates a new {@link OfflinePageFragment} instance.
*
- * @return The new instance of the {@link OfflinePageFragment} class.
+ * @return The new {@link OfflinePageFragment} instance.
*/
+ @CheckResult
+ @MainThread
@NonNull
public static OfflinePageFragment createInstance() {
return new OfflinePageFragment();
}
+ @MainThread
@Override
public void onAttach(final Context context) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onAttach");
+ }
+
super.onAttach(context);
if (context instanceof InteractionListener) {
- this.mListener = (InteractionListener)context;
+ this.mListener = (InteractionListener) context;
} else {
throw new RuntimeException(String.format("%s must implement InteractionListener", context.toString()));
}
}
+ @MainThread
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+
+ super.onCreate(savedInstanceState);
+
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ final Slide slide = new Slide();
+ slide.setDuration(500);
+ this.setEnterTransition(slide);
+ }
+ }
+
+ @MainThread
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreateView");
+ }
+
return inflater.inflate(R.layout.fragment_page_offline, container, false);
}
+ @MainThread
+ @Override
+ public void onDetach() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onDetach");
+ }
+
+ super.onDetach();
+
+ this.mListener = null;
+ }
+
+ @MainThread
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onViewCreated");
+ }
+
super.onViewCreated(view, savedInstanceState);
ImageButton closeButton = view.findViewById(R.id.fragment_page_offline_close);
@@ -71,27 +140,9 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
public void onClick(View v) {
InteractionListener listener = OfflinePageFragment.this.mListener;
if (listener != null) {
- listener.onClose();
+ listener.close();
}
}
});
}
-
- @Override
- public void onDetach() {
- super.onDetach();
-
- this.mListener = null;
- }
-
- /**
- * Activities that include {@link OfflinePageFragment} must implement this interface to handle
- * events.
- */
- public interface InteractionListener {
- /**
- * Called when the user requests that the page be closed.
- */
- void onClose();
- }
}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/Zapic.java b/zapic/src/main/java/com/zapic/android/sdk/Zapic.java
index fd4a1fc..8104dde 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/Zapic.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/Zapic.java
@@ -4,33 +4,37 @@
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Intent;
+import android.support.annotation.AnyThread;
import android.support.annotation.CheckResult;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.util.Log;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
/**
- * Provides static methods to manage the Zapic application.
+ * Provides static methods to manage and interact with Zapic.
*
- * The Zapic application runs in a {@link WebView}. Generally, the {@link WebView} runs for the
- * lifetime of the Android application. While the game is in focus, the {@link WebView} runs in
- * the background (managed by a {@link ZapicFragment} attached to the game's activity) and the Zapic
- * application processes events and receives notifications. When the player shifts focus to the
- * Zapic application, the {@link WebView} moves to the foreground and is presented by a
- * {@link ZapicActivity}. When the player shifts focus back to the game, the {@link ZapicActivity}
- * finishes and the {@link WebView} again runs in the background (again managed by a
- * {@link ZapicFragment} attached to the game's activity).
+ * It is the responsibility of the game's activity (or game's activities if there are multiple) to
+ * call the {@link #attachFragment(Activity)} method during its {@code onCreate} lifecycle method.
+ * This method creates and attaches a non-UI fragment, {@link ZapicFragment}, to the activity. The
+ * {@link ZapicFragment} downloads and runs the Zapic JavaScript application. If a game's activity
+ * does not call {@link #attachFragment(Activity)}, the Zapic JavaScript application may be
+ * garbage collected and the current player's session reset. It is also the responsibility of the
+ * game's activity to call the {@link #show(Activity)} method after the player interacts with a
+ * Zapic-branded button visible from the game's main menu (requirements are outlined in the
+ * Terms of Use).
*
- * It is the responsibility of the game's activity (or activities if there are multiple) to call
- * {@link Zapic#attachFragment(Activity)} during its {@code onCreate} lifecycle callback. This will
- * create and attach a {@link ZapicFragment} to the game's activity. If the game's activity does
- * not call {@link Zapic#attachFragment(Activity)}, the {@link WebView} may be garbage
- * collected by the system.
+ * The Zapic JavaScript application runs in a {@link WebView}. Generally, the {@link WebView} runs
+ * for the lifetime of the Android application. While the game is in focus, the {@link WebView} runs
+ * in the background (managed by one or more {@link ZapicFragment}s attached to the game's
+ * activities). The Zapic JavaScript application processes events and receives notifications while
+ * running in the background. When the player shifts focus to Zapic, the {@link WebView} moves to
+ * the foreground and is presented by a {@link ZapicActivity}. When the player shifts focus back to
+ * the game, the {@link ZapicActivity} finishes and the {@link WebView} returns to the background
+ * (again managed by one or more {@link ZapicFragment}s attached to the game's activities).
*
* @author Kyle Dodson
* @since 1.0.0
@@ -46,10 +50,10 @@ public final class Zapic {
* The player's unique identifier.
*/
@Nullable
- private static String playerId;
+ private static volatile String playerId;
/**
- * Prevents creating a new instance.
+ * Prevents creating a new {@link Zapic} instance.
*/
private Zapic() {
}
@@ -59,11 +63,11 @@ private Zapic() {
*
* This must be called from the Android application's main thread.
*
- * @param gameActivity The game's activity.
+ * @param gameActivity The game's activity.
* @throws IllegalArgumentException If {@code gameActivity} is {@code null}.
*/
@MainThread
- @SuppressWarnings("UnusedDeclaration") // documented as public API
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
public static void attachFragment(@Nullable final Activity gameActivity) {
if (gameActivity == null) {
throw new IllegalArgumentException("gameActivity must not be null");
@@ -80,14 +84,13 @@ public static void attachFragment(@Nullable final Activity gameActivity) {
* Detaches a {@link ZapicFragment} from the specified game's activity.
*
* This must be called from the Android application's main thread. Generally, this should not be
- * called. The fragment is automatically destroyed and garbage collected when the game's
- * activity finishes.
+ * called. The fragment is automatically destroyed when the game's activity finishes.
*
- * @param gameActivity The game's activity.
+ * @param gameActivity The game's activity.
* @throws IllegalArgumentException If {@code gameActivity} is {@code null}.
*/
@MainThread
- @SuppressWarnings("UnusedDeclaration") // documented as public API
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
public static void detachFragment(@Nullable final Activity gameActivity) {
if (gameActivity == null) {
throw new IllegalArgumentException("gameActivity must not be null");
@@ -100,14 +103,30 @@ public static void detachFragment(@Nullable final Activity gameActivity) {
}
}
+ /**
+ * Gets a {@link ZapicFragment} from the specified game's activity.
+ *
+ * @param gameActivity The game's activity.
+ * @return The {@link ZapicFragment} or {@code null} if one has not been attached.
+ */
+ @CheckResult
+ @MainThread
+ @Nullable
+ static ZapicFragment getFragment(@NonNull final Activity gameActivity) {
+ final FragmentManager manager = gameActivity.getFragmentManager();
+ final Fragment fragment = manager.findFragmentByTag(Zapic.FRAGMENT_TAG);
+ return fragment instanceof ZapicFragment ? (ZapicFragment) fragment : null;
+ }
+
/**
* Gets the player's unique identifier.
*
* @return The player's unique identifier or {@code null} if the player has not logged in.
*/
+ @AnyThread
@CheckResult
@Nullable
- @SuppressWarnings("UnusedDeclaration") // documented as public API
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
public static String getPlayerId() {
return Zapic.playerId;
}
@@ -117,25 +136,26 @@ public static String getPlayerId() {
*
* @param playerId The player's unique identifier or {@code null} if the player has logged out.
*/
+ @AnyThread
static void setPlayerId(@Nullable final String playerId) {
Zapic.playerId = playerId;
}
/**
- * Shows the Zapic application.
+ * Shows the Zapic JavaScript application and opens the default page.
*
* This must be called from the Android application's main thread. This starts a
- * {@link ZapicActivity} and opens the default Zapic application page.
+ * {@link ZapicActivity} and opens the default Zapic JavaScript application page.
*
* This must be called from the Zapic-branded button visible from the game's main menu
* (requirements are outlined in the Terms of Use).
- * This may be called from other game elements or events.
+ * This may be called from other game elements or user interactions.
*
- * @param gameActivity The game's activity.
+ * @param gameActivity The game's activity.
* @throws IllegalArgumentException If {@code gameActivity} is {@code null}.
*/
@MainThread
- @SuppressWarnings("UnusedDeclaration") // documented as public API
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
public static void show(@Nullable final Activity gameActivity) {
if (gameActivity == null) {
throw new IllegalArgumentException("gameActivity must not be null");
@@ -146,22 +166,22 @@ public static void show(@Nullable final Activity gameActivity) {
}
/**
- * Shows the Zapic application and opens the specified Zapic application page.
+ * Shows the Zapic JavaScript application and opens the specified page.
*
* This must be called from the Android application's main thread. This starts a
- * {@link ZapicActivity} and opens the default Zapic application page.
+ * {@link ZapicActivity} and opens the specified Zapic JavaScript application page.
*
* This must not be called from the Zapic-branded button visible from the game's main
- * menu (see {@link Zapic#show(Activity)}. This may be called from other game elements or
- * events to refer players to more specific pages.
+ * menu (see {@link #show(Activity)}. This may be called from other game elements or
+ * user interactions to navigate players to specific pages.
*
- * @param gameActivity The game's activity.
- * @param page The Zapic application page to open.
+ * @param gameActivity The game's activity.
+ * @param page The Zapic JavaScript application page to open.
* @throws IllegalArgumentException If {@code gameActivity} or {@code page} are {@code null}.
*/
@MainThread
- @SuppressWarnings("UnusedDeclaration") // documented as public API
- public static void show(@Nullable final Activity gameActivity, @Nullable final String page) {
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
+ public static void show(@Nullable final Activity gameActivity, @SuppressWarnings("SameParameterValue") @Nullable final String page) {
if (gameActivity == null) {
throw new IllegalArgumentException("gameActivity must not be null");
}
@@ -177,15 +197,17 @@ public static void show(@Nullable final Activity gameActivity, @Nullable final S
/**
* Submits a gameplay event to Zapic.
*
- * @param gameActivity The game's activity.
- * @param parameters The JSON-encoded object of gameplay event parameters.
+ * @param gameActivity The game's activity.
+ * @param parameters The JSON-encoded object of gameplay event parameters.
* @throws IllegalArgumentException If {@code gameActivity} or {@code parameters} are
- * {@code null} or if {@code parameters} is not a valid JSON
- * object.
+ * {@code null}; if {@code gameActivity} does not have a
+ * {@link ZapicFragment} attached (see
+ * {@link #attachFragment(Activity)}; if {@code parameters} is
+ * not a valid JSON object.
*/
@MainThread
- @SuppressWarnings("UnusedDeclaration") // documented as public API
- public static void submitEvent(@Nullable Activity gameActivity, @Nullable String parameters) {
+ @SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
+ public static void submitEvent(@Nullable final Activity gameActivity, @Nullable final String parameters) {
if (gameActivity == null) {
throw new IllegalArgumentException("gameActivity must not be null");
}
@@ -194,6 +216,16 @@ public static void submitEvent(@Nullable Activity gameActivity, @Nullable String
throw new IllegalArgumentException("parameters must not be null");
}
+ final ZapicFragment fragment = Zapic.getFragment(gameActivity);
+ if (fragment == null) {
+ throw new IllegalArgumentException("gameActivity must have a ZapicFragment attached");
+ }
+
+ final App app = fragment.getApp();
+ if (app == null) {
+ throw new IllegalArgumentException("gameActivity must have a ZapicFragment attached");
+ }
+
JSONObject payload;
try {
payload = new JSONObject(parameters);
@@ -204,11 +236,10 @@ public static void submitEvent(@Nullable Activity gameActivity, @Nullable String
JSONObject gameplayEvent;
try {
gameplayEvent = new JSONObject().put("type", "gameplay").put("payload", payload);
- } catch (JSONException e) {
- throw new IllegalArgumentException("payload must be a valid JSON object");
+ } catch (JSONException ignored) {
+ return;
}
- // TODO: Submit event.
- Log.d("Zapic", String.format("Gameplay Event: %s", gameplayEvent));
+ app.submitEvent(gameplayEvent);
}
}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java b/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java
index 9724625..19e53c5 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java
@@ -4,45 +4,41 @@
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.transition.Slide;
import android.util.Log;
-import android.view.View;
import android.webkit.WebView;
/**
- * An {@link Activity} that presents the Zapic application.
+ * An {@link Activity} that presents the Zapic JavaScript application in the foreground.
*
- * Use the {@link ZapicActivity#createIntent} factory method to create an intent that starts this
- * activity and opens a specific Zapic application page.
+ * Use {@link Zapic#show(Activity)} or {@link Zapic#show(Activity, String)} to start instances of
+ * this activity. Alternatively, use the {@link #createIntent(Activity)} or
+ * {@link #createIntent(Activity, String)} factory methods to create an intent that starts instances
+ * of this activity.
*
- * The Zapic application runs in a {@link WebView}. Generally, the {@link WebView} runs for the
- * lifetime of the Android application. While the game is in focus, the {@link WebView} runs in
- * the background (managed by a {@link ZapicFragment} attached to the game's activity) and the Zapic
- * application processes events and receives notifications. When the player shifts focus to the
- * Zapic application, the {@link WebView} moves to the foreground and is presented by a
- * {@link ZapicActivity}. When the player shifts focus back to the game, the {@link ZapicActivity}
- * finishes and the {@link WebView} again runs in the background (again managed by a
- * {@link ZapicFragment} attached to the game's activity).
+ * The Zapic JavaScript application runs in a {@link WebView}. Generally, the {@link WebView} runs
+ * for the lifetime of the Android application. While the game is in focus, the {@link WebView} runs
+ * in the background (managed by one or more {@link ZapicFragment}s attached to the game's
+ * activities). The Zapic JavaScript application processes events and receives notifications while
+ * running in the background. When the player shifts focus to Zapic, the {@link WebView} moves to
+ * the foreground and is presented by a {@link ZapicActivity}. When the player shifts focus back to
+ * the game, the {@link ZapicActivity} finishes and the {@link WebView} returns to the background
+ * (again managed by one or more {@link ZapicFragment}s attached to the game's activities).
*
* @author Kyle Dodson
* @since 1.0.0
*/
public final class ZapicActivity extends Activity implements
- AppManager.StateChangedListener,
LoadingPageFragment.InteractionListener,
OfflinePageFragment.InteractionListener {
/**
- * The fragment tag used to identify {@link ZapicFragment} instances.
- */
- @NonNull
- private static final String FRAGMENT_TAG = "Zapic";
-
- /**
- * The {@link Intent} parameter that identifies the Zapic application page to open.
+ * The {@link Intent} parameter that identifies the Zapic JavaScript application page to open.
*/
@NonNull
private static final String PAGE_PARAM = "page";
@@ -51,28 +47,29 @@ public final class ZapicActivity extends Activity implements
* The tag used to identify log messages.
*/
@NonNull
- private static final String TAG = "ZapicFragment";
+ private static final String TAG = "ZapicActivity";
/**
- * The Zapic application manager.
+ * The {@link WebView}.
*/
@Nullable
- private AppManager mAppManager;
+ private WebView mWebView;
/**
- * Creates a new instance.
+ * Creates a new {@link ZapicActivity} instance
*/
public ZapicActivity() {
- this.mAppManager = null;
+ this.mWebView = null;
}
/**
* Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the default Zapic
- * application page.
+ * JavaScript application page.
*
* @param gameActivity The game's activity.
- * @return The {@link Intent}.
+ * @return The {@link Intent}.
*/
+ @MainThread
@CheckResult
@NonNull
public static Intent createIntent(@NonNull final Activity gameActivity) {
@@ -81,149 +78,176 @@ public static Intent createIntent(@NonNull final Activity gameActivity) {
/**
* Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the specified Zapic
- * application page.
+ * JavaScript application page.
*
* @param gameActivity The game's activity.
- * @param page The Zapic application page to open.
- * @return The {@link Intent}.
+ * @param page The Zapic JavaScript application page to open.
+ * @return The {@link Intent}.
*/
+ @MainThread
@CheckResult
@NonNull
public static Intent createIntent(@NonNull final Activity gameActivity, @NonNull final String page) {
final Intent intent = new Intent(gameActivity, ZapicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.putExtra(ZapicActivity.PAGE_PARAM, page);
return intent;
}
+ @Override
+ public void close() {
+ this.finish();
+ }
+
+// TODO: Determine if the following is required when we use a fullscreen theme.
+// /**
+// * Enables an immersive full-screen mode. This hides the system status and navigation bars until
+// * the user swipes in from the edges of the screen.
+// */
+// @MainThread
+// private void enableImmersiveFullScreenMode() {
+// this.getWindow().getDecorView().setSystemUiVisibility(
+// View.SYSTEM_UI_FLAG_FULLSCREEN
+// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+// | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+// | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+// | View.SYSTEM_UI_FLAG_LOW_PROFILE);
+// }
+
/**
- * Enables an immersive full-screen mode. This hides the system status and navigation bars until
- * the user swipes in from the edges of the screen.
+ * Gets the {@link WebView}.
+ *
+ * @return The {@link WebView}.
*/
+ @CheckResult
@MainThread
- private void enableImmersiveFullScreenMode() {
- this.getWindow().getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_FULLSCREEN
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ @Nullable
+ WebView getWebView() {
+ return this.mWebView;
}
@MainThread
@Override
- public void onAppStarted() {
- assert this.mAppManager != null : "mAppManager == null";
-
- // TODO: Show loading page, dispatch OPEN_PAGE, wait for PAGE_READY.
- final FragmentManager fragmentManager = this.getFragmentManager();
- final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
- if (!(currentFragment instanceof AppPageFragment)) {
- fragmentManager.beginTransaction().add(R.id.activity_zapic_container, AppPageFragment.createInstance()).commit();
+ protected void onCreate(final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreate");
}
- }
- @Override
- public void onOffline() {
- assert this.mAppManager != null : "mAppManager == null";
- if (this.mAppManager.isAppStarted()) {
- return;
- }
+ super.onCreate(savedInstanceState);
- final FragmentManager fragmentManager = this.getFragmentManager();
- final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
- if (!(currentFragment instanceof OfflinePageFragment)) {
- fragmentManager.beginTransaction().add(R.id.activity_zapic_container, OfflinePageFragment.createInstance()).commit();
- }
- }
+// TODO: Determine if the following is required when we use a fullscreen theme.
+// // Hide the system status and navigation bars.
+// this.enableImmersiveFullScreenMode();
- @Override
- public void onOnline() {
- assert this.mAppManager != null : "mAppManager == null";
- if (this.mAppManager.isAppStarted()) {
- return;
+ this.setContentView(R.layout.activity_zapic);
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ final Slide slide = new Slide();
+ slide.setDuration(500);
+ this.getWindow().setEnterTransition(slide);
}
- final FragmentManager fragmentManager = this.getFragmentManager();
- final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
- if (!(currentFragment instanceof LoadingPageFragment)) {
- fragmentManager.beginTransaction().add(R.id.activity_zapic_container, LoadingPageFragment.createInstance()).commit();
+ // Attach Zapic.
+ Zapic.attachFragment(this);
+ this.getFragmentManager().executePendingTransactions();
+
+ ZapicFragment fragment = Zapic.getFragment(this);
+ assert fragment != null : "fragment == null";
+ App app = fragment.getApp();
+ assert app != null : "app == null";
+ this.mWebView = app.getWebView();
+ assert this.mWebView != null : "mWebView == null";
+
+ switch (app.getState()) {
+ case READY:
+ this.openAppPage();
+ break;
+ case STARTED:
+ this.openLoadingPage();
+ String page = this.getIntent().getStringExtra("page");
+ if (page == null) {
+ page = "default";
+ }
+
+ final String escapedPage = page.replace("'", "\\'");
+ this.mWebView.evaluateJavascript("window.zapic.dispatch({ type: 'OPEN_PAGE', payload: '" + escapedPage + "' })", null);
+ break;
+ default:
+ if (app.getConnected()) {
+ this.openLoadingPage();
+ } else {
+ this.openOfflinePage();
+ }
}
}
@MainThread
@Override
- public void onClose() {
- this.finish();
- }
-
- @MainThread
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- Log.d(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- // Hide the system status and navigation bars.
- this.enableImmersiveFullScreenMode();
-
- // Get a reference to the AppManager (to keep it from being garbage collected).
- if (this.mAppManager == null) {
- this.mAppManager = AppManager.getInstance();
- }
-
- // Listen to application state changes.
- this.mAppManager.addStateChangedListener(this);
-
- // Render the page.
- this.setContentView(R.layout.activity_zapic);
- final Fragment fragment;
- if (this.mAppManager.isAppStarted()) {
- // TODO: Show loading page, dispatch OPEN_PAGE, wait for PAGE_READY.
- fragment = AppPageFragment.createInstance();
- } else if (this.mAppManager.isOffline()) {
- fragment = OfflinePageFragment.createInstance();
- } else {
- fragment = LoadingPageFragment.createInstance();
+ protected void onNewIntent(final Intent intent) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onNewIntent");
}
- final FragmentManager fragmentManager = this.getFragmentManager();
- fragmentManager.beginTransaction().add(R.id.activity_zapic_container, fragment).commit();
- fragmentManager.beginTransaction().add(ZapicFragment.createInstance(), ZapicActivity.FRAGMENT_TAG).commit();
+ super.onNewIntent(intent);
+ this.setIntent(intent);
}
- @MainThread
- @Override
- public void onDestroy() {
- Log.d(TAG, "onDestroy");
- super.onDestroy();
+// TODO: Determine if the following is required when we use a fullscreen theme.
+// @MainThread
+// @Override
+// public void onWindowFocusChanged(final boolean hasFocus) {
+// if (BuildConfig.DEBUG) {
+// Log.d(TAG, "onWindowFocusChanged");
+// }
+//
+// super.onWindowFocusChanged(hasFocus);
+//
+// // Hide the system status and navigation bars.
+// if (hasFocus) {
+// this.enableImmersiveFullScreenMode();
+// }
+// }
- // Destroy the WebView (if this is the last view).
- assert this.mAppManager != null : "mAppManager == null";
- this.mAppManager.removeStateChangedListener(this);
- this.mAppManager = null;
+ /**
+ * Opens the Zapic JavaScript application page.
+ */
+ @MainThread
+ void openAppPage() {
+ final FragmentManager fragmentManager = this.getFragmentManager();
+ final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
+ if (currentFragment == null) {
+ fragmentManager.beginTransaction().add(R.id.activity_zapic_container, AppPageFragment.createInstance()).commit();
+ } else if (!(currentFragment instanceof AppPageFragment)) {
+ fragmentManager.beginTransaction().replace(R.id.activity_zapic_container, AppPageFragment.createInstance()).commit();
+ }
}
+ /**
+ * Opens the loading page.
+ */
@MainThread
- @Override
- protected void onNewIntent(final Intent intent) {
- Log.d(TAG, "onNewIntent");
- super.onNewIntent(intent);
- this.setIntent(intent);
-
- // TODO: If the app is loaded, navigate to the requested page. If the app is not loaded,
- // ensure we navigate to the newly requested page.
+ void openLoadingPage() {
+ final FragmentManager fragmentManager = this.getFragmentManager();
+ final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
+ if (currentFragment == null) {
+ fragmentManager.beginTransaction().add(R.id.activity_zapic_container, LoadingPageFragment.createInstance()).commit();
+ } else if (!(currentFragment instanceof LoadingPageFragment)) {
+ fragmentManager.beginTransaction().replace(R.id.activity_zapic_container, LoadingPageFragment.createInstance()).commit();
+ }
}
+ /**
+ * Opens the offline page.
+ */
@MainThread
- @Override
- public void onWindowFocusChanged(final boolean hasFocus) {
- Log.d(TAG, "onWindowFocusChanged");
- super.onWindowFocusChanged(hasFocus);
-
- // Hide the system status and navigation bars.
- if (hasFocus) {
- this.enableImmersiveFullScreenMode();
+ void openOfflinePage() {
+ final FragmentManager fragmentManager = this.getFragmentManager();
+ final Fragment currentFragment = fragmentManager.findFragmentById(R.id.activity_zapic_container);
+ if (currentFragment == null) {
+ fragmentManager.beginTransaction().add(R.id.activity_zapic_container, OfflinePageFragment.createInstance()).commit();
+ } else if (!(currentFragment instanceof OfflinePageFragment)) {
+ fragmentManager.beginTransaction().replace(R.id.activity_zapic_container, OfflinePageFragment.createInstance()).commit();
}
}
@@ -234,32 +258,52 @@ public void onWindowFocusChanged(final boolean hasFocus) {
@MainThread
@Override
protected void onStart() {
- Log.d(TAG, "onStart");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onStart");
+ }
+
super.onStart();
}
@MainThread
@Override
protected void onResume() {
- Log.d(TAG, "onResume");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onResume");
+ }
+
super.onResume();
}
@MainThread
@Override
protected void onPause() {
- Log.d(TAG, "onPause");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onPause");
+ }
+
super.onPause();
}
@MainThread
@Override
protected void onStop() {
- Log.d(TAG, "onStop");
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onStop");
+ }
+
super.onStop();
}
- // onDestroy
+ @MainThread
+ @Override
+ public void onDestroy() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onDestroy");
+ }
+
+ super.onDestroy();
+ }
//endregion
}
diff --git a/zapic/src/main/java/com/zapic/android/sdk/ZapicFragment.java b/zapic/src/main/java/com/zapic/android/sdk/ZapicFragment.java
index 09ab5aa..0350115 100644
--- a/zapic/src/main/java/com/zapic/android/sdk/ZapicFragment.java
+++ b/zapic/src/main/java/com/zapic/android/sdk/ZapicFragment.java
@@ -1,10 +1,11 @@
package com.zapic.android.sdk;
+import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.CheckResult;
import android.support.annotation.MainThread;
@@ -15,25 +16,330 @@
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
+import android.widget.Toast;
+
+import com.google.android.gms.auth.api.signin.GoogleSignIn;
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
+import com.google.android.gms.auth.api.signin.GoogleSignInClient;
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
+import com.google.android.gms.common.api.ApiException;
+import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.Task;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
/**
- * A {@link Fragment} that runs the Zapic application in the background.
+ * A {@link Fragment} that runs the Zapic JavaScript application in the background.
*
- * Use the {@link ZapicFragment#createInstance} factory method to create instances of this fragment.
+ * Use the {@link Zapic#attachFragment(Activity)} method to create and attach instances of this
+ * fragment to your game's activity. Alternatively, use the {@link #createInstance()} factory method
+ * to create instances of this fragment.
*
- * The Zapic application runs in a {@link WebView}. Generally, the {@link WebView} runs for the
- * lifetime of the Android application. While the game is in focus, the {@link WebView} runs in
- * the background (managed by a {@link ZapicFragment} attached to the game's activity) and the Zapic
- * application processes events and receives notifications. When the player shifts focus to the
- * Zapic application, the {@link WebView} moves to the foreground and is presented by a
- * {@link ZapicActivity}. When the player shifts focus back to the game, the {@link ZapicActivity}
- * finishes and the {@link WebView} again runs in the background (again managed by a
- * {@link ZapicFragment} attached to the game's activity).
+ * The Zapic JavaScript application runs in a {@link WebView}. Generally, the {@link WebView} runs
+ * for the lifetime of the Android application. While the game is in focus, the {@link WebView} runs
+ * in the background (managed by one or more {@link ZapicFragment}s attached to the game's
+ * activities). The Zapic JavaScript application processes events and receives notifications while
+ * running in the background. When the player shifts focus to Zapic, the {@link WebView} moves to
+ * the foreground and is presented by a {@link ZapicActivity}. When the player shifts focus back to
+ * the game, the {@link ZapicActivity} finishes and the {@link WebView} returns to the background
+ * (again managed by one or more {@link ZapicFragment}s attached to the game's activities).
*
* @author Kyle Dodson
* @since 1.0.0
*/
public final class ZapicFragment extends Fragment {
+ /**
+ * A utility class that coordinates the Zapic JavaScript application with the various views
+ * ({@link ZapicActivity}, {@link ZapicFragment}, and {@link AppPageFragment} instances).
+ *
+ * This starts and stops the Zapic JavaScript application when the first {@link ZapicFragment}
+ * is created and the last {@link ZapicFragment} is destroyed, respectively.
+ *
+ * This ensures at most one {@link ZapicActivity} is running to prevent adding the
+ * {@link WebView} to multiple view hierarchies.
+ *
+ * This adds and removes the {@link WebView} to and from the {@link AppPageFragment}.
+ *
+ * Use the {@link #getInstance()} method to get the singleton instance.
+ *
+ * @author Kyle Dodson
+ * @see App
+ * @see AppPageFragment
+ * @see ZapicActivity
+ * @see ZapicFragment
+ * @since 1.0.0
+ */
+ private static final class ZapicViewManager implements App.InteractionListener {
+ /**
+ * A weak reference to the {@link ZapicViewManager} "singleton instance". Technically, as a
+ * weak reference, this is not a singleton instance as defined by the software design
+ * pattern. The weak reference is required, though. It ensures we don't leak the Android
+ * application's context. In practice, we cannot discern this implementation difference.
+ * {@link ZapicFragment} instances keep a strong reference to this {@link ZapicViewManager}
+ * instance. As such, this {@link ZapicViewManager} instance can only be garbage collected
+ * after all of our {@link ZapicFragment} instances have been garbage collected. This is no
+ * different from the state at initial launch.
+ */
+ @NonNull
+ private static WeakReference