Java tutorial
/* * 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.services.vpn; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.View; import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.app.SherlockFragment; import com.cocosw.undobar.UndoBarController; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.rm3l.ddwrt.DDWRTMainActivity; import org.rm3l.ddwrt.R; import org.rm3l.ddwrt.actions.RouterAction; import org.rm3l.ddwrt.actions.RouterActionListener; import org.rm3l.ddwrt.actions.SetNVRAMVariablesAction; import org.rm3l.ddwrt.exceptions.DDWRTNoDataException; import org.rm3l.ddwrt.exceptions.DDWRTTileAutoRefreshNotAllowedException; import org.rm3l.ddwrt.mgmt.RouterManagementActivity; import org.rm3l.ddwrt.resources.conn.NVRAMInfo; import org.rm3l.ddwrt.resources.conn.Router; import org.rm3l.ddwrt.tiles.DDWRTTile; import org.rm3l.ddwrt.tiles.status.wireless.WirelessIfaceTile; import org.rm3l.ddwrt.utils.DDWRTCompanionConstants; import org.rm3l.ddwrt.utils.SSHUtils; import org.rm3l.ddwrt.utils.Utils; import java.util.Map; import de.keyboardsurfer.android.widget.crouton.Style; import static com.google.common.base.Strings.isNullOrEmpty; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_ADV; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_AUTH; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_BRIDGE; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_CA; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_CERTTYPE; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_CIPHER; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_CLIENT; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_CONFIG; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_ENABLE; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_FRAGMENT; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_IP; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_KEY; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_LZO; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_MASK; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_MSSFIX; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_MTU; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_NAT; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_PKCS_12; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_PROTO; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_REMOTEIP; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_REMOTEPORT; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_ROUTE; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_SEC; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_STATIC; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_TLSAUTH; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_TLSCIP; import static org.rm3l.ddwrt.resources.conn.NVRAMInfo.OPENVPNCL_TUNTAP; import static org.rm3l.ddwrt.tiles.status.wireless.WirelessIfaceTile.CAT_SYS_CLASS_NET_S_STATISTICS; import static org.rm3l.ddwrt.tiles.status.wireless.WirelessIfaceTile.IfaceStatsType.RX_BYTES; import static org.rm3l.ddwrt.tiles.status.wireless.WirelessIfaceTile.IfaceStatsType.TX_BYTES; import static org.rm3l.ddwrt.utils.Utils.isThemeLight; public class OpenVPNClientTile extends DDWRTTile<NVRAMInfo> implements DDWRTTile.ActivityResultListener, UndoBarController.AdvancedUndoListener, RouterActionListener { private static final String LOG_TAG = OpenVPNClientTile.class.getSimpleName(); public static final String OPENVPNCL__DEV = "___openvpncl__dev"; public static final String OPENVPNCL__DEV_RX_RATE = OPENVPNCL__DEV + "_rx_rate_human_readable"; public static final String OPENVPNCL__DEV_TX_RATE = OPENVPNCL__DEV + "_tx_rate_human_readable"; public static final String OPENVPNCL__DEV_RX_PACKETS = OPENVPNCL__DEV + "_rx_packets"; public static final String OPENVPNCL__DEV_TX_PACKETS = OPENVPNCL__DEV + "_tx_packets"; public static final String N_A = "-"; public static final String OPENVPNCL_NVRAMINFO = "OPENVPNCL_NVRAMINFO"; private NVRAMInfo mNvramInfo; public OpenVPNClientTile(@NotNull SherlockFragment parentFragment, @NotNull Bundle arguments, @Nullable Router router) { super(parentFragment, arguments, router, R.layout.tile_services_openvpn_client, R.id.tile_services_openvpn_client_togglebutton); } @Override public int getTileTitleViewId() { return R.id.tile_services_openvpn_client_title; } @Nullable @Override protected Loader<NVRAMInfo> getLoader(int id, Bundle args) { return new AsyncTaskLoader<NVRAMInfo>(this.mParentFragmentActivity) { @Nullable @Override public NVRAMInfo loadInBackground() { try { Log.d(LOG_TAG, "Init background loader for " + OpenVPNClientTile.class + ": routerInfo=" + mRouter + " / this.mAutoRefreshToggle= " + mAutoRefreshToggle + " / nbRunsLoader=" + nbRunsLoader); if (nbRunsLoader > 0 && !mAutoRefreshToggle) { //Skip run Log.d(LOG_TAG, "Skip loader run"); return new NVRAMInfo().setException(new DDWRTTileAutoRefreshNotAllowedException()); } nbRunsLoader++; mNvramInfo = null; @NotNull final NVRAMInfo nvramInfo = new NVRAMInfo(); NVRAMInfo nvramInfoTmp = null; try { nvramInfoTmp = SSHUtils.getNVRamInfoFromRouter(mRouter, mGlobalPreferences, //Status: {1,0} OPENVPNCL_ENABLE, //Server IP Name OPENVPNCL_REMOTEIP, //Port (from 1 to 65535): default=1194 OPENVPNCL_REMOTEPORT, //Tunnel Device: {"tun", "tap"} OPENVPNCL_TUNTAP, //Tunnel Protocol: { "udp", "tcp-client"} OPENVPNCL_PROTO, //Encryption Cipher: //{aes-512-cbc, aes-256-cbc, aes-192-cbc, aes-128-cbc, bf-cbc, none} OPENVPNCL_CIPHER, //Hash Algorithm //{sha512, sha256, sha1, md5, md4, none} OPENVPNCL_AUTH, //TLS Cipher //{TLS-DHE-RSA-WITH-AES-256-GCM-SHA384, //TLS-DHE-RSA-WITH-AES-256-CBC-SHA256, //TLS-DHE-RSA-WITH-AES-128-CBC-SHA, //TLS-RSA-WITH-AES-256-GCM-SHA384, //TLS-RSA-WITH-AES-256-CBC-SHA256, //TLS-RSA-WITH-AES-128-CBC-SHA, //TLS-RSA-WITH-RC4-128-MD5, //0} //Advanced Options: {1,0} OPENVPNCL_ADV, OPENVPNCL_TLSCIP, //LZO Compression: {yes, adaptive, no, off} OPENVPNCL_LZO, //NAT: {1, 0} OPENVPNCL_NAT, //Firewall Protection (enabled if openvpncl_nat==1): {1,0} OPENVPNCL_SEC, //Bridge TAP to br0 (enabled if openvpncl_nat==0)"{1,0} OPENVPNCL_BRIDGE, //IP Address OPENVPNCL_IP, //Subnet Mask OPENVPNCL_MASK, //Tunnel MTU Setting: (from 1 to 65535) OPENVPNCL_MTU, //Tunnel UDP Fragment (num max chars=5): if nothing => Disabled OPENVPNCL_FRAGMENT, //Tunnel UDP MSS-Fix: {1,0} OPENVPNCL_MSSFIX, //nsCertType verification: {1,0} OPENVPNCL_CERTTYPE, //TLS Auth Key OPENVPNCL_TLSAUTH, //Additional Config OPENVPNCL_CONFIG, //Policy based Routing OPENVPNCL_ROUTE, //PKCS12 Key OPENVPNCL_PKCS_12, //Static Key OPENVPNCL_STATIC, //CA Cert OPENVPNCL_CA, //Public Client Cert OPENVPNCL_CLIENT, //Private Client Key OPENVPNCL_KEY); } finally { if (nvramInfoTmp != null) { nvramInfo.putAll(nvramInfoTmp); } final String[] devDeviceLine = SSHUtils.getManualProperty(mRouter, mGlobalPreferences, "cat /tmp/openvpncl/openvpn.conf | grep \"dev \""); String openvpnclIface = null; if (devDeviceLine != null && devDeviceLine.length > 0) { openvpnclIface = devDeviceLine[0].replace("dev ", "").trim(); } if (!Strings.isNullOrEmpty(openvpnclIface)) { nvramInfo.setProperty(OPENVPNCL__DEV, openvpnclIface); //noinspection ConstantConditions final Map<WirelessIfaceTile.IfaceStatsType, Long> ifaceRxAndTxRates = getIfaceRxAndTxRates( openvpnclIface); final Long rxBps = ifaceRxAndTxRates.get(RX_BYTES); final Long txBps = ifaceRxAndTxRates.get(TX_BYTES); if (rxBps != null) { nvramInfo.setProperty(OPENVPNCL__DEV_RX_RATE, rxBps + " B/s (" + FileUtils.byteCountToDisplaySize(rxBps) + "/s)"); } if (txBps != null) { nvramInfo.setProperty(OPENVPNCL__DEV_TX_RATE, txBps + " B/s (" + FileUtils.byteCountToDisplaySize(txBps) + "/s)"); } //Packet Info final String sysClassNetStatsFolder = String.format(CAT_SYS_CLASS_NET_S_STATISTICS, openvpnclIface); try { final String[] packetsInfo = SSHUtils.getManualProperty(mRouter, mGlobalPreferences, String.format("%s/rx_packets", sysClassNetStatsFolder), String.format("%s/rx_errors", sysClassNetStatsFolder), String.format("%s/tx_packets", sysClassNetStatsFolder), String.format("%s/tx_errors", sysClassNetStatsFolder)); if (packetsInfo != null) { final long rxErrors = Long.parseLong(packetsInfo[1]); nvramInfo.setProperty(OPENVPNCL__DEV_RX_PACKETS, String.format("%s (%s)", packetsInfo[0], rxErrors <= 0 ? "no error" : (rxErrors + String.format("error%s", rxErrors > 1 ? "s" : "")))); final long txErrors = Long.parseLong(packetsInfo[3]); nvramInfo.setProperty(OPENVPNCL__DEV_TX_PACKETS, String.format("%s (%s)", packetsInfo[2], txErrors <= 0 ? "no error" : (txErrors + String.format(" error%s", txErrors > 1 ? "s" : "")))); } } catch (final Exception e) { e.printStackTrace(); //No worries } } } if (nvramInfo.isEmpty()) { throw new DDWRTNoDataException("No Data!"); } return nvramInfo; } catch (@NotNull final Exception e) { e.printStackTrace(); return new NVRAMInfo().setException(e); } } @NotNull private Map<WirelessIfaceTile.IfaceStatsType, Long> getIfaceRxAndTxRates( @NotNull final String phyIface) { final Map<WirelessIfaceTile.IfaceStatsType, Long> result = Maps.newHashMapWithExpectedSize(2); final String sysClassNetStatsFolder = String.format(CAT_SYS_CLASS_NET_S_STATISTICS, phyIface); final String rxBytesCmd = String.format("%s/rx_bytes", sysClassNetStatsFolder); final String txBytesCmd = String.format("%s/tx_bytes", sysClassNetStatsFolder); try { final long[] bytesBeforeAndAfter = parseFloatDataFromOutput(SSHUtils.getManualProperty(mRouter, mGlobalPreferences, rxBytesCmd, txBytesCmd, "sleep 1", rxBytesCmd, txBytesCmd)); if (bytesBeforeAndAfter.length >= 4) { result.put(RX_BYTES, Math.abs(bytesBeforeAndAfter[1] - bytesBeforeAndAfter[0])); result.put(TX_BYTES, Math.abs(bytesBeforeAndAfter[3] - bytesBeforeAndAfter[2])); } } catch (Exception e) { e.printStackTrace(); } return result; } @NotNull private long[] parseFloatDataFromOutput(@Nullable final String[] output) { if (output == null || output.length == 0) { throw new IllegalArgumentException("Output null or empty"); } final long[] result = new long[output.length]; for (int i = 0; i < output.length; i++) { result[i] = Long.parseLong(output[i]); } return result; } }; } @Nullable @Override protected String getLogTag() { return LOG_TAG; } @Override public void onLoadFinished(Loader<NVRAMInfo> loader, NVRAMInfo data) { Log.d(LOG_TAG, "onLoadFinished: loader=" + loader + " / data=" + data); layout.findViewById(R.id.tile_services_openvpn_client_header_loading_view).setVisibility(View.GONE); layout.findViewById(R.id.tile_services_openvpn_client_loading_view).setVisibility(View.GONE); final View openvpnclStatus = layout.findViewById(R.id.tile_services_openvpn_client_status); openvpnclStatus.setVisibility(View.VISIBLE); layout.findViewById(R.id.tile_services_openvpn_client_grid_layout).setVisibility(View.VISIBLE); layout.findViewById(R.id.tile_services_openvpn_client_note).setVisibility(View.VISIBLE); if (data == null) { data = new NVRAMInfo().setException(new DDWRTNoDataException("No Data!")); } @NotNull final TextView errorPlaceHolderView = (TextView) this.layout .findViewById(R.id.tile_services_openvpn_client_error); @Nullable final Exception exception = data.getException(); if (!(exception instanceof DDWRTTileAutoRefreshNotAllowedException)) { mNvramInfo = new NVRAMInfo(); mNvramInfo.putAll(data); if (exception == null) { errorPlaceHolderView.setVisibility(View.GONE); } updateTileDisplayData(data, true); } 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_services_openvpn_client_togglebutton_title, R.id.tile_services_openvpn_client_togglebutton_separator); Log.d(LOG_TAG, "onLoadFinished(): done loading!"); } private void updateTileDisplayData(@NotNull final NVRAMInfo data, final boolean defaultValuesIfNotFound) { //State final String statusKey = data.getProperty(OPENVPNCL_ENABLE, defaultValuesIfNotFound ? DDWRTCompanionConstants.EMPTY_STRING : null); if (statusKey != null) { final String statusValue; switch (statusKey) { case "1": statusValue = "Enabled"; break; case "0": statusValue = "Disabled"; break; default: statusValue = N_A; break; } ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_status)).setText(statusValue); ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_state)).setText(statusValue); } //Server IP/Name String property = data.getProperty(OPENVPNCL_REMOTEIP, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_server_ip_name)).setText(property); } //Port property = data.getProperty(OPENVPNCL_REMOTEPORT, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_server_port)).setText(property); } //BW In property = data.getProperty(OPENVPNCL__DEV_RX_RATE, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_rx_rate)).setText(property); } //BW Out property = data.getProperty(OPENVPNCL__DEV_TX_RATE, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_tx_rate)).setText(property); } //RX Packets property = data.getProperty(OPENVPNCL__DEV_RX_PACKETS, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_rx_packets)).setText(property); } //TX Packets property = data.getProperty(OPENVPNCL__DEV_TX_PACKETS, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_tx_packets)).setText(property); } //Tunnel Device //noinspection ConstantConditions property = data.getProperty(OPENVPNCL_TUNTAP, defaultValuesIfNotFound ? N_A : null); if (property != null) { ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_tunnel_device)) .setText(property.toUpperCase()); } //Tunnel Proto final String protoProp = data.getProperty(OPENVPNCL_PROTO, defaultValuesIfNotFound ? N_A : null); if (protoProp != null) { final String tunnelProto; if ("tcp-client".equalsIgnoreCase(protoProp)) { tunnelProto = "TCP"; } else { //noinspection ConstantConditions tunnelProto = protoProp.toUpperCase(); } ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_tunnel_protocol)) .setText(tunnelProto); } //Encryption Cipher final String encCipherProp = data.getProperty(OPENVPNCL_CIPHER, defaultValuesIfNotFound ? N_A : null); if (encCipherProp != null) { final String encCipher; //noinspection ConstantConditions switch (encCipherProp.toLowerCase()) { case "aes-512-cbc": encCipher = "AES-512 CBC"; break; case "aes-256-cbc": encCipher = "AES-256 CBC"; break; case "aes-192-cbc": encCipher = "AES-192 CBC"; break; case "aes-128-cbc": encCipher = "AES-128 CBC"; break; case "bf-cbc": encCipher = "Blowfish CBC"; break; default: encCipher = encCipherProp; break; } ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_encryption_cipher)) .setText(encCipher); } //Hash Algo final String hashAlgoProp = data.getProperty(OPENVPNCL_AUTH, defaultValuesIfNotFound ? N_A : null); if (hashAlgoProp != null) { final String hashAlgo; if (!"none".equalsIgnoreCase(hashAlgoProp)) { //noinspection ConstantConditions hashAlgo = hashAlgoProp.toUpperCase(); } else { hashAlgo = "None"; } ((TextView) layout.findViewById(R.id.tile_services_openvpn_client_hash_algorithm)).setText(hashAlgo); } } @Nullable @Override protected OnClickIntent getOnclickIntent() { if (mNvramInfo == null) { //Loading Utils.displayMessage(mParentFragmentActivity, "Loading data from router - please wait a few seconds.", Style.ALERT); return null; } if (mNvramInfo.isEmpty()) { //No data! Utils.displayMessage(mParentFragmentActivity, "No data available - please retry later.", Style.ALERT); return null; } final String mRouterUuid = mRouter.getUuid(); final Intent editOpenVPNClSettingsIntent = new Intent(mParentFragment.getActivity(), isThemeLight(mParentFragmentActivity, mRouterUuid) ? EditOpenVPNClientSettingsActivityLight.class : EditOpenVPNClientSettingsActivity.class); editOpenVPNClSettingsIntent.putExtra(OPENVPNCL_NVRAMINFO, mNvramInfo); editOpenVPNClSettingsIntent.putExtra(RouterManagementActivity.ROUTER_SELECTED, mRouterUuid); return new OnClickIntent("Loading OpenVPN Client Settings...", editOpenVPNClSettingsIntent, this); } @Override public void onResultCode(int resultCode, Intent data) { switch (resultCode) { case Activity.RESULT_OK: final NVRAMInfo newNvramInfoData = (NVRAMInfo) data.getSerializableExtra(OPENVPNCL_NVRAMINFO); if (newNvramInfoData == null || newNvramInfoData.isEmpty()) { Utils.displayMessage(mParentFragmentActivity, "No change", Style.INFO); break; } final Bundle token = new Bundle(); token.putString(DDWRTMainActivity.ROUTER_ACTION, RouterAction.SET_NVRAM_VARIABLES.toString()); token.putSerializable(OPENVPNCL_NVRAMINFO, newNvramInfoData); new UndoBarController.UndoBar(mParentFragmentActivity) .message("OpenVPN Client Settings will be updated").listener(this).token(token).show(); break; default: //Ignored break; } } @Override public void onHide(@android.support.annotation.Nullable Parcelable parcelable) { if (parcelable instanceof Bundle) { final Bundle token = (Bundle) parcelable; final String routerAction = token.getString(DDWRTMainActivity.ROUTER_ACTION); Log.d(LOG_TAG, "routerAction: [" + routerAction + "]"); if (isNullOrEmpty(routerAction)) { return; } try { switch (RouterAction.valueOf(routerAction)) { case SET_NVRAM_VARIABLES: new SetNVRAMVariablesAction((NVRAMInfo) token.getSerializable(OPENVPNCL_NVRAMINFO), true, //Reboot Router at the end of the operation this, mGlobalPreferences).execute(mRouter); break; default: //Ignored break; } } catch (IllegalArgumentException | NullPointerException e) { e.printStackTrace(); } } } @Override public void onClear(@NonNull Parcelable[] parcelables) { //Nothing to do } @Override public void onRouterActionSuccess(@NotNull RouterAction routerAction, @NotNull Router router, @Nullable final Object returnData) { Utils.displayMessage(mParentFragmentActivity, "Success", Style.CONFIRM); //Update info right away if (returnData instanceof NVRAMInfo) { //Run on main thread to avoid the exception: //"Only the original thread that created a view hierarchy can touch its views." mParentFragmentActivity.runOnUiThread(new Runnable() { @Override public void run() { updateTileDisplayData((NVRAMInfo) returnData, false); } }); } } @Override public void onRouterActionFailure(@NotNull RouterAction routerAction, @NotNull Router router, @Nullable Exception exception) { Utils.displayMessage(mParentFragmentActivity, String.format("Error: %s", ExceptionUtils.getRootCauseMessage(exception)), Style.ALERT); } @Override public void onUndo(@android.support.annotation.Nullable Parcelable parcelable) { //Nothing to do } }