org.comixwall.pffw.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.comixwall.pffw.MainActivity.java

Source

/*
 * Copyright (C) 2017-2018 Soner Tari
 *
 * This file is part of PFFW.
 *
 * PFFW is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PFFW 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PFFW.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.comixwall.pffw;

import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import org.json.JSONObject;

import java.util.HashMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

    public static Cache cache;

    public static Controller controller;
    private static boolean boundToController = false;

    // Firebase token handling
    public static String token = "";
    public static String user = "Unknown user";
    public static final String product = Build.PRODUCT;
    public static boolean sendToken = false;
    public static boolean deleteToken = false;
    private static final int PERMISSION_REQUEST_CODE = 1;
    private static final int PICK_CONTACT_REQUEST = 2;

    /**
     * Used to get the constructor of and instantiate the fragment class referred by the menu item selected.
     */
    private HashMap<Integer, Class> mMenuItems2Fragments;
    public static Fragment fragment = new Fragment();

    public LogFilePickerDialog logFilePickerDialog;

    public ActionBarDrawerToggle toggle;
    public DrawerLayout drawer;

    private NavigationView navigationView;

    public static final Logger logger;

    private Menu optionsMenu;
    public static int lastNotificationTimestamp = 0;

    static {
        logger = Logger.getLogger("org.comixwall.PFFW");

        Level mLogLevel = Level.ALL;
        logger.setLevel(mLogLevel);

        // TODO: setUseParentHandlers() Does not seem to have any effect
        //logger.setUseParentHandlers(true);

        if (logger.getHandlers().length == 0) {
            Handler ch = new ConsoleHandler();
            ch.setLevel(mLogLevel);
            logger.addHandler(ch);
        }

        System.out.println("Created logger with level: " + logger.getLevel());
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // TODO: Check why main activity is recreated on rotation
        logger.finest("MainActivity onCreate()");

        mMenuItems2Fragments = new HashMap<Integer, Class>() {
            {
                // ATTENTION: Dashboard fragment should never be instantiated using this map
                put(R.id.menuDashboard, Dashboard.class);
                put(R.id.menuNotifications, Notifications.class);
                put(R.id.menuInfoPf, InfoPf.class);
                put(R.id.menuInfoSystem, InfoSystem.class);
                put(R.id.menuInfoHosts, InfoHosts.class);
                put(R.id.menuInfoIfs, InfoIfs.class);
                put(R.id.menuInfoRules, InfoRules.class);
                put(R.id.menuInfoStates, InfoStates.class);
                put(R.id.menuInfoQueues, InfoQueues.class);
                put(R.id.menuStatsGeneral, StatsGeneral.class);
                put(R.id.menuStatsDaily, StatsDaily.class);
                put(R.id.menuStatsHourly, StatsHourly.class);
                put(R.id.menuStatsLive, StatsLive.class);
                put(R.id.menuGraphsInterfaces, GraphsIfs.class);
                put(R.id.menuGraphsTransfer, GraphsTransfer.class);
                put(R.id.menuGraphsStates, GraphsStates.class);
                put(R.id.menuGraphsMbufs, GraphsMbufs.class);
                put(R.id.menuLogsArchives, LogsArchives.class);
                put(R.id.menuLogsLive, LogsLive.class);
            }
        };

        FragmentManager fm = getSupportFragmentManager();

        // ATTENTION: Always load cache from the FragmentManager.
        // Otherwise, the cache survives even after the application is closed by the last Back button press
        cache = (Cache) fm.findFragmentByTag("cache");
        if (cache == null) {
            // create the fragment and data the first time
            cache = new Cache();
            fm.beginTransaction().add(cache, "cache").commit();
        }

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        drawer = findViewById(R.id.drawerLayout);

        toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open,
                R.string.navigation_drawer_close);
        drawer.addDrawerListener(toggle);
        toggle.syncState();

        navigationView = findViewById(R.id.navView);
        navigationView.setNavigationItemSelectedListener(this);

        logFilePickerDialog = new LogFilePickerDialog();

        int result = ContextCompat.checkSelfPermission(this, Manifest.permission.GET_ACCOUNTS);
        if (result == PackageManager.PERMISSION_GRANTED) {
            setUser();
        } else {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.GET_ACCOUNTS)) {
                Toast.makeText(this, R.string.account_perms, Toast.LENGTH_LONG).show();
            }
            ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.GET_ACCOUNTS },
                    PERMISSION_REQUEST_CODE);
        }

        if (user.equals("Unknown user")) {
            // In Android 8.0 (API level 26), apps can no longer get access to user accounts unless the
            // authenticator owns the accounts or the user grants that access. The GET_ACCOUNTS permission
            // is no longer sufficient. To be granted access to an account, apps should either use
            // AccountManager.newChooseAccountIntent() or an authenticator-specific method. After getting
            // access to accounts, an app can call AccountManager.getAccounts() to access them.
            // @see https://developer.android.com/about/versions/oreo/android-8.0-changes.html#aaad
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                // TODO: Use new String[]{"com.google"} for allowableAccountTypes instead?
                Intent intent = AccountManager.newChooseAccountIntent(null, null, null, null, null, null, null);
                startActivityForResult(intent, PICK_CONTACT_REQUEST);

            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
            @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                setUser();
                Toast.makeText(this, user + ", " + product, Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, R.string.account_perms, Toast.LENGTH_LONG).show();
            }
        }
    }

    private void setUser() {
        // TODO: Use getAccountsByType("com.google") instead?
        Account[] accounts = AccountManager.get(this).getAccounts();

        // Use the first entry, if any
        if (accounts.length > 0) {
            user = accounts[0].name;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == PICK_CONTACT_REQUEST) {
            if (resultCode == RESULT_OK) {
                // The user picked a contact.
                // The Intent's data Uri identifies which contact was selected.
                Bundle bundle = data.getExtras();
                if (bundle != null && bundle.containsKey(KEY_ACCOUNT_NAME)) {
                    user = data.getExtras().getString(KEY_ACCOUNT_NAME);
                    Toast.makeText(this, user + ", " + product, Toast.LENGTH_LONG).show();
                }
            } else {
                Toast.makeText(this, R.string.account_perms, Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        if (controller == null || !controller.isLoggedin()) {
            // ATTENTION: Login fragment should be inflated only after SSH session is created first
            // onResume() is executed after onRestoreInstanceState().
            FragmentManager fm = getSupportFragmentManager();
            android.support.v4.app.FragmentTransaction transaction = fm.beginTransaction();

            fragment = new Login();
            // ATTENTION: Do not add but replace, because login page may be rotated, which brings us here again.
            //transaction.add(R.id.fragmentContainer, fragment, "MainFragment");
            transaction.replace(R.id.fragmentContainer, fragment);
            transaction.commit();
        } else {
            showFirstFragment();
        }
    }

    public void showFirstFragment() {
        NavigationView navigationView = findViewById(R.id.navView);

        Intent intent = getIntent();
        Bundle bundle = intent.getExtras();

        if (bundle != null && bundle.containsKey("title") && bundle.containsKey("body")
                && bundle.containsKey("data")) {
            // If there is a notification bundle, show the notification fragment
            try {
                JSONObject data = new JSONObject(bundle.getString("data"));

                // ATTENTION: Timestamp check is a workaround for the case the user clicks the Overview button
                // If the activity was created with a notification intent while the app was in the background,
                // closing the app and then pressing the Overview button recreates the activity with the same intent,
                // hence we reach here and add the same notification one more time.
                // Timestamp is a unique notification id to prevent such mistakes
                // TODO: Find a way to fix this Overview button issue
                int timestamp = Integer.parseInt(data.getString("timestamp"));
                if (lastNotificationTimestamp < timestamp) {
                    lastNotificationTimestamp = timestamp;

                    Notifications.addNotification(Notification.newInstance(data));

                    // Remove one of the extras, so we don't add the same notification again
                    intent.removeExtra("title");
                    setIntent(new Intent());
                } else {
                    Toast.makeText(this, R.string.notification_not_recent, Toast.LENGTH_LONG).show();
                    logger.finest("showFirstFragment will not process the same or older notification: "
                            + lastNotificationTimestamp);
                }
            } catch (Exception e) {
                logger.warning("showFirstFragment Exception= " + e.getMessage());
            }
            // Reset the fragment, so onNavigationItemSelected() displays the Notifications fragment in any case
            fragment = new Fragment();
            onNavigationItemSelected(navigationView.getMenu().findItem((R.id.menuNotifications)));
        } else {
            // Avoid blank pages by showing Dashboard fragment if the backstack is empty
            if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
                // Reset the fragment, so onNavigationItemSelected() displays the Dashboard fragment in any case
                fragment = new Fragment();
                onNavigationItemSelected(navigationView.getMenu().findItem(R.id.menuDashboard));
            }
        }

        createOptionsMenu();
    }

    @Override
    public void onBackPressed() {
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();

            if (controller != null && controller.isLoggedin()) {
                // Avoid blank pages by showing Dashboard fragment if the backstack is empty
                // ATTENTION: This may show Dashboard for a brief period of time, if the super.onBackPressed()
                // call above is going to close the activity after returning from this function
                if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
                    // Reset the fragment, so onNavigationItemSelected() displays the Dashboard fragment in any case
                    fragment = new Fragment();
                    onNavigationItemSelected(navigationView.getMenu().findItem(R.id.menuDashboard));
                }
            }
        }

        createOptionsMenu();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        optionsMenu = menu;
        createOptionsMenu();
        return true;
    }

    public void createOptionsMenu() {
        if (optionsMenu != null) {
            optionsMenu.clear();
            if (fragment.getClass() == Notifications.class) {
                getMenuInflater().inflate(R.menu.app_bar_notifications_menu, optionsMenu);
            } else if (fragment.getClass() != Login.class) {
                // Login fragment should not have any appbar menu
                getMenuInflater().inflate(R.menu.app_bar_menu, optionsMenu);
            }
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle time bar item clicks here.
        int id = item.getItemId();

        if (id == R.id.menuRefresh) {
            // Refresh requests are handled by fragments
            return fragment.onOptionsItemSelected(item);
        } else if (id == R.id.menuLogout) {
            logger.finest("onOptionsItemSelected recreate()");

            // TODO: Do we need to reset the backstack if we are going to recreate the activity next?
            popAllBackStack();

            // Raise the deleteToken flag and refresh the fragment, so its resume method runs delToken() in Controller.execute()
            deleteToken = true;
            try {
                // Throws exception to stop further command execution after deleting token
                // ATTENTION: All fragments should have the Refresh menu item for this workaround to work
                fragment.onOptionsItemSelected(optionsMenu.findItem(R.id.menuRefresh));
            } catch (Exception ignored) {
            }

            controller.logout();

            // We recreate the activity so that onCreate() resets everything and
            // onResume() displays the Login fragment.
            recreate();
        } else if (id == R.id.menuDelete) {
            return fragment.onOptionsItemSelected(item);
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * Select the fragment to display.
     * We modify the backstack ourselves so that no fragment is pushed to the backstack twice.
     * So if a fragment which is already in the backstack is selected, we roll back the backstack
     * to its position.
     * <p>
     * We never push the Dashboard fragment to the backstack because it is always
     * the first fragment displayed (if we push it to the backstack too, pressing the back button
     * while Dashboard fragment is displayed causes a blank activity screen).
     *
     * @param item The menu item selected.
     * @return See {@link NavigationView.OnNavigationItemSelectedListener}
     */
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        // Ignore requests for the same fragment already displayed
        if (!mMenuItems2Fragments.get(id).isInstance(fragment)) {
            FragmentManager fm = getSupportFragmentManager();

            boolean add = true;

            if (id == R.id.menuDashboard) {
                // Dashboard is the main fragment, should never be removed,
                // so remove all backstack entries first to reach the first Dashboard.
                popAllBackStack();

                // Never add Dashboard to the backstack
                add = false;
                fragment = new Dashboard();

                // ATTENTION: menuDashboard does not check initially, so we need to manage it ourselves
                item.setChecked(true);
            } else {
                // TODO: Check why android:checkableBehavior="single" does not uncheck menuDashboard
                MenuItem itemDashboard = navigationView.getMenu().findItem(R.id.menuDashboard);
                if (itemDashboard.isChecked()) {
                    itemDashboard.setChecked(false);
                }

                try {
                    fragment = (Fragment) mMenuItems2Fragments.get(id).getConstructor().newInstance();
                } catch (Exception e) {
                    e.printStackTrace();
                    logger.severe("EXCEPTION: " + e.toString());
                    return false;
                }
            }

            String fragmentName = fragment.getClass().getSimpleName();

            // Rolls back the backstack if the fragment is already in
            if (!fm.popBackStackImmediate(fragmentName, 0)) {
                android.support.v4.app.FragmentTransaction transaction = fm.beginTransaction();
                transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

                if (add) {
                    transaction.addToBackStack(fragmentName);
                }

                // TODO: Check if we need to pass any args
                //fragment.setArguments(getIntent().getExtras());
                transaction.replace(R.id.fragmentContainer, fragment);

                transaction.commit();
            }
        } else {
            logger.finest("onNavigationItemSelected will not show the same fragment");
        }

        drawer.closeDrawer(GravityCompat.START);
        createOptionsMenu();
        return true;
    }

    private void popAllBackStack() {
        // TODO: What is the best way to pop all backstack items?
        // It does NOT help to use a special StackBottom name while adding the first Dashboard to the backstack,
        // then we could pop all items simply by:
        // fm.popBackStackImmediate("StackBottom", FragmentManager.POP_BACK_STACK_INCLUSIVE);
        // But if we add the first Dashboard to the backstack, we get a blank page when the user presses the back button.

        FragmentManager fm = getSupportFragmentManager();
        while (fm.getBackStackEntryCount() > 0) {
            fm.popBackStackImmediate();
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        // Bind to Controller
        Intent intent = new Intent(this, Controller.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    public void onStop() {
        super.onStop();
        // Unbind from the service
        if (boundToController) {
            unbindService(mConnection);
            boundToController = false;
        }
    }

    /**
     * Defines callbacks for service binding, passed to bindService()
     */
    private final ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            // We've bound to Controller, cast the IBinder and get Controller instance
            controller = ((Controller.ControllerBinder) service).getService();
            boundToController = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            boundToController = false;
        }
    };
}