org.catrobat.catroid.cast.CastManager.java Source code

Java tutorial

Introduction

Here is the source code for org.catrobat.catroid.cast.CastManager.java

Source

/*
 * Catroid: An on-device visual programming system for Android devices
 * Copyright (C) 2010-2017 The Catrobat Team
 * (<http://developer.catrobat.org/credits>)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * An additional term exception under section 7 of the GNU Affero
 * General Public License, version 3, is available at
 * http://developer.catrobat.org/license_additional_term
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.catrobat.catroid.cast;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.RelativeLayout;

import com.badlogic.gdx.backends.android.surfaceview.GLSurfaceView20;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.CastRemoteDisplayLocalService;
import com.google.android.gms.common.api.Status;

import org.catrobat.catroid.ProjectManager;
import org.catrobat.catroid.R;
import org.catrobat.catroid.common.Constants;
import org.catrobat.catroid.content.Project;
import org.catrobat.catroid.content.bricks.CameraBrick;
import org.catrobat.catroid.content.bricks.ChooseCameraBrick;
import org.catrobat.catroid.content.bricks.FlashBrick;
import org.catrobat.catroid.formulaeditor.Sensors;
import org.catrobat.catroid.stage.StageActivity;
import org.catrobat.catroid.stage.StageListener;
import org.catrobat.catroid.ui.adapter.CastDevicesAdapter;
import org.catrobat.catroid.ui.dialogs.SelectCastDialog;
import org.catrobat.catroid.utils.ToastUtil;

import java.util.ArrayList;
import java.util.EnumMap;

import static org.catrobat.catroid.common.Constants.CAST_IDLE_BACKGROUND_COLOR;

public final class CastManager {

    private static final CastManager INSTANCE = new CastManager();
    private final ArrayList<MediaRouter.RouteInfo> routeInfos = new ArrayList<MediaRouter.RouteInfo>();
    StageActivity gamepadActivity;
    private EnumMap<Sensors, Boolean> isGamepadButtonPressed = new EnumMap<>(Sensors.class);
    private MediaRouter mediaRouter;
    private MediaRouteSelector mediaRouteSelector;
    private MyMediaRouterCallback callback;
    private ArrayAdapter<MediaRouter.RouteInfo> deviceAdapter;
    private CastDevice selectedDevice;
    private boolean isConnected = false;
    private GLSurfaceView20 stageViewDisplayedOnCast;
    private Activity initializingActivity;
    private RelativeLayout remoteLayout;
    private RelativeLayout pausedView = null;
    private MenuItem castButton;
    private boolean pausedScreenShowing = false;
    private boolean isCastDeviceAvailable;

    public static ArrayList<Class<?>> unsupportedBricks = new ArrayList<Class<?>>() {
        {
            add(CameraBrick.class);
            add(ChooseCameraBrick.class);
            add(FlashBrick.class);
        }
    };

    private CastManager() {
        isGamepadButtonPressed.put(Sensors.GAMEPAD_A_PRESSED, false);
        isGamepadButtonPressed.put(Sensors.GAMEPAD_B_PRESSED, false);
        isGamepadButtonPressed.put(Sensors.GAMEPAD_LEFT_PRESSED, false);
        isGamepadButtonPressed.put(Sensors.GAMEPAD_RIGHT_PRESSED, false);
        isGamepadButtonPressed.put(Sensors.GAMEPAD_UP_PRESSED, false);
        isGamepadButtonPressed.put(Sensors.GAMEPAD_DOWN_PRESSED, false);
    }

    public static CastManager getInstance() {
        return INSTANCE;
    }

    public synchronized void initializeGamepadActivity(StageActivity gamepadActivity) { //TODO needs to be synced?
        this.gamepadActivity = gamepadActivity;
        initGamepadListeners();
    }

    public synchronized void setIsConnected(boolean isConnected) {

        int drawableId = isConnected ? R.drawable.ic_cast_connected_white : R.drawable.ic_cast_white;
        castButton.setIcon(drawableId);
        this.isConnected = isConnected;
        initializingActivity.invalidateOptionsMenu();
    }

    public void startCastButtonAnimation() {
        int drawableId = R.drawable.animation_cast_button_connecting;
        castButton.setIcon(drawableId);
        ((AnimationDrawable) castButton.getIcon()).start();
    }

    public synchronized boolean isConnected() {
        return isConnected;
    }

    public MediaRouter getMediaRouter() {
        return mediaRouter;
    }

    public ArrayList<MediaRouter.RouteInfo> getRouteInfos() {
        return routeInfos;
    }

    public boolean isButtonPressed(Sensors btnSensor) {
        return isGamepadButtonPressed.get(btnSensor);
    }

    public void setButtonPress(Sensors btn, boolean b) {
        isGamepadButtonPressed.put(btn, b);
    }

    public CastDevice getSelectedDevice() {
        return selectedDevice;
    }

    public synchronized void initializeCast(Activity activity) {

        initializingActivity = activity;

        if (mediaRouter != null) {
            return;
        }
        deviceAdapter = new CastDevicesAdapter(activity, R.layout.fragment_cast_device_list_item, routeInfos);
        mediaRouter = MediaRouter.getInstance(activity.getApplicationContext());
        mediaRouteSelector = new MediaRouteSelector.Builder()
                .addControlCategory(CastMediaControlIntent.categoryForCast(Constants.REMOTE_DISPLAY_APP_ID))
                .build();
        setCallback();
    }

    public void addCallback() {
        callback = new MyMediaRouterCallback();
        mediaRouter.addCallback(mediaRouteSelector, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
    }

    public synchronized void setCallback() {
        setCallback(MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
    }

    public synchronized void setCallback(int callbackFlag) {
        if (callback == null) {
            callback = new MyMediaRouterCallback();
        }
        mediaRouter.addCallback(mediaRouteSelector, callback, callbackFlag);
    }

    public void openDeviceSelectorOrDisconnectDialog() {
        openDeviceSelectorOrDisconnectDialog(initializingActivity);
    }

    private void initGamepadListeners() {

        View.OnClickListener pauseButtonListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                gamepadActivity.onBackPressed();
            }
        };

        gamepadActivity.findViewById(R.id.gamepadPauseButton).setOnClickListener(pauseButtonListener);

        View.OnTouchListener otl = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                handleGamepadTouch((ImageButton) v, event);
                return true;
            }
        };

        ImageButton[] gamepadButtons = {

                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonA),
                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonB),
                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonUp),
                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonDown),
                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonLeft),
                (ImageButton) gamepadActivity.findViewById(R.id.gamepadButtonRight) };

        for (ImageButton btn : gamepadButtons) {
            btn.setOnTouchListener(otl);
        }
    }

    private synchronized void handleGamepadTouch(ImageButton button, MotionEvent event) {

        if (event.getAction() != MotionEvent.ACTION_DOWN && event.getAction() != MotionEvent.ACTION_UP) {
            // We only care about the event when a gamepad button is pressed and when a gamepad button is unpressed
            return;
        }

        if (gamepadActivity == null) {
            return;
        }

        boolean isActionDown = (event.getAction() == MotionEvent.ACTION_DOWN);
        String buttonPressedName;

        switch (button.getId()) {
        case R.id.gamepadButtonA:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_A);
            button.setImageResource(
                    isActionDown ? R.drawable.gamepad_button_a_pressed : R.drawable.gamepad_button_a);
            setButtonPress(Sensors.GAMEPAD_A_PRESSED, isActionDown);
            break;
        case R.id.gamepadButtonB:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_B);
            button.setImageResource(
                    isActionDown ? R.drawable.gamepad_button_b_pressed : R.drawable.gamepad_button_b);
            setButtonPress(Sensors.GAMEPAD_B_PRESSED, isActionDown);
            break;
        case R.id.gamepadButtonUp:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_up);
            setButtonPress(Sensors.GAMEPAD_UP_PRESSED, isActionDown);
            break;
        case R.id.gamepadButtonDown:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_down);
            setButtonPress(Sensors.GAMEPAD_DOWN_PRESSED, isActionDown);
            break;
        case R.id.gamepadButtonLeft:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_left);
            setButtonPress(Sensors.GAMEPAD_LEFT_PRESSED, isActionDown);
            break;
        case R.id.gamepadButtonRight:
            buttonPressedName = gamepadActivity.getString(R.string.cast_gamepad_right);
            setButtonPress(Sensors.GAMEPAD_RIGHT_PRESSED, isActionDown);
            break;
        default:
            throw new IllegalArgumentException("Unknown button pressed");
        }

        if (isActionDown) {
            ((StageListener) gamepadActivity.getApplicationListener()).gamepadPressed(buttonPressedName);
            button.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
        }
    }

    public synchronized void addStageViewToLayout(GLSurfaceView20 stageView) {
        stageViewDisplayedOnCast = stageView;
        remoteLayout.setBackgroundColor(ContextCompat.getColor(initializingActivity, android.R.color.white));
        remoteLayout.removeAllViews();
        remoteLayout.addView(stageViewDisplayedOnCast);
        Project project = ProjectManager.getInstance().getCurrentProject();
        stageView.surfaceChanged(stageView.getHolder(), 0, project.getXmlHeader().getVirtualScreenWidth(),
                project.getXmlHeader().getVirtualScreenHeight());
    }

    public synchronized boolean currentlyConnecting() {
        return (!isConnected && selectedDevice != null);
    }

    public synchronized void openDeviceSelectorOrDisconnectDialog(Activity activity) {
        SelectCastDialog dialog = new SelectCastDialog(deviceAdapter, activity);
        dialog.openDialog();
    }

    public synchronized void setCastButton(MenuItem castButton) {
        this.castButton = castButton;
        castButton.setVisible(
                mediaRouter.isRouteAvailable(mediaRouteSelector, MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH));
        setIsConnected(isConnected);
    }

    public void selectRoute(MediaRouter.RouteInfo routeInfo) {
        mediaRouter.selectRoute(routeInfo);
    }

    public synchronized void setRemoteLayout(RelativeLayout remoteLayout) {
        this.remoteLayout = remoteLayout;
    }

    public synchronized void setRemoteLayoutToIdleScreen(Context context) {
        remoteLayout.removeAllViews();
        Drawable drawable = ContextCompat.getDrawable(context, R.drawable.idle_screen_1);
        remoteLayout.setBackground(drawable);
    }

    @SuppressLint("InflateParams")
    public synchronized void setRemoteLayoutToPauseScreen(Context context) {
        if (remoteLayout != null) {
            if ((pausedView == null) && !pausedScreenShowing) {
                pausedView = (RelativeLayout) LayoutInflater.from(context).inflate(R.layout.cast_pause_screen,
                        null);
                remoteLayout.addView(pausedView);
                RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) pausedView
                        .getLayoutParams();
                Project p = ProjectManager.getInstance().getCurrentProject();
                layoutParams.height = p.getXmlHeader().getVirtualScreenHeight();
                layoutParams.width = p.getXmlHeader().getVirtualScreenWidth();
                pausedView.setLayoutParams(layoutParams);
                pausedView.setBackgroundColor(CAST_IDLE_BACKGROUND_COLOR);
                pausedScreenShowing = true;
            }
            pausedView.setVisibility(View.VISIBLE);
            pausedScreenShowing = true;
        }
    }

    public synchronized void resumeRemoteLayoutFromPauseScreen() {
        if (remoteLayout != null && pausedView != null) {
            pausedView.setVisibility(View.GONE);
            pausedScreenShowing = false;
        }
    }

    public synchronized void onStageDestroyed() {
        if (isConnected) {
            setRemoteLayoutToIdleScreen(initializingActivity);
        }
        stageViewDisplayedOnCast = null;
        pausedView = null;
        pausedScreenShowing = false;
    }

    private class MyMediaRouterCallback extends MediaRouter.Callback {

        private long lastConnectionTry;

        @Override
        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
            // Add route to list of discovered routes
            synchronized (this) {
                for (int i = 0; i < routeInfos.size(); i++) {
                    MediaRouter.RouteInfo routeInfo = routeInfos.get(i);
                    if (routeInfo.equals(info)) {
                        routeInfos.remove(i);
                    }
                }
                routeInfos.add(info);
                castButton.setVisible(mediaRouter.isRouteAvailable(mediaRouteSelector,
                        MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH));
                deviceAdapter.notifyDataSetChanged();
            }
        }

        @Override
        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
            // Remove route from list of routes
            synchronized (this) {
                for (int i = 0; i < routeInfos.size(); i++) {
                    MediaRouter.RouteInfo routeInfo = routeInfos.get(i);
                    if (routeInfo.equals(info)) {
                        routeInfos.remove(i);
                        if (routeInfos.size() == 0) {
                            castButton.setVisible(mediaRouter.isRouteAvailable(mediaRouteSelector,
                                    MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH));
                        }
                        deviceAdapter.notifyDataSetChanged();
                    }
                }
            }
        }

        @Override
        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
            synchronized (this) {
                selectedDevice = CastDevice.getFromBundle(info.getExtras());
                startCastService(initializingActivity);
                lastConnectionTry = System.currentTimeMillis();
                // Show a msg if still connecting after CAST_CONNECTION_TIMEOUT milliseconds
                // and abort connection.
                isCastDeviceAvailable = (CastRemoteDisplayLocalService.getInstance() != null)
                        && (System.currentTimeMillis() - lastConnectionTry >= Constants.CAST_CONNECTION_TIMEOUT);
                (new Handler()).postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (this) {
                            if (currentlyConnecting() && isCastDeviceAvailable) {
                                CastRemoteDisplayLocalService.stopService();
                                ToastUtil.showError(initializingActivity,
                                        initializingActivity.getString(R.string.cast_connection_timout_msg));
                            }
                        }
                    }
                }, Constants.CAST_CONNECTION_TIMEOUT);
            }
        }

        @Override
        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
            onCastStop();
        }

        public synchronized void onCastStop() {

            if (stageViewDisplayedOnCast != null) {
                // Meaning that there is currently a stage being displayed on the remote screen
                gamepadActivity.onBackPressed();
            }
            stageViewDisplayedOnCast = null;
            setIsConnected(false);
            selectedDevice = null;
            gamepadActivity = null;
            remoteLayout = null;
            pausedView = null;
            pausedScreenShowing = false;
            CastRemoteDisplayLocalService.stopService();
        }

        public void startCastService(final Activity activity) {

            Intent intent = new Intent(activity, activity.getClass());
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            PendingIntent notificationPendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);

            CastRemoteDisplayLocalService.NotificationSettings settings = new CastRemoteDisplayLocalService.NotificationSettings.Builder()
                    .setNotificationPendingIntent(notificationPendingIntent).build();

            CastRemoteDisplayLocalService.Callbacks callbacks = new CastRemoteDisplayLocalService.Callbacks() {

                @Override
                public void onServiceCreated(CastRemoteDisplayLocalService castRemoteDisplayLocalService) {
                }

                @Override
                public void onRemoteDisplaySessionStarted(CastRemoteDisplayLocalService service) {
                }

                @Override
                public void onRemoteDisplaySessionError(Status errorReason) {
                    onCastStop();
                    activity.finish();
                }
            };

            CastRemoteDisplayLocalService.startService(activity, CastService.class, Constants.REMOTE_DISPLAY_APP_ID,
                    selectedDevice, settings, callbacks);
        }
    }
}