org.rm3l.ddwrt.tiles.status.wireless.WirelessClientsTile.java Source code

Java tutorial

Introduction

Here is the source code for org.rm3l.ddwrt.tiles.status.wireless.WirelessClientsTile.java

Source

/*
 * DD-WRT Companion is a mobile app that lets you connect to,
 * monitor and manage your DD-WRT routers on the go.
 *
 * Copyright (C) 2014  Armel Soro
 *
 * This program 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.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contact Info: Armel Soro <apps+ddwrt@rm3l.org>
 */

package org.rm3l.ddwrt.tiles.status.wireless;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.widget.CardView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;

import com.actionbarsherlock.app.SherlockFragment;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Ordering;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.rm3l.ddwrt.R;
import org.rm3l.ddwrt.exceptions.DDWRTNoDataException;
import org.rm3l.ddwrt.exceptions.DDWRTTileAutoRefreshNotAllowedException;
import org.rm3l.ddwrt.resources.ClientDevices;
import org.rm3l.ddwrt.resources.Device;
import org.rm3l.ddwrt.resources.conn.Router;
import org.rm3l.ddwrt.tiles.DDWRTTile;
import org.rm3l.ddwrt.utils.DDWRTCompanionConstants;
import org.rm3l.ddwrt.utils.SSHUtils;
import org.rm3l.ddwrt.utils.Utils;
import org.rm3l.ddwrt.utils.WoLUtils;

import java.net.InetAddress;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import static com.google.common.base.Strings.nullToEmpty;
import static org.rm3l.ddwrt.utils.Utils.getThemeBackgroundColor;
import static org.rm3l.ddwrt.utils.Utils.isThemeLight;

/**
 *
 */
public class WirelessClientsTile extends DDWRTTile<ClientDevices> {

    public static final Comparator<Device> COMPARATOR = new Comparator<Device>() {
        @Override
        public int compare(Device device, Device device2) {
            return Ordering.natural().compare(device.getName(), device2.getName());
        }
    };
    private static final String LOG_TAG = WirelessClientsTile.class.getSimpleName();
    private static final int MAX_CLIENTS_TO_SHOW_IN_TILE = 99;

    private String mBroadcastAddress;
    private String mCurrentIpAddress;

    //Generate a random string, to use as discriminator for determining dhcp clients
    private final String MAP_KEYWORD;

    public WirelessClientsTile(@NotNull SherlockFragment parentFragment, @NotNull Bundle arguments, Router router) {
        super(parentFragment, arguments, router, R.layout.tile_status_wireless_clients,
                R.id.tile_status_wireless_clients_togglebutton);
        MAP_KEYWORD = WirelessClientsTile.class.getSimpleName() + UUID.randomUUID().toString();
    }

    @Override
    public int getTileTitleViewId() {
        return R.id.tile_status_wireless_clients_title;
    }

    @Nullable
    @Override
    protected Loader<ClientDevices> getLoader(int id, Bundle args) {
        return new AsyncTaskLoader<ClientDevices>(this.mParentFragmentActivity) {

            @Nullable
            @Override
            public ClientDevices loadInBackground() {

                Log.d(LOG_TAG, "Init background loader for " + WirelessClientsTile.class + ": routerInfo=" + mRouter
                        + " / this.mAutoRefreshToggle= " + mAutoRefreshToggle + " / nbRunsLoader=" + nbRunsLoader);

                //Determine broadcast address at each run (because that might change if connected to another network)
                try {
                    final WifiManager wifiManager = (WifiManager) mParentFragmentActivity
                            .getSystemService(Context.WIFI_SERVICE);

                    mCurrentIpAddress = Utils.intToIp(wifiManager.getConnectionInfo().getIpAddress());

                    final InetAddress broadcastAddress = Utils.getBroadcastAddress(wifiManager);
                    if (broadcastAddress != null) {
                        mBroadcastAddress = broadcastAddress.getHostAddress();
                    }
                } catch (@NotNull final Exception e) {
                    e.printStackTrace();
                    //No worries
                }

                if (nbRunsLoader > 0 && !mAutoRefreshToggle) {
                    //Skip run
                    Log.d(LOG_TAG, "Skip loader run");
                    return new ClientDevices().setException(new DDWRTTileAutoRefreshNotAllowedException());
                }
                nbRunsLoader++;

                final ClientDevices devices = new ClientDevices();

                if (DDWRTCompanionConstants.TEST_MODE) {
                    //FIXME TEST MODE
                    for (int i = 1, j = i + 1; i <= 15; i++, j++) {
                        final int randomI = new Random().nextInt(i);
                        final int randomJ = new Random().nextInt(j);
                        devices.addDevice(
                                new Device(String.format("A%1$s:B%1$s:C%1$s:D%2$s:E%2$s:F%2$s", randomI, randomJ))
                                        .setIpAddress(String.format("172.17.1%1$s.2%2$s", randomI, randomJ))
                                        .setSystemName(String.format("Device %1$s-%2$s", randomI, randomJ)));
                    }
                    Log.d(LOG_TAG, "wireless client devices: " + devices);
                    return devices;
                    //FIXME END TEST MODE
                }

                try {
                    @Nullable
                    final String[] output = SSHUtils.getManualProperty(mRouter, mGlobalPreferences,
                            "grep dhcp-host /tmp/dnsmasq.conf | sed 's/.*=//' | awk -F , '{print \"" + MAP_KEYWORD
                                    + "\",$1,$3 ,$2}'",
                            "awk '{print \"" + MAP_KEYWORD + "\",$2,$3,$4}' /tmp/dnsmasq.leases",
                            "awk 'NR>1{print \"" + MAP_KEYWORD + "\",$4,$1,\"*\"}' /proc/net/arp", "echo done");

                    Log.d(LOG_TAG, "output: " + (output == null ? "NULL" : Arrays.toString(output)));

                    if (output == null) {
                        return null;
                    }

                    for (final String stdoutLine : output) {
                        if ("done".equals(stdoutLine)) {
                            break;
                        }
                        final List<String> as = Splitter.on(" ").splitToList(stdoutLine);
                        if (as != null && as.size() >= 4 && MAP_KEYWORD.equals(as.get(0))) {
                            final String macAddress = as.get(1);
                            if ("00:00:00:00:00:00".equals(macAddress)) {
                                //Skip clients with incomplete ARP set-up
                                continue;
                            }
                            final Device device = new Device(macAddress);
                            device.setIpAddress(as.get(2));

                            final String systemName = as.get(3);
                            if (!"*".equals(systemName)) {
                                device.setSystemName(systemName);
                            }
                            devices.addDevice(device);
                        }

                    }

                    return devices;

                } catch (@NotNull final Exception e) {
                    Log.e(LOG_TAG, e.getMessage() + ": " + Throwables.getStackTraceAsString(e));
                    return new ClientDevices().setException(e);
                }
            }
        };
    }

    @Nullable
    @Override
    protected String getLogTag() {
        return LOG_TAG;
    }

    /**
     * Called when a previously created loader has finished its load.  Note
     * that normally an application is <em>not</em> allowed to commit fragment
     * transactions while in this call, since it can happen after an
     * activity's state is saved.  See {@link android.support.v4.app.FragmentManager#beginTransaction()
     * FragmentManager.openTransaction()} for further discussion on this.
     * <p/>
     * <p>This function is guaranteed to be called prior to the release of
     * the last data that was supplied for this Loader.  At this point
     * you should remove all use of the old data (since it will be released
     * soon), but should not do your own release of the data since its Loader
     * owns it and will take care of that.  The Loader will take care of
     * management of its data so you don't have to.  In particular:
     * <p/>
     * <ul>
     * <li> <p>The Loader will monitor for changes to the data, and report
     * them to you through new calls here.  You should not monitor the
     * data yourself.  For example, if the data is a {@link android.database.Cursor}
     * and you place it in a {@link android.widget.CursorAdapter}, use
     * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context,
     * android.database.Cursor, int)} constructor <em>without</em> passing
     * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY}
     * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER}
     * (that is, use 0 for the flags argument).  This prevents the CursorAdapter
     * from doing its own observing of the Cursor, which is not needed since
     * when a change happens you will get a new Cursor throw another call
     * here.
     * <li> The Loader will release the data once it knows the application
     * is no longer using it.  For example, if the data is
     * a {@link android.database.Cursor} from a {@link android.content.CursorLoader},
     * you should not call close() on it yourself.  If the Cursor is being placed in a
     * {@link android.widget.CursorAdapter}, you should use the
     * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)}
     * method so that the old Cursor is not closed.
     * </ul>
     *
     * @param loader The Loader that has finished.
     * @param data   The data generated by the Loader.
     */
    @Override
    public void onLoadFinished(Loader<ClientDevices> loader, ClientDevices data) {
        Log.d(LOG_TAG, "onLoadFinished: loader=" + loader + " / data=" + data);

        layout.findViewById(R.id.tile_status_wireless_clients_loading_view).setVisibility(View.GONE);
        layout.findViewById(R.id.tile_status_wireless_clients_layout_list_container).setVisibility(View.VISIBLE);
        layout.findViewById(R.id.tile_status_wireless_clients_togglebutton_container).setVisibility(View.VISIBLE);

        if (data == null || (data.getDevices().isEmpty()
                && !(data.getException() instanceof DDWRTTileAutoRefreshNotAllowedException))) {
            data = new ClientDevices().setException(new DDWRTNoDataException("No Data!"));
        }

        @NotNull
        final TextView errorPlaceHolderView = (TextView) this.layout
                .findViewById(R.id.tile_status_wireless_clients_error);

        @Nullable
        final Exception exception = data.getException();

        if (!(exception instanceof DDWRTTileAutoRefreshNotAllowedException)) {

            if (exception == null) {
                errorPlaceHolderView.setVisibility(View.GONE);
            }

            final GridLayout clientsContainer = (GridLayout) this.layout
                    .findViewById(R.id.tile_status_wireless_clients_layout_list_container);
            clientsContainer.removeAllViews();
            clientsContainer.setBackgroundColor(
                    mParentFragmentActivity.getResources().getColor(android.R.color.transparent));

            final Set<Device> devices = data.getDevices(MAX_CLIENTS_TO_SHOW_IN_TILE);
            final int themeBackgroundColor = getThemeBackgroundColor(mParentFragmentActivity, mRouter.getUuid());
            final boolean isThemeLight = isThemeLight(mParentFragmentActivity, mRouter.getUuid());
            for (final Device device : devices) {

                final CardView cardView = (CardView) mParentFragmentActivity.getLayoutInflater()
                        .inflate(R.layout.tile_status_wireless_client, null);

                //Create Options Menu
                final ImageButton tileMenu = (ImageButton) cardView
                        .findViewById(R.id.tile_status_wireless_client_device_menu);

                if (!isThemeLight) {
                    //Set menu background to white
                    tileMenu.setImageResource(R.drawable.abs__ic_menu_moreoverflow_normal_holo_dark);
                }

                cardView.setCardBackgroundColor(themeBackgroundColor);

                //Add padding to CardView on v20 and before to prevent intersections between the Card content and rounded corners.
                cardView.setPreventCornerOverlap(true);
                //Add padding in API v21+ as well to have the same measurements with previous versions.
                cardView.setUseCompatPadding(true);

                final TextView deviceName = (TextView) cardView
                        .findViewById(R.id.tile_status_wireless_client_device_name);
                final String name = device.getName();
                deviceName.setText(name);

                final TextView deviceMac = (TextView) cardView
                        .findViewById(R.id.tile_status_wireless_client_device_mac);
                final String macAddress = device.getMacAddress();
                deviceMac.setText(macAddress);

                final TextView deviceIp = (TextView) cardView
                        .findViewById(R.id.tile_status_wireless_client_device_ip);
                final String ipAddress = device.getIpAddress();
                final boolean isThisDevice = (ipAddress != null && ipAddress.equals(mCurrentIpAddress));
                deviceIp.setText(ipAddress);
                if (isThisDevice) {
                    final View thisDevice = cardView.findViewById(R.id.tile_status_wireless_client_device_this);
                    if (isThemeLight) {
                        //Set text color to blue
                        ((TextView) thisDevice)
                                .setTextColor(mParentFragmentActivity.getResources().getColor(R.color.blue));
                    }
                    thisDevice.setVisibility(View.VISIBLE);
                }

                cardView.setOnClickListener(new DeviceOnClickListener(device));

                clientsContainer.addView(cardView);

                tileMenu.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        final PopupMenu popup = new PopupMenu(mParentFragmentActivity, v);
                        popup.setOnMenuItemClickListener(new DeviceOnMenuItemClickListener(device));
                        final MenuInflater inflater = popup.getMenuInflater();

                        final Menu menu = popup.getMenu();

                        inflater.inflate(R.menu.tile_status_wireless_client_options, menu);

                        if (isThisDevice) {
                            //WOL not needed as this is the current device
                            menu.findItem(R.id.tile_status_wireless_client_wol).setEnabled(false);
                        }

                        popup.show();
                    }
                });
            }

            final Button showMore = (Button) this.layout.findViewById(R.id.tile_status_wireless_clients_show_more);
            //Whether to display 'Show more' button
            if (data.getDevicesCount() > MAX_CLIENTS_TO_SHOW_IN_TILE) {
                showMore.setVisibility(View.VISIBLE);
                showMore.setOnClickListener(this);
            } else {
                showMore.setVisibility(View.GONE);
            }

        }

        if (exception != null && !(exception instanceof DDWRTTileAutoRefreshNotAllowedException)) {
            //noinspection ThrowableResultOfMethodCallIgnored
            final Throwable rootCause = Throwables.getRootCause(exception);
            errorPlaceHolderView.setText("Error: " + (rootCause != null ? rootCause.getMessage() : "null"));
            final Context parentContext = this.mParentFragmentActivity;
            errorPlaceHolderView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(final View v) {
                    //noinspection ThrowableResultOfMethodCallIgnored
                    if (rootCause != null) {
                        Toast.makeText(parentContext, rootCause.getMessage(), Toast.LENGTH_LONG).show();
                    }
                }
            });
            errorPlaceHolderView.setVisibility(View.VISIBLE);
        }

        doneWithLoaderInstance(this, loader, R.id.tile_status_wireless_clients_togglebutton_title,
                R.id.tile_status_wireless_clients_togglebutton_separator);

        Log.d(LOG_TAG, "onLoadFinished(): done loading!");
    }

    @Nullable
    @Override
    protected OnClickIntent getOnclickIntent() {
        //TODO
        return null;
    }

    private class DeviceOnMenuItemClickListener implements PopupMenu.OnMenuItemClickListener {

        @NotNull
        private final Device device;

        private DeviceOnMenuItemClickListener(@NotNull final Device device) {
            this.device = device;
        }

        @Override
        public boolean onMenuItemClick(MenuItem item) {
            switch (item.getItemId()) {
            case R.id.tile_status_wireless_client_wol:
                final String deviceName = nullToEmpty(device.getName());
                final String macAddress = device.getMacAddress();
                new AlertDialog.Builder(mParentFragmentActivity).setIcon(R.drawable.ic_action_alert_warning)
                        .setTitle(String.format("Wake up %s (%s)", deviceName, macAddress))
                        .setMessage(String.format("This lets you turn on a computer via the network.\n"
                                + "For this to work properly:\n"
                                + "- '%s' (%s) must support Wake-on-LAN. You can enable it in the BIOS or in the Operating System Settings.\n"
                                + "- '%s' (%s) and this mobile device must be on the same network. To wake over the Internet, "
                                + "you must forward packets from any port you want to the computer you wish to wake.",
                                deviceName, macAddress, deviceName, macAddress))
                        .setCancelable(true)
                        .setPositiveButton("Send Magic Packet!", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(final DialogInterface dialogInterface, final int i) {
                                //Send Magic Packet
                                new WoLUtils.SendWoLMagicPacketAsyncTask(mParentFragmentActivity, device)
                                        .execute(macAddress, mBroadcastAddress);
                            }
                        }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                //Cancelled - nothing more to do!
                            }
                        }).create().show();
                return true;
            default:
                break;
            }
            return false;
        }
    }

    private class DeviceOnClickListener implements View.OnClickListener {

        @NotNull
        private final Device device;

        private DeviceOnClickListener(@NotNull final Device device) {
            this.device = device;
        }

        @Override
        public void onClick(View v) {
            //Open WebView with MAC OUI Search: http://standards.ieee.org/cgi-bin/ouisearch?f0-b4-79
            final String macAddress = device.getMacAddress();
            if (Strings.isNullOrEmpty(macAddress)) {
                return;
            }

            final String manufacturerPart = macAddress.toUpperCase().substring(0, 8).replaceAll(":", "-");

            final WebView webView = new WebView(mParentFragmentActivity);
            webView.loadUrl(String.format("%s%s", DDWRTCompanionConstants.MAC_OUI_SEARCH_URL, manufacturerPart));
            final AlertDialog.Builder dialog = new AlertDialog.Builder(mParentFragmentActivity);
            dialog.setView(webView);
            dialog.setPositiveButton("Close", null);
            dialog.setTitle("OUI Lookup Tool");
            dialog.show();
        }
    }

}