com.amazon.appstream.fireclient.FireClientActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.amazon.appstream.fireclient.FireClientActivity.java

Source

/*
 * Copyright 2013-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Amazon Software License (the "License"). You may not use
 * this file except in compliance with the License. A copy of the License is
 * located at
 *
 *      http://aws.amazon.com/asl/
 *
 * This Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

package com.amazon.appstream.fireclient;

import java.util.List;
import java.util.Locale;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast;

import com.amazon.appstream.AppStreamInterface;
import com.amazon.appstream.DesQuery;
import com.amazon.appstream.HardwareDecoder;
import com.amazon.appstream.JoystickHelper;
import com.amazon.appstream.KeyRemap;

/**
 * The Activity for the AppStream Example Client on Android.
 *
 * After creating the basic OpenGL surface and starting the
 * AppStream server, the main task of FireClientActivity is to
 * siphon up all relevant events and send them to the native
 * layer.
 */
public class FireClientActivity extends FragmentActivity
        implements ConnectDialogFragment.ConnectDialogListener, android.view.GestureDetector.OnGestureListener,
        DesQuery.DesQueryListener, AppStreamInterface.AppStreamListener {

    static {
        System.loadLibrary("stlport_shared");
        System.loadLibrary("avutil");
        System.loadLibrary("avcodec");
        System.loadLibrary("avformat");
        System.loadLibrary("swresample");
        System.loadLibrary("XStxClientLibraryShared");
        System.loadLibrary("appstreamsample");
    }

    private static final String TAG = "FireClientActivity";

    private boolean mStopped = false;

    private GL2JNIView mGlView = null;

    private String mServerAddress = null;
    private String mDESServerAddress = null;
    private boolean mUseAppServer = false;
    private static final String USE_APP_SERVER = "use_app_server";
    private static final String DES_SERVER_ADDRESS = "des_server_address";
    private final String SERVER_ADDRESS = "server_address";
    private final String APP_ID = "appid";
    private final String USER_ID = "userid";

    private boolean mKeyboardActive = false;

    private GestureDetector mGestureDetector = null;
    private int mKeyboardOffset = 0;

    private FrameLayout mActivityRootView;

    private ConnectDialogFragment mConnectDialog = null;
    private boolean mTouchscreenAvailable;

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        Log.i(TAG, "onNewIntent");
    }

    /**
     * Initialization. Sets up the app and spawns the connection
     * dialog.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.v(TAG, "onCreate");
        mGestureDetector = new GestureDetector(this, this);

        mGestureDetector.setIsLongpressEnabled(false);

        mTouchscreenAvailable = getPackageManager().hasSystemFeature("android.hardware.touchscreen");

        SharedPreferences prefs = getSharedPreferences("main", MODE_PRIVATE);
        if (prefs.contains(SERVER_ADDRESS)) {
            mServerAddress = prefs.getString(SERVER_ADDRESS, null);
        }
        if (prefs.contains(DES_SERVER_ADDRESS)) {
            mDESServerAddress = prefs.getString(DES_SERVER_ADDRESS, null);
        }
        if (prefs.contains(USE_APP_SERVER)) {
            mUseAppServer = prefs.getBoolean(USE_APP_SERVER, false);
        }
        if (prefs.contains(APP_ID)) {
            mAppId = prefs.getString(APP_ID, null);
        }
        if (prefs.contains(USER_ID)) {
            mUserId = prefs.getString(USER_ID, null);
        }

        requestWindowFeature(Window.FEATURE_NO_TITLE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.v(TAG, "onStart");

        AppStreamInterface.setListener(this);

        setContentView(R.layout.activity_sample_client);

        attemptEnableHardwareDecode();

        openConnectDialog(null);

    }

    HardwareDecoder mHardwareDecoder = null;

    private void attemptEnableHardwareDecode() {
        if (mHardwareDecoder == null) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                Log.i(TAG, "JellyBean or higher: Using Hardware Decoder");
                try {
                    mHardwareDecoder = new HardwareDecoder("video/avc", 1280, 720);
                } catch (RuntimeException e) {
                    Log.w(TAG, "Never mind. Can't create hardware decoder: " + e.getMessage());
                }
            }
        }
        AppStreamInterface.setHardwareDecoder(mHardwareDecoder);
    }

    private void disableHardwareDecode() {
        AppStreamInterface.setHardwareDecoder(null);
    }

    boolean mFnVisible = false;
    boolean mArrowBarVisible = false;

    Runnable mClearFullscreen = new Runnable() {
        @Override
        public void run() {
            Log.i(TAG, "Clearing Fullscreen");
            if (getWindow() != null) {
                Log.v(TAG, "Clearing/setting window flags");
                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
            }
            mActivityRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
        }
    };

    /**
     * Switch to the "game" view. Sets the content view to the one
     * for your game; in the example, the content view contains only
     * a keyboard icon.
     */
    public void showGame() {
        setContentView(R.layout.game);

        mActivityRootView = (FrameLayout) findViewById(R.id.outer_frame);

        // If we have a touchscreen, then configure all the buttons.
        if (mTouchscreenAvailable) {
            // This little bit of magic is required to detect when the user hides the keyboard.
            // Unfortunately, the setSystemUiVisibility() has a minimum requirement of Honeycomb (3.0/API11).
            // As such, we have raised the minimum API to 11.
            //
            // A workaround that you could try if you need API10 support is to NOT specify a fullscreen
            // theme for the app. The keyboard detection code below doesn't work when the theme is fullscreen.
            // In fact, you could request a non-fullscreen theme on API10, though you may have to also disable
            // requestWindowFeature(Window.FEATURE_NO_TITLE) in onCreate().
            mActivityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
                @SuppressLint("NewApi")
                @Override
                public void onGlobalLayout() {
                    int heightDiff = mActivityRootView.getRootView().getHeight() - mActivityRootView.getHeight();
                    if (heightDiff > 100) { // if more than 100 pixels, its probably a keyboard...
                        Log.v(TAG, "keyboard found");
                        mKeyboardActive = true;
                        mKeyboardOffset = 0;
                    } else if (mKeyboardActive) {
                        Log.v(TAG, "keyboard not found");
                        mKeyboardActive = false;
                        AppStreamInterface.setKeyboardOffset(0);
                        runOnUiThread(mClearFullscreen);
                    }
                }
            });
        }

        // Keep the screen on when we're visible.
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        mGlView = new GL2JNIView(getApplication());
        mActivityRootView.addView(mGlView, 0);
    }

    /**
     * Open (or reopen) the connection dialog. This dialog will
     * collect the server information from the user and then call
     * the onDialogConnectClick() function to handle the result.
     *
     * If the result is an error, then this function is called AGAIN
     * to change the state of the dialog from "waiting" to "there
     * was an error."
     *
     * @param error An optional error string to display as part of
     *              the dialog.
     */
    public void openConnectDialog(String error) {

        if (mConnectDialog != null) {
            mConnectDialog.setAddressError(error);
            mConnectDialog.resetProgress();
            return;
        }

        ConnectDialogFragment dialog = new ConnectDialogFragment();
        dialog.setAddress(mServerAddress);
        dialog.setAppID(mAppId);
        dialog.setUserId(mUserId);
        dialog.setAddressError(error);
        dialog.setDESAddress(mDESServerAddress);
        dialog.setUseAppServer(mUseAppServer);
        if (mHardwareDecoder == null) {
            dialog.disableUseHardware();
        }
        dialog.show(getSupportFragmentManager(), "ConnectDialogFragment");

        mConnectDialog = dialog;
    }

    PointerCoords mCoordHolder = new PointerCoords();

    /**
     * A "touch event" includes mouse motion when the mouse button
     * is down.
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        if (mKeyboardActive) {
            if (mGestureDetector.onTouchEvent(event)) {
                return true;
            }
        }

        if (super.dispatchTouchEvent(event))
            return true;

        int flags = 0;
        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
            flags = AppStreamInterface.CET_TOUCH_FLAG;
        }

        event.getPointerCoords(0, mCoordHolder);
        switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            AppStreamInterface.mouseEvent((int) mCoordHolder.x, (int) mCoordHolder.y, flags);
            break;
        case MotionEvent.ACTION_DOWN:
            AppStreamInterface.mouseEvent((int) mCoordHolder.x, (int) mCoordHolder.y,
                    AppStreamInterface.CET_MOUSE_1_DOWN | flags);
            break;
        case MotionEvent.ACTION_UP:
            AppStreamInterface.mouseEvent((int) mCoordHolder.x, (int) mCoordHolder.y,
                    AppStreamInterface.CET_MOUSE_1_UP | flags);
            break;
        }
        return true;
    }

    boolean mInitializedJoystick = false;
    int mLTrigger = MotionEvent.AXIS_BRAKE;
    int mRTrigger = MotionEvent.AXIS_GAS;

    /**
     * A "generic motion event" includes joystick and mouse motion
     * when the mouse button isn't down. In our simple sample, we're
     * not handling the joystick, but this is where any such code
     * would live.
     *
     * This will only ever be called in HONEYCOMB_MR1 (12) or later, so I'm marking the
     * function using \@TargetApi to allow it to call the super.
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    @Override
    public boolean dispatchGenericMotionEvent(MotionEvent event) {
        if (event.getSource() == InputDevice.SOURCE_MOUSE) {
            event.getPointerCoords(0, mCoordHolder);
            switch (event.getAction()) {
            case MotionEvent.ACTION_HOVER_MOVE:
                AppStreamInterface.mouseEvent((int) mCoordHolder.x, (int) mCoordHolder.y, 0);
                break;

            default:
                return super.dispatchGenericMotionEvent(event);
            }
            return true;
        } else if (event.getSource() == InputDevice.SOURCE_JOYSTICK) {
            Log.v(TAG, "Joystick event:" + event.toString());

            if (!mInitializedJoystick) {
                mInitializedJoystick = true;

                InputDevice joystick = InputDevice.getDevice(event.getDeviceId());
                InputDevice.MotionRange lThumbX = joystick.getMotionRange(MotionEvent.AXIS_X,
                        InputDevice.SOURCE_JOYSTICK);
                InputDevice.MotionRange lThumbY = joystick.getMotionRange(MotionEvent.AXIS_Y,
                        InputDevice.SOURCE_JOYSTICK);
                InputDevice.MotionRange rThumbX = joystick.getMotionRange(MotionEvent.AXIS_Z);
                InputDevice.MotionRange rThumbY = joystick.getMotionRange(MotionEvent.AXIS_RZ);
                InputDevice.MotionRange rTrigger = joystick.getMotionRange(MotionEvent.AXIS_GAS);
                if (rTrigger == null) {
                    rTrigger = joystick.getMotionRange(MotionEvent.AXIS_RTRIGGER);
                    mRTrigger = MotionEvent.AXIS_RTRIGGER;
                }

                InputDevice.MotionRange lTrigger = joystick.getMotionRange(MotionEvent.AXIS_BRAKE);
                if (lTrigger == null) {
                    lTrigger = joystick.getMotionRange(MotionEvent.AXIS_LTRIGGER);
                    mLTrigger = MotionEvent.AXIS_LTRIGGER;
                }

                List<InputDevice.MotionRange> ranges = joystick.getMotionRanges();

                InputDevice.MotionRange dPad = null;
                String name = joystick.getName();

                /*
                The Amazon Fire Game Controller follows the NVidia standard of sending
                AXIS_HAT_X/AXIS_HAT_Y results when the user hits the D-Pad. Only if we
                return false from dispatchGenericMotionEvent() will it then send
                DPAD keycodes.
                    
                But the most popular Android joystick on the market at this time, the
                Nyko Playpad Pro, returns AXIS_HAT_X/AXIS_HAT_Y results when the left
                analog stick hits its extremes, meaning that the analog stick will
                generate DPAD keys if we return false. The Nyko generates DPAD keys
                directly for the DPAD controller.
                    
                So we have two incompatible standards fighting with each other. Probably
                the safest thing to do would be to ask the user to press on their DPAD
                and see what messages we get, but that is beyond the scope of this
                example code.
                */
                if (name.equals("Amazon Fire Game Controler")) {
                    for (int i = 0; i < ranges.size(); ++i) {
                        InputDevice.MotionRange range = ranges.get(i);
                        int axis = range.getAxis();

                        if (axis == MotionEvent.AXIS_HAT_X || axis == MotionEvent.AXIS_HAT_Y) {
                            dPad = ranges.get(i);
                            break;
                        }
                    }
                }

                JoystickHelper.joystickDeadZones(lTrigger, rTrigger, lThumbX, lThumbY, rThumbX, rThumbY, dPad);
            }

            float lThumbX = event.getAxisValue(MotionEvent.AXIS_X);
            float lThumbY = -event.getAxisValue(MotionEvent.AXIS_Y);
            float rThumbX = event.getAxisValue(MotionEvent.AXIS_Z);
            float rThumbY = -event.getAxisValue(MotionEvent.AXIS_RZ);
            float lTrigger = event.getAxisValue(mLTrigger);
            float rTrigger = event.getAxisValue(mRTrigger);
            float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
            float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);

            JoystickHelper.setJoystickState(lTrigger, rTrigger, lThumbX, lThumbY, rThumbX, rThumbY, hatX, hatY);
            return true;
        }

        return super.dispatchGenericMotionEvent(event);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mGlView != null) {
            mGlView.onPause();
        }

        savePrefs();
        AppStreamInterface.pause(true);
    }

    @Override
    protected void onStop() {
        Log.i(TAG, "onStop");
        super.onStop();
        stopAppStream();
    }

    public void stopAppStream() {
        if (!mStopped) {
            mStopped = true;
            AppStreamInterface.stop();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        mStopped = false;
        if (mGlView != null) {
            mGlView.onResume();
        }
        AppStreamInterface.pause(false);
    }

    /**
     * Translate from an onKeyDown() or onKeyUp() message to a
     * Windows virtual key code and send it to the NDK layer.
     *
     * @param[in] msg Message to translate.
     * @param[in] down True if a key down message; false otherwise.
     *
     * @see AppStreamWrapper::keyPress
     */
    public boolean onKey(KeyEvent msg, boolean down) {
        return KeyRemap.handleAndroidKey(msg, down);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent msg) {
        if (onKey(msg, true)) {
            return true; // don't call super; it can translate this key to something else
        }
        // We don't know what this key means, so go ahead and let the OS
        // translate it, in case the translated key *is* something we understand.
        return super.onKeyDown(keyCode, msg);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent msg) {
        if (onKey(msg, false)) {
            return true;
        }
        return super.onKeyUp(keyCode, msg);
    }

    private DesQuery mDesQuery = new DesQuery();

    private String mAppId;

    private String mUserId;

    @Override
    public void onDialogConnectClick(String address, String appid, String userid, boolean hardwareEnabled) {

        Resources r = getResources();

        if (hardwareEnabled) {
            attemptEnableHardwareDecode();
        } else {
            disableHardwareDecode();
        }

        mStopped = false;

        if (address == null) {
            openConnectDialog(r.getString(R.string.invalid_address));
        } else if (appid == null) {
            if (!address.matches("\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}")) {
                openConnectDialog(r.getString(R.string.invalid_address));
                return;
            }

            mServerAddress = address;
            mUseAppServer = true;

            String url = String.format(Locale.US, "ssm://%s:%d?sessionId=%s", address, 80, "9070-0");
            AppStreamInterface.connect(url);
            AppStreamInterface.newFrame();

            savePrefs();
            return;
        } else if (address != null && address.matches(
                "^(http[s]?://)?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}(:[0-9]+)?(/.*)?$")
                || address.matches("^(http[s]?://)?\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}(:[0-9]+)?(/.*)?$")) {
            if (userid == null || (userid.isEmpty())) {
                openConnectDialog(r.getString(R.string.user_id_required));
                return;
            } else if (appid.isEmpty()) {
                openConnectDialog(r.getString(R.string.app_id_required));
                return;
            }
            mUseAppServer = false;
            mDESServerAddress = address;
            mAppId = appid;
            mUserId = userid;
            savePrefs();

            // we've received an entitlement server + path
            mDesQuery.setActivity(this);
            mDesQuery.setListener(this);
            mDesQuery.makeQuery(address, appid, userid);
        } else {
            openConnectDialog(r.getString(R.string.invalid_address));
        }
    }

    private void savePrefs() {
        SharedPreferences prefs = getSharedPreferences("main", MODE_PRIVATE);
        Editor e = prefs.edit();
        if (mServerAddress != null) {
            e.putString(SERVER_ADDRESS, mServerAddress);
        }
        if (mDESServerAddress != null) {
            e.putString(DES_SERVER_ADDRESS, mDESServerAddress);
        }
        if (mUserId != null) {
            e.putString(USER_ID, mUserId);
        }
        if (mAppId != null) {
            e.putString(APP_ID, mAppId);
        }
        e.putBoolean(USE_APP_SERVER, mUseAppServer);
        e.apply();
    }

    @Override
    public void onDesQuerySuccess(String address) {
        AppStreamInterface.connect(address);
        AppStreamInterface.newFrame();
    }

    @Override
    public void onDesQueryFailure(String error) {
        // if we fail, try and try again...
        openConnectDialog(error);
    }

    @Override
    public void onConnectSuccess() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mConnectDialog != null) {
                    mConnectDialog.dismiss();
                    mConnectDialog = null;
                }
                showGame();
            }
        });
    }

    @Override
    public void onErrorMessage(final boolean fatal, final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mStopped) {
                    Log.i(TAG, "Ignoring error during stopped state :" + (fatal ? "fatal" : "non fatal") + ":"
                            + message);
                    return; // ignore errors if we're stopped.
                }
                if (fatal) {
                    // Tell the app it needs to pause.
                    AppStreamInterface.pause(true);
                    ErrorDialogFragment dialog = new ErrorDialogFragment();
                    dialog.setMessage(message);
                    dialog.show(getSupportFragmentManager(), "ErrorDialogFragment");

                    // And finally stop AppStream; kill the interfaces to give us a clean slate.
                    stopAppStream();
                } else {
                    if (mConnectDialog != null) {
                        openConnectDialog(message);
                    } else {
                        Toast toast = Toast.makeText(FireClientActivity.this, message, Toast.LENGTH_LONG);
                        toast.setGravity(Gravity.RIGHT | Gravity.BOTTOM, 10, 10);
                        toast.show();
                    }
                }
            }
        });
    }

    /**
     * Request an OpenGL frame.
     */
    @Override
    public void newFrame() {
        if (mGlView != null) {
            mGlView.requestRender();
        }
    }

    @Override
    public void onReconnecting(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (message != null) {
                    openConnectDialog("");
                    mConnectDialog.setReconnectMessage(message);
                } else {
                    if (mConnectDialog != null) {
                        mConnectDialog.dismiss();
                        mConnectDialog = null;
                    }
                }
            }
        });
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        stopAppStream();
    }

    // GestureDetector interface methods. We only use onScroll, but all
    // must be implemented.
    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (mKeyboardActive) {
            mKeyboardOffset += (int) distanceY;
            mKeyboardOffset = Math.min(Math.max(0, mKeyboardOffset), mGlView.getHeightDelta());
            AppStreamInterface.setKeyboardOffset(mKeyboardOffset);
            return true;
        }
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

}