- * 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/AppPageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java deleted file mode 100644 index 5b5e8f0..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/AppPageFragment.java +++ /dev/null @@ -1,144 +0,0 @@ -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 presents the Zapic JavaScript application page. - *
- * Use the {@link #createInstance()} factory method to create instances of this fragment. - * - * @author Kyle Dodson - * @since 1.0.0 - */ -public final class AppPageFragment extends Fragment { - /** - * The tag used to identify log messages. - */ - @NonNull - private static final String TAG = "AppPageFragment"; - - /** - * The {@link WebView}. - */ - @Nullable - private WebView mWebView; - - /** - * Creates a new {@link AppPageFragment} instance. - */ - @MainThread - public AppPageFragment() { - this.mWebView = null; - } - - /** - * Creates a new {@link LoadingPageFragment} instance. - * - * @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) { - 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"); - } - - 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(this.mWebView); - } - - return view; - } - - @MainThread - @Override - public void 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); - } - - this.mWebView = null; - } - - super.onDestroyView(); - } -} diff --git a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityBroadcastReceiver.java b/zapic/src/main/java/com/zapic/android/sdk/ConnectivityBroadcastReceiver.java deleted file mode 100644 index b9ce5d4..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityBroadcastReceiver.java +++ /dev/null @@ -1,63 +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.support.annotation.AnyThread; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; - -/** - * A {@link ConnectivityManager} broadcast receiver that relays network connectivity changes. - * - * @author Kyle Dodson - * @since 1.0.0 - */ -final class ConnectivityBroadcastReceiver extends BroadcastReceiver { - /** - * The {@link ConnectivityListener} to notify on network connectivity changes. - */ - @NonNull - private final ConnectivityListener mConnectivityListener; - - /** - * Creates a new {@link ConnectivityBroadcastReceiver} instance. - * - * @param connectivityListener The {@link ConnectivityListener} to notify on network - * connectivity changes. - */ - @AnyThread - ConnectivityBroadcastReceiver(@NonNull final ConnectivityListener connectivityListener) { - this.mConnectivityListener = connectivityListener; - } - - /** - * Notifies the listener of connectivity changes. - * - * @param context The application context or an activity context. - */ - @MainThread - void notify(@NonNull final Context context) { - final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager == null) { - return; - } - - if (ConnectivityManagerUtilities.isConnected(connectivityManager)) { - this.mConnectivityListener.onConnected(); - } else { - this.mConnectivityListener.onDisconnected(); - } - } - - @MainThread - @Override - public void onReceive(Context context, Intent intent) { - if (context == null) { - throw new IllegalArgumentException("context must not be null"); - } - - this.notify(context); - } -} diff --git a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityListener.java b/zapic/src/main/java/com/zapic/android/sdk/ConnectivityListener.java deleted file mode 100644 index 5cd8634..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zapic.android.sdk; - -import android.support.annotation.MainThread; - -/** - * Represents a network connectivity listener. - * - * @author Kyle Dodson - * @since 1.0.0 - */ -interface ConnectivityListener { - /** - * Called when the application gains network connectivity. - *
- * 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 deleted file mode 100644 index 6906625..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/ConnectivityManagerUtilities.java +++ /dev/null @@ -1,73 +0,0 @@ -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/LoadingPageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java deleted file mode 100644 index 2df44f6..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/LoadingPageFragment.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.zapic.android.sdk; - -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 presents a loading page. - *
- * Use the {@link #createInstance()} factory method to create instances of this fragment. - *
- * 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. - */ - @Nullable - private InteractionListener mListener; - - /** - * Creates a new {@link LoadingPageFragment} instance. - */ - @MainThread - public LoadingPageFragment() { - } - - /** - * Creates a new {@link LoadingPageFragment} instance. - * - * @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; - } 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); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - InteractionListener listener = LoadingPageFragment.this.mListener; - if (listener != null) { - listener.close(); - } - } - }); - } -} diff --git a/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java b/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java deleted file mode 100644 index c112997..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/OfflinePageFragment.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.zapic.android.sdk; - -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 presents an offline page. - *
- * Use the {@link #createInstance()} factory method to create instances of this fragment. - *
- * 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. - */ - @Nullable - private InteractionListener mListener; - - /** - * Creates a new {@link OfflinePageFragment} instance. - */ - @MainThread - public OfflinePageFragment() { - } - - /** - * Creates a new {@link OfflinePageFragment} instance. - * - * @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; - } 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); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - InteractionListener listener = OfflinePageFragment.this.mListener; - if (listener != null) { - listener.close(); - } - } - }); - } -} diff --git a/zapic/src/main/java/com/zapic/android/sdk/WatchdogTimer.java b/zapic/src/main/java/com/zapic/android/sdk/WatchdogTimer.java deleted file mode 100644 index a2e1533..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/WatchdogTimer.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.zapic.android.sdk; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -/** - * A watchdog timer that handles timeout events on a background thread. - * - * @author Kyle Dodson - * @since 1.0.0 - */ -final class WatchdogTimer { - /** - * The tag used to identify log entries. - */ - @NonNull - private static final String TAG = "WatchdogTimer"; - - /** - * The background thread name. - */ - @NonNull - private static final String THREAD_NAME = "ZapicWatchdogTimerThread"; - - /** - * The callback that handles timeout events on the background thread when the watchdog timer has - * elapsed. - */ - @NonNull - private final WatchdogTimer.Callback callback; - - /** - * The handler that sends timeout events on the background thread when the watchdog timer has - * elapsed. - */ - @Nullable - private Handler handler; - - /** - * The background thread. - */ - @Nullable - private HandlerThread handlerThread; - - /** - * The synchronization lock for {@see handler}, {@see handlerThread}, and {@see messageId}. - */ - @NonNull - private final Object lock; - - /** - * The message sequence identifier. - */ - private int messageSequenceId; - - /** - * Creates a new instance. - * - * @param callback The callback that handles timeout events on the background thread when the - * watchdog timer has elapsed. - */ - WatchdogTimer(@NonNull final WatchdogTimer.Callback callback) { - this.callback = callback; - this.handler = null; - this.handlerThread = null; - this.lock = new Object(); - this.messageSequenceId = 0; - } - - /** - * Starts (or resets) the watchdog timer. - * - * @param timeout The timeout (in milliseconds). - */ - void start(final long timeout) { - synchronized (this.lock) { - if (this.handlerThread == null) { - this.handlerThread = new HandlerThread(WatchdogTimer.THREAD_NAME); - this.handlerThread.start(); - } - - if (this.handler == null) { - this.handler = new Handler(this.handlerThread.getLooper(), new Handler.Callback() { - @Override - public boolean handleMessage(Message msg) { - boolean elapsed; - synchronized (WatchdogTimer.this.lock) { - elapsed = msg.arg1 == WatchdogTimer.this.messageSequenceId; - } - - if (elapsed) { - Log.d(TAG, "Firing timeout event; the watchdog timer has elapsed"); - WatchdogTimer.this.callback.handleTimeout(); - } - - return true; - } - }); - } - - if (this.messageSequenceId == Integer.MAX_VALUE) { - this.messageSequenceId = 0; - } - - this.handler.removeCallbacksAndMessages(null); - this.handler.sendMessageDelayed(this.handler.obtainMessage(0, ++this.messageSequenceId, 0), timeout); - } - } - - /** - * Stops the watchdog timer. - */ - void stop() { - synchronized (this.lock) { - if (this.handlerThread != null) { - this.handlerThread.quit(); - this.handlerThread = null; - } - - if (this.handler != null) { - this.handler = null; - } - - this.messageSequenceId = 0; - } - } - - /** - * A callback interface. - */ - interface Callback { - /** - * Handles timeout events on the background thread when the watchdog timer has elapsed. - */ - void handleTimeout(); - } -} diff --git a/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java b/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java deleted file mode 100644 index 19e53c5..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/ZapicActivity.java +++ /dev/null @@ -1,309 +0,0 @@ -package com.zapic.android.sdk; - -import android.app.Activity; -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.webkit.WebView; - -/** - * An {@link Activity} that presents the Zapic JavaScript application in the foreground. - *
- * 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 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 - LoadingPageFragment.InteractionListener, - OfflinePageFragment.InteractionListener { - /** - * The {@link Intent} parameter that identifies the Zapic JavaScript application page to open. - */ - @NonNull - private static final String PAGE_PARAM = "page"; - - /** - * The tag used to identify log messages. - */ - @NonNull - private static final String TAG = "ZapicActivity"; - - /** - * The {@link WebView}. - */ - @Nullable - private WebView mWebView; - - /** - * Creates a new {@link ZapicActivity} instance - */ - public ZapicActivity() { - this.mWebView = null; - } - - /** - * Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the default Zapic - * JavaScript application page. - * - * @param gameActivity The game's activity. - * @return The {@link Intent}. - */ - @MainThread - @CheckResult - @NonNull - public static Intent createIntent(@NonNull final Activity gameActivity) { - return ZapicActivity.createIntent(gameActivity, "default"); - } - - /** - * Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the specified Zapic - * JavaScript application page. - * - * @param gameActivity The game's activity. - * @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); -// } - - /** - * Gets the {@link WebView}. - * - * @return The {@link WebView}. - */ - @CheckResult - @MainThread - @Nullable - WebView getWebView() { - return this.mWebView; - } - - @MainThread - @Override - protected void onCreate(final Bundle savedInstanceState) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onCreate"); - } - - super.onCreate(savedInstanceState); - -// TODO: Determine if the following is required when we use a fullscreen theme. -// // Hide the system status and navigation bars. -// this.enableImmersiveFullScreenMode(); - - 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); - } - - // 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 - protected void onNewIntent(final Intent intent) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onNewIntent"); - } - - super.onNewIntent(intent); - this.setIntent(intent); - } - -// 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(); -// } -// } - - /** - * 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 - 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 - 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(); - } - } - - //region Lifecycle Events - - // onCreate - - @MainThread - @Override - protected void onStart() { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onStart"); - } - - super.onStart(); - } - - @MainThread - @Override - protected void onResume() { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onResume"); - } - - super.onResume(); - } - - @MainThread - @Override - protected void onPause() { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onPause"); - } - - super.onPause(); - } - - @MainThread - @Override - protected void onStop() { - if (BuildConfig.DEBUG) { - Log.d(TAG, "onStop"); - } - - super.onStop(); - } - - @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 deleted file mode 100644 index 0350115..0000000 --- a/zapic/src/main/java/com/zapic/android/sdk/ZapicFragment.java +++ /dev/null @@ -1,736 +0,0 @@ -package com.zapic.android.sdk; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Context; -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; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; -import android.view.LayoutInflater; -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 JavaScript application in the background. - *
- * 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 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
+ * 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(TAG, "Failed to parse serialized action", e);
+ }
+
+ return;
+ }
+
+ String type;
+ try {
+ type = action.getString("type");
+ } catch (JSONException e) {
+ if (BuildConfig.DEBUG) {
+ Log.e(TAG, "The action does not have a type", e);
+ }
+
+ return;
+ }
+
+ switch (type) {
+ case "APP_LOADED":
+ this.onAppLoadedDispatched();
+ break;
+ case "APP_STARTED":
+ this.onAppStartedDispatched();
+ break;
+ case "CLOSE_PAGE_REQUESTED":
+ this.onClosePageRequestedDispatched();
+ break;
+ case "LOGGED_IN":
+ this.onLoggedInDispatched(action);
+ break;
+ case "LOGGED_OUT":
+ this.onLoggedOutDispatched();
+ break;
+ case "LOGIN":
+ this.onLoginDispatched();
+ break;
+ case "LOGOUT":
+ this.onLogoutDispatched();
+ break;
+ case "PAGE_READY":
+ this.onPageReadyDispatched();
+ break;
+ case "SHOW_BANNER":
+ this.onShowBannerDispatched(action);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @WorkerThread
+ private void onAppLoadedDispatched() {
+ this.mHandler.obtainMessage(APP_LOADED).sendToTarget();
+ }
+
+ @MainThread
+ private void onAppLoadedHandled() {
+ WebViewManager.getInstance().setLoaded();
+ }
+
+ @WorkerThread
+ private void onAppStartedDispatched() {
+ this.mHandler.obtainMessage(APP_STARTED).sendToTarget();
+ }
+
+ @MainThread
+ private void onAppStartedHandled() {
+ final WebViewManager webViewManager = WebViewManager.getInstance();
+ webViewManager.setStarted();
+
+ final ZapicActivity activity = webViewManager.getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ final WebView webView = webViewManager.getWebView();
+ webView.evaluateJavascript("window.zapic.dispatch({ type: 'OPEN_PAGE', payload: '" + activity.getPageParameter() + "' })", null);
+ }
+
+ @WorkerThread
+ private void onClosePageRequestedDispatched() {
+ this.mHandler.obtainMessage(CLOSE_PAGE_REQUESTED).sendToTarget();
+ }
+
+ @MainThread
+ private void onClosePageRequestedHandled() {
+ WebViewManager.getInstance().finishActivity();
+ }
+
+ @WorkerThread
+ private void onLoggedInDispatched(@NonNull final JSONObject action) {
+ String userId;
+ try {
+ final JSONObject payload = action.getJSONObject("payload");
+ userId = payload.getString("userId");
+ } catch (JSONException ignored) {
+ // TODO: Send an error to the JavaScript application.
+ return;
+ }
+
+ if (userId.equals("")) {
+ // TODO: Send an error to the JavaScript application.
+ return;
+ }
+
+ Zapic.setPlayerId(userId);
+ }
+
+ @WorkerThread
+ private void onLoggedOutDispatched() {
+ Zapic.setPlayerId(null);
+ }
+
+ @WorkerThread
+ private void onLoginDispatched() {
+ this.mHandler.obtainMessage(LOGIN).sendToTarget();
+ }
+
+ @MainThread
+ private void onLoginHandled() {
+ WebViewManager.getInstance().login();
+ }
+
+ @WorkerThread
+ private void onLogoutDispatched() {
+ this.mHandler.obtainMessage(LOGOUT).sendToTarget();
+ }
+
+ @MainThread
+ private void onLogoutHandled() {
+ WebViewManager.getInstance().logout();
+ }
+
+ @WorkerThread
+ private void onPageReadyDispatched() {
+ this.mHandler.obtainMessage(PAGE_READY).sendToTarget();
+ }
+
+ @MainThread
+ private void onPageReadyHandled() {
+ WebViewManager.getInstance().showAppFragment();
+ }
+
+ @WorkerThread
+ private void onShowBannerDispatched(@NonNull final JSONObject action) {
+ String title;
+ String subtitle;
+ String encodedIcon;
+ try {
+ final JSONObject payload = action.getJSONObject("payload");
+ title = payload.getString("title");
+ subtitle = payload.optString("subtitle");
+ encodedIcon = payload.optString("icon");
+ } catch (JSONException e) {
+ // TODO: Send an error to the JavaScript application.
+ return;
+ }
+
+ if (title.equals("")) {
+ // TODO: Send an error to the JavaScript application.
+ return;
+ }
+
+ if (subtitle.equals("")) {
+ subtitle = null;
+ }
+
+ Bitmap icon;
+ if (encodedIcon.equals("")) {
+ icon = null;
+ } else {
+ try {
+ byte[] imageBytes = Base64.decode(encodedIcon, Base64.DEFAULT);
+ icon = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ } catch (IllegalArgumentException e) {
+ if (BuildConfig.DEBUG) {
+ Log.e(TAG, "Failed to parse icon", e);
+ }
+
+ icon = null;
+ }
+
+ if (icon != null) {
+ final int size = this.mContext.getResources().getDimensionPixelSize(R.dimen.component_zapic_toast_icon_size);
+ icon = Bitmap.createScaledBitmap(icon, size, size, false);
+ }
+ }
+
+ Map
+ * A strong reference to the {@link WebViewManager} instance is kept by {@link ZapicActivity}
+ * and {@link ZapicFragment}.
+ */
+ @NonNull
+ private static WeakReference
* 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
@@ -76,7 +76,7 @@ public static void attachFragment(@Nullable final Activity gameActivity) {
final FragmentManager manager = gameActivity.getFragmentManager();
final Fragment fragment = manager.findFragmentByTag(Zapic.FRAGMENT_TAG);
if (fragment == null) {
- manager.beginTransaction().add(ZapicFragment.createInstance(), Zapic.FRAGMENT_TAG).commit();
+ manager.beginTransaction().add(new ZapicFragment(), Zapic.FRAGMENT_TAG).commit();
}
}
@@ -103,21 +103,6 @@ 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.
*
@@ -205,7 +190,7 @@ public static void show(@Nullable final Activity gameActivity, @SuppressWarnings
* {@link #attachFragment(Activity)}; if {@code parameters} is
* not a valid JSON object.
*/
- @MainThread
+ @AnyThread
@SuppressWarnings({"UnusedDeclaration", "WeakerAccess"}) // documented as public API
public static void submitEvent(@Nullable final Activity gameActivity, @Nullable final String parameters) {
if (gameActivity == null) {
@@ -216,16 +201,6 @@ public static void submitEvent(@Nullable final Activity gameActivity, @Nullable
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);
@@ -240,6 +215,6 @@ public static void submitEvent(@Nullable final Activity gameActivity, @Nullable
return;
}
- app.submitEvent(gameplayEvent);
+ WebViewManager.getInstance().submitEvent(gameplayEvent);
}
}
diff --git a/zapic/src/main/java/com/zapic/sdk/android/ZapicActivity.java b/zapic/src/main/java/com/zapic/sdk/android/ZapicActivity.java
new file mode 100644
index 0000000..25726a7
--- /dev/null
+++ b/zapic/src/main/java/com/zapic/sdk/android/ZapicActivity.java
@@ -0,0 +1,519 @@
+package com.zapic.sdk.android;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.support.annotation.CheckResult;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
+import android.transition.Slide;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnSystemUiVisibilityChangeListener;
+import android.widget.Toast;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class ZapicActivity extends FragmentActivity {
+ /**
+ * Identifies an action request to capture an image from the device camera.
+ */
+ private static final int IMAGE_CAMERA_REQUEST = 1000;
+
+ /**
+ * Identifies a permission request to capture an image from the device camera.
+ */
+ private static final int IMAGE_CAMERA_PERMISSION_REQUEST = 1001;
+
+ /**
+ * Identifies an action request to import an image from the media library.
+ */
+ private static final int IMAGE_LIBRARY_REQUEST = 1002;
+
+ /**
+ * Identifies a permission request to import an image from the media library.
+ */
+ private static final int IMAGE_LIBRARY_PERMISSION_REQUEST = 1003;
+
+ /**
+ * The {@link Intent} parameter that identifies the Zapic JavaScript application page to open.
+ */
+ @NonNull
+ private static final String PAGE_PARAMETER = "page";
+
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "ZapicActivity";
+
+ /**
+ * The file to use to capture an image from the device camera.
+ */
+ @Nullable
+ private File mImageCameraFile;
+
+ /**
+ * A strong reference to the {@link WebViewManager} instance.
+ */
+ @Nullable
+ private WebViewManager mWebViewManager;
+
+ /**
+ * Creates a new {@link ZapicActivity} instance.
+ */
+ public ZapicActivity() {
+ this.mImageCameraFile = null;
+ this.mWebViewManager = null;
+ }
+
+ /**
+ * Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the default Zapic
+ * JavaScript application page.
+ *
+ * @param gameActivity The game's activity.
+ * @return The {@link Intent}.
+ */
+ @MainThread
+ @CheckResult
+ @NonNull
+ public static Intent createIntent(@NonNull final Activity gameActivity) {
+ return ZapicActivity.createIntent(gameActivity, "default");
+ }
+
+ /**
+ * Creates an {@link Intent} that starts a {@link ZapicActivity} and opens the specified Zapic
+ * JavaScript application page.
+ *
+ * @param gameActivity The game's activity.
+ * @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(PAGE_PARAMETER, page);
+ return intent;
+ }
+
+ /**
+ * 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.
+ */
+ 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);
+ }
+
+ @NonNull
+ String getPageParameter() {
+ final Bundle parameters = this.getIntent().getExtras();
+ if (parameters == null) {
+ return "default";
+ }
+
+ final String page = parameters.getString(PAGE_PARAMETER);
+ if (page == null || page.equals("")) {
+ return "default";
+ }
+
+ return page;
+ }
+
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode, @NonNull final Intent data) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onActivityResult");
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode) {
+ case IMAGE_CAMERA_REQUEST: {
+ final Uri[] files = resultCode == RESULT_OK && this.mImageCameraFile != null
+ ? new Uri[]{FileProvider.getUriForFile(this.getApplicationContext(), this.getApplicationContext().getPackageName() + ".zapic", this.mImageCameraFile)}
+ : null;
+ if (files == null) {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+ } else {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.submitImageUpload(files);
+ }
+
+ this.mImageCameraFile = null;
+ break;
+ }
+ case IMAGE_LIBRARY_REQUEST: {
+ final Uri[] files = resultCode == RESULT_OK && data.getData() != null
+ ? new Uri[]{data.getData()}
+ : null;
+ if (files == null) {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+ } else {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.submitImageUpload(files);
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @Override
+ protected void onCreate(@Nullable final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreate");
+ }
+
+ this.enableImmersiveFullScreenMode();
+ this.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(final int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ ZapicActivity.this.enableImmersiveFullScreenMode();
+ }
+ }
+ });
+
+ super.onCreate(savedInstanceState);
+ this.setContentView(R.layout.activity_zapic);
+ if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ final Slide slide = new Slide();
+ slide.setDuration(150);
+ this.getWindow().setEnterTransition(slide);
+ }
+
+ Zapic.attachFragment(this);
+
+ assert this.mWebViewManager == null : "mWebViewManager is not null";
+ this.mWebViewManager = WebViewManager.getInstance();
+ this.mWebViewManager.onActivityCreated(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onDestroy");
+ }
+
+ super.onDestroy();
+
+ Zapic.detachFragment(this);
+
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.onActivityDestroyed(this);
+ this.mWebViewManager = null;
+ }
+
+ @Override
+ protected void onNewIntent(@NonNull final Intent intent) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onNewIntent");
+ }
+
+ super.onNewIntent(intent);
+ this.setIntent(intent);
+ }
+
+ @Override
+ protected void onPause() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onPause");
+ }
+
+ super.onPause();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onRequestPermissionsResult");
+ }
+
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
+ case IMAGE_CAMERA_PERMISSION_REQUEST:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ this.startImageCameraActivity();
+ } else {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+ }
+
+ break;
+ case IMAGE_LIBRARY_PERMISSION_REQUEST:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ this.startImageLibraryActivity();
+ } else {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+ }
+
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ protected void onRestart() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onRestart");
+ }
+
+ super.onRestart();
+ }
+
+ @Override
+ protected void onResume() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onResume");
+ }
+
+ this.enableImmersiveFullScreenMode();
+ this.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(final int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ ZapicActivity.this.enableImmersiveFullScreenMode();
+ }
+ }
+ });
+
+ super.onResume();
+ }
+
+ @Override
+ protected void onStart() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onStart");
+ }
+
+ super.onStart();
+
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.onActivityStarted();
+ }
+
+ @Override
+ protected void onStop() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onStop");
+ }
+
+ super.onStop();
+
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.onActivityStopped();
+ }
+
+ @Override
+ public void onWindowFocusChanged(final boolean hasFocus) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onWindowFocusChanged");
+ }
+
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ this.enableImmersiveFullScreenMode();
+ }
+ }
+
+ private void showImagePermissionPrompt(final int permission) {
+ int message;
+ switch (permission) {
+ case IMAGE_CAMERA_PERMISSION_REQUEST:
+ message = R.string.zapic_activity_image_permission_camera_message;
+ break;
+ case IMAGE_LIBRARY_PERMISSION_REQUEST:
+ message = R.string.zapic_activity_image_permission_library_message;
+ break;
+ default:
+ return;
+ }
+
+ final DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(@NonNull final DialogInterface dialog, final int which) {
+ if (which == AlertDialog.BUTTON_POSITIVE) {
+ ZapicActivity.this.startSettingsActivity();
+ }
+
+ dialog.dismiss();
+ }
+ };
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.zapic_activity_image_permission_title)
+ .setMessage(message)
+ .setPositiveButton(R.string.zapic_activity_image_permission_submit, dialogClickListener)
+ .create()
+ .show();
+ }
+
+ void showImagePrompt() {
+ final AtomicBoolean handled = new AtomicBoolean(false);
+
+ final DialogInterface.OnDismissListener dialogDismissListener = new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(@NonNull final DialogInterface dialog) {
+ if (handled.getAndSet(true)) {
+ return;
+ }
+
+ assert ZapicActivity.this.mWebViewManager != null : "mWebViewManager is null";
+ ZapicActivity.this.mWebViewManager.cancelImageUpload();
+ }
+ };
+
+ final DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(@NonNull final DialogInterface dialog, final int which) {
+ if (handled.getAndSet(true)) {
+ return;
+ }
+
+ switch (which) {
+ case 0: {
+ // Check if the camera permission is declared in the manifest.
+ boolean hasDeclaredCameraPermission = false;
+ try {
+ final PackageInfo packageInfo = ZapicActivity.this.getApplicationContext().getPackageManager().getPackageInfo(ZapicActivity.this.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS);
+ final String[] permissions = packageInfo.requestedPermissions;
+ if (permissions != null) {
+ for (String permission : permissions) {
+ if (permission.equals(Manifest.permission.CAMERA)) {
+ hasDeclaredCameraPermission = true;
+ break;
+ }
+ }
+ }
+ } catch (NameNotFoundException ignored) {
+ }
+
+ // The camera permission is automatically granted if the camera permission is *not*
+ // declared in the manifest.
+ if (!hasDeclaredCameraPermission || ContextCompat.checkSelfPermission(ZapicActivity.this.getApplicationContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ ZapicActivity.this.startImageCameraActivity();
+ } else if (ActivityCompat.shouldShowRequestPermissionRationale(ZapicActivity.this, Manifest.permission.CAMERA)) {
+ assert ZapicActivity.this.mWebViewManager != null : "mWebViewManager is null";
+ ZapicActivity.this.mWebViewManager.cancelImageUpload();
+ ZapicActivity.this.showImagePermissionPrompt(IMAGE_CAMERA_PERMISSION_REQUEST);
+ } else {
+ ActivityCompat.requestPermissions(ZapicActivity.this, new String[]{Manifest.permission.CAMERA}, IMAGE_CAMERA_PERMISSION_REQUEST);
+ }
+
+ break;
+ }
+ case 1:
+ if (ContextCompat.checkSelfPermission(ZapicActivity.this.getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+ ZapicActivity.this.startImageLibraryActivity();
+ } else if (ActivityCompat.shouldShowRequestPermissionRationale(ZapicActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ assert ZapicActivity.this.mWebViewManager != null : "mWebViewManager is null";
+ ZapicActivity.this.mWebViewManager.cancelImageUpload();
+ ZapicActivity.this.showImagePermissionPrompt(IMAGE_LIBRARY_PERMISSION_REQUEST);
+ } else {
+ ActivityCompat.requestPermissions(ZapicActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, IMAGE_LIBRARY_PERMISSION_REQUEST);
+ }
+
+ break;
+ default:
+ assert ZapicActivity.this.mWebViewManager != null : "mWebViewManager is null";
+ ZapicActivity.this.mWebViewManager.cancelImageUpload();
+ break;
+ }
+
+ dialog.dismiss();
+ }
+ };
+
+ new AlertDialog
+ .Builder(this)
+ .setTitle(R.string.zapic_activity_image_source_title)
+ .setItems(R.array.zapic_activity_image_source_items, dialogClickListener)
+ .setNegativeButton(R.string.zapic_activity_image_source_cancel, dialogClickListener)
+ .setOnDismissListener(dialogDismissListener)
+ .create()
+ .show();
+ }
+
+ private void startImageCameraActivity() {
+ final File filesDir = this.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ if (filesDir == null) {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+
+ Toast.makeText(this.getApplicationContext(), R.string.zapic_activity_folder_error, Toast.LENGTH_SHORT).show();
+ } else {
+ this.mImageCameraFile = new File(filesDir.getAbsolutePath() + File.separator + "IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()) + ".jpg");
+ final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(this.getApplicationContext(), this.getApplicationContext().getPackageName() + ".zapic", this.mImageCameraFile));
+ try {
+ this.startActivityForResult(intent, IMAGE_CAMERA_REQUEST);
+ } catch (ActivityNotFoundException e) {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+
+ Toast.makeText(this.getApplicationContext(), R.string.zapic_activity_camera_error, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private void startImageLibraryActivity() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("image/*");
+ try {
+ this.startActivityForResult(intent, IMAGE_LIBRARY_REQUEST);
+ } catch (ActivityNotFoundException e) {
+ assert this.mWebViewManager != null : "mWebViewManager is null";
+ this.mWebViewManager.cancelImageUpload();
+
+ Toast.makeText(this.getApplicationContext(), R.string.zapic_activity_library_error, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void startSettingsActivity() {
+ final Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", this.getApplicationContext().getPackageName(), null));
+ try {
+ this.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this.getApplicationContext(), R.string.zapic_activity_settings_error, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/zapic/src/main/java/com/zapic/sdk/android/ZapicAppFragment.java b/zapic/src/main/java/com/zapic/sdk/android/ZapicAppFragment.java
new file mode 100644
index 0000000..16031c4
--- /dev/null
+++ b/zapic/src/main/java/com/zapic/sdk/android/ZapicAppFragment.java
@@ -0,0 +1,83 @@
+package com.zapic.sdk.android;
+
+import android.content.MutableContextWrapper;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.FrameLayout;
+
+public final class ZapicAppFragment extends Fragment {
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "ZapicAppFragment";
+
+ /**
+ * The {@link WebView}.
+ */
+ @Nullable
+ private WebView mWebView;
+
+ /**
+ * Creates a new {@link ZapicAppFragment} instance.
+ */
+ public ZapicAppFragment() {
+ this.mWebView = null;
+ }
+
+ @Override
+ public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreateView");
+ }
+
+ final WebViewManager webViewManager = WebViewManager.getInstance();
+
+ assert this.mWebView == null : "mWebView is not null";
+ this.mWebView = webViewManager.getWebView();
+ final FrameLayout.LayoutParams webViewLayoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+ this.mWebView.setLayoutParams(webViewLayoutParams);
+
+ final FragmentActivity activity = this.getActivity();
+ assert activity != null : "activity is null";
+
+ final MutableContextWrapper webViewContext = (MutableContextWrapper) this.mWebView.getContext();
+ webViewContext.setBaseContext(activity);
+
+ final View view = inflater.inflate(R.layout.fragment_zapic_app, container, false);
+
+ final FrameLayout frameLayout = view.findViewById(R.id.fragment_zapic_app_container);
+ frameLayout.addView(this.mWebView);
+
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onCreateView");
+ }
+
+ assert this.mWebView != null : "mWebView is null";
+ final FrameLayout frameLayout = (FrameLayout) this.mWebView.getParent();
+ frameLayout.removeView(this.mWebView);
+
+ final FragmentActivity activity = this.getActivity();
+ assert activity != null : "activity is null";
+
+ final MutableContextWrapper webViewContext = (MutableContextWrapper) this.mWebView.getContext();
+ webViewContext.setBaseContext(activity.getApplicationContext());
+
+ this.mWebView = null;
+
+ super.onDestroyView();
+ }
+}
diff --git a/zapic/src/main/java/com/zapic/sdk/android/ZapicFragment.java b/zapic/src/main/java/com/zapic/sdk/android/ZapicFragment.java
new file mode 100644
index 0000000..03128ba
--- /dev/null
+++ b/zapic/src/main/java/com/zapic/sdk/android/ZapicFragment.java
@@ -0,0 +1,284 @@
+package com.zapic.sdk.android;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+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.util.Log;
+
+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;
+
+public final class ZapicFragment extends Fragment {
+ /**
+ * Identifies an action request to sign-in using Google Play Games Services.
+ */
+ private static final int GOOGLE_SIGN_IN_REQUEST = 1000;
+
+ /**
+ * The tag used to identify log messages.
+ */
+ @NonNull
+ private static final String TAG = "ZapicFragment";
+
+ /**
+ * The Google Sign-In client.
+ */
+ @Nullable
+ private GoogleSignInClient mGoogleSignInClient;
+
+ /**
+ * A value that indicates whether the Zapic JavaScript application has requested to login using
+ * the Google Sign-In client.
+ */
+ private boolean mGoogleSignInRequested;
+
+ /**
+ * A strong reference to the {@link WebViewManager} instance.
+ */
+ @Nullable
+ private WebViewManager mWebViewManager;
+
+ /**
+ * Creates a new {@link ZapicFragment} instance.
+ */
+ public ZapicFragment() {
+ this.mGoogleSignInClient = null;
+ this.mGoogleSignInRequested = false;
+ this.mWebViewManager = null;
+ }
+
+ /**
+ * Creates the Google Sign-In client.
+ *
+ * @param activity The activity to which the Google Sign-In client is bound.
+ * @return The Google Sign-In client.
+ * @throws RuntimeException If the Android application's manifest does not have the APP_ID and
+ * WEB_CLIENT_ID meta-data tags.
+ */
+ @CheckResult
+ @MainThread
+ @NonNull
+ private static GoogleSignInClient createGoogleSignInClient(@NonNull final Activity activity) {
+ final String packageName = activity.getPackageName();
+ Bundle metaData;
+ try {
+ final ApplicationInfo info = activity.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_META_DATA);
+ metaData = info.metaData;
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException("Using Zapic requires a metadata tag with the name \"com.google.android.gms.games.APP_ID\" in the application tag of the manifest for " + packageName);
+ }
+
+ String appId = metaData.getString("com.google.android.gms.games.APP_ID");
+ if (appId != null) {
+ appId = appId.trim();
+ if (appId.length() == 0) {
+ appId = null;
+ }
+ }
+
+ if (appId == null) {
+ throw new RuntimeException("Using Zapic requires a metadata tag with the name \"com.google.android.gms.games.APP_ID\" in the application tag of the manifest for " + packageName);
+ }
+
+ String webClientId = metaData.getString("com.google.android.gms.games.WEB_CLIENT_ID");
+ if (webClientId != null) {
+ webClientId = webClientId.trim();
+ if (webClientId.length() == 0) {
+ webClientId = null;
+ }
+ }
+
+ if (webClientId == null) {
+ throw new RuntimeException("Using Zapic requires a metadata tag with the name \"com.google.android.gms.games.WEB_CLIENT_ID\" in the application tag of the manifest for " + packageName);
+ }
+
+ GoogleSignInOptions options = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
+ .requestServerAuthCode(webClientId)
+ .build();
+ return GoogleSignIn.getClient(activity, options);
+ }
+
+ void login() {
+ this.mGoogleSignInRequested = true;
+
+ assert this.mGoogleSignInClient != null : "mGoogleSignInClient is null";
+ this.startActivityForResult(this.mGoogleSignInClient.getSignInIntent(), GOOGLE_SIGN_IN_REQUEST);
+ }
+
+ void logout() {
+ this.mGoogleSignInRequested = false;
+
+ assert this.mGoogleSignInClient != null : "mGoogleSignInClient is null";
+ this.mGoogleSignInClient.signOut();
+ }
+
+ @Override
+ public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent intent) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onActivityResult");
+ }
+
+ if (requestCode == GOOGLE_SIGN_IN_REQUEST) {
+ this.mGoogleSignInRequested = false;
+
+ Task