/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2013-2017 Carmen Alvarez (c@rmen.ca)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ca.rmen.android.networkmonitor.app.dbops.backend.export;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.List;
import ca.rmen.android.networkmonitor.Constants;
import ca.rmen.android.networkmonitor.R;
import ca.rmen.android.networkmonitor.app.dbops.backend.export.FormatterFactory.FormatterStyle;
import ca.rmen.android.networkmonitor.app.dbops.ui.Share;
import ca.rmen.android.networkmonitor.app.prefs.FilterPreferences;
import ca.rmen.android.networkmonitor.app.prefs.NetMonPreferences;
import ca.rmen.android.networkmonitor.app.prefs.SortPreferences;
import ca.rmen.android.networkmonitor.app.prefs.SortPreferences.SortOrder;
import ca.rmen.android.networkmonitor.provider.NetMonColumns;
import ca.rmen.android.networkmonitor.util.Log;
/**
* Export the Network Monitor data to an HTML file. The HTML file includes CSS specified in the strings XML file.
*/
public class HTMLExport extends TableFileExport {
private static final String TAG = Constants.TAG + TableFileExport.class.getSimpleName();
private static final String SCHEME_NETMON = "netmon:";
private static final String TABLE_HEIGHT_FILE_EXPORT = "100vh";
public static final String URL_SORT = SCHEME_NETMON + "//sort";
public static final String URL_FILTER = SCHEME_NETMON + "//filter";
private static final String HTML_FILE = "networkmonitor.html";
private PrintWriter mPrintWriter;
private final String mFixedTableHeight;
/**
* @param external if true, the file will be exported to the sd card. Otherwise it will written to the application's internal storage.
*/
public HTMLExport(Context context, boolean external) {
this(context, external, TABLE_HEIGHT_FILE_EXPORT);
}
/**
* @param external if true, the file will be exported to the sd card. Otherwise it will written to the application's internal storage.
* @param fixedTableHeight CSS height specification for the body of the table (below the column headers). Ex: "100vh" or "1080px". If provided, the header of the table will remain in a fixed position, the body of the table will have the given fixed height, and the contents of the table body will be scrollable.
*/
public HTMLExport(Context context, boolean external, String fixedTableHeight) {
super(context, new File(external ? Share.getExportFolder(context) : context.getFilesDir(), HTML_FILE), FormatterStyle.XML);
mFixedTableHeight = fixedTableHeight;
}
@Override
void writeHeader(String[] columnLabels) {
try {
mPrintWriter = new PrintWriter(mFile, "utf-8");
} catch (FileNotFoundException | UnsupportedEncodingException e) {
Log.e(TAG, "writeHeader Could not initialize print writer", e);
return;
}
mPrintWriter.println("<!DOCTYPE html>");
mPrintWriter.println("<html>");
mPrintWriter.println(" <head>");
mPrintWriter.println(" <title>" + mContext.getString(R.string.app_name) + "</title>");
String columnCss = mContext.getString(R.string.css_template, getColumnCss());
mPrintWriter.println(mContext.getString(R.string.css));
mPrintWriter.println(mContext.getString(R.string.css_api_level_custom));
mPrintWriter.println(mContext.getString(R.string.css_themed));
mPrintWriter.println(columnCss);
if (mFixedTableHeight != null) {
String fixedHeaderCss = mContext.getString(R.string.fixed_header_css, mFixedTableHeight);
mPrintWriter.println(fixedHeaderCss);
}
mPrintWriter.println(" </head><body>");
mPrintWriter.println("<table class='main-table'>");
mPrintWriter.println(getColgroup(columnLabels.length));
mPrintWriter.println(" <thead><tr>");
SortPreferences sortPreferences = NetMonPreferences.getInstance(mContext).getSortPreferences();
mPrintWriter.println(" <th></th>");
for (String columnLabel : columnLabels) {
String dbColumnName = NetMonColumns.getColumnName(mContext, columnLabel);
if (dbColumnName != null) {
String labelClass = "column_heading";
// Indicate if this is the sorting column: specify a particular css style
// for the label, and show an up or down arrow depending on if we're
// sorting ascending or descending.
String sortIconCharacter = "";
if (dbColumnName.equals(sortPreferences.sortColumnName)) {
labelClass += " sort_column";
if (sortPreferences.sortOrder == SortOrder.DESC)
sortIconCharacter = mContext.getString(R.string.icon_sort_desc);
else
sortIconCharacter = mContext.getString(R.string.icon_sort_asc);
}
// Indicate if this is a filtered column: specify a particular css style
// for the label, and show the filter on or off icon.
boolean isFilterable = NetMonColumns.isColumnFilterable(mContext, dbColumnName);
String filterIconClass = "filter_icon";
String filterIconCharacter = isFilterable ? mContext.getString(R.string.icon_filter_off) : "";
if (isFilterable) {
List<String> columnFilterValues = NetMonPreferences.getInstance(mContext).getColumnFilterValues(dbColumnName);
if (columnFilterValues != null && columnFilterValues.size() > 0) {
labelClass += " filtered_column_label";
filterIconCharacter = mContext.getString(R.string.icon_filter_on);
}
}
// One cell for the sort icon and column label.
String sort = sortIconCharacter + "<a class=\"" + labelClass + "\" href=\"" + URL_SORT + dbColumnName + "\">" + columnLabel + "</a>";
// One cell for the filter icon.
String filter = "<a class=\"" + filterIconClass + "\" href=\"" + URL_FILTER + dbColumnName + "\">" + filterIconCharacter + "</a>";
// Write out the table cell for this column header.
mPrintWriter.println(" <th>" + sort + filter + "</th>");
} else {
mPrintWriter.println(" <th></th>");
}
}
mPrintWriter.println(" </tr></thead><tbody>");
mPrintWriter.println(" <tr>");
// container-container? :( Wish I could do this more cleanly/simply.
mPrintWriter.println(" <td class='table-data-container-container' colspan='" + (columnLabels.length + 1) + "'>");
mPrintWriter.println(" <div class='table-data-container'>");
mPrintWriter.println(" <table class='table-data'>");
mPrintWriter.println(getColgroup(columnLabels.length));
}
@Override
void writeRow(int rowNumber, String[] cellValues) {
if (mPrintWriter == null) return;
// Alternating styles for odd and even rows.
String trClass = "odd";
if (rowNumber % 2 == 0) trClass = "even";
mPrintWriter.println(" <tr class=\"" + trClass + "\">");
mPrintWriter.println(" <td>" + (rowNumber + 1) + "</td>");
for (String cellValue : cellValues) {
String tdClass = "";
// Highlight PASS in green and FAIL in red.
if (Constants.CONNECTION_TEST_FAIL.equals(cellValue)) tdClass = "fail";
else if (Constants.CONNECTION_TEST_PASS.equals(cellValue)) tdClass = "pass";
else if (Constants.CONNECTION_TEST_SLOW.equals(cellValue)) tdClass = "slow";
mPrintWriter.println(" <td class=\"" + tdClass + "\">" + cellValue + "</td>");
}
mPrintWriter.println(" </tr>");
mPrintWriter.flush();
}
@Override
void writeFooter() {
if (mPrintWriter == null) return;
mPrintWriter.println("</tbody></table></div></td></tr>");
mPrintWriter.println("</tbody></table>");
mPrintWriter.println("</body></html>");
mPrintWriter.flush();
mPrintWriter.close();
}
/**
* @return the longest word in the given string.
*/
private static String findLongestWord(String s) {
String[] tokens = s.split(" ");
String longestWord = null;
for (String token : tokens) {
if(longestWord == null || token.length() > longestWord.length()) longestWord = token;
}
return longestWord;
}
/**
* @return the HTML <colgroup> tag including all the <col> child tags, used to size each column.
*/
private String getColgroup(int numColumns) {
StringBuilder stringBuilder = new StringBuilder(" <colgroup>");
for (int i = 0; i <= numColumns; i++) {
stringBuilder.append(" <col class='col").append(i).append("'/>\n");
}
stringBuilder.append(" </colgroup>");
return stringBuilder.toString();
}
/**
* @return the CSS code for the styles of each column. The styles only specify the column width.
*/
private String getColumnCss() {
int[] columnWidths = getBestColumnWidths();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(".col0{width:50px}\n");
for (int i = 0; i < columnWidths.length; i++) {
stringBuilder.append(".col").append((i+1)).append("{width:").append((columnWidths[i] + 3)).append("em;}").append('\n');
}
return stringBuilder.toString();
}
/**
* Determines the best width for each column based on the column titles and the column data.
* @return an array of widths, in em, for each column in the table.
*/
private int[] getBestColumnWidths() {
String[] usedColumnNames = (String[]) NetMonPreferences.getInstance(mContext).getSelectedColumns().toArray();
Uri uri = NetMonColumns.CONTENT_URI;
FilterPreferences.Selection selection = FilterPreferences.getSelectionClause(mContext);
String[] projection = new String[usedColumnNames.length];
for (int i =0; i < usedColumnNames.length; i++) {
projection[i] = "MAX(length(" + usedColumnNames[i] + "))";
}
int[] columnWidths = new int[usedColumnNames.length];
Cursor c = mContext.getContentResolver().query(uri, projection, selection.selectionString, selection.selectionArgs,
null);
if (c != null) {
try {
if (c.moveToFirst()) {
for (int i=0; i < c.getColumnCount(); i++) {
String columnLabel = NetMonColumns.getColumnLabel(mContext, usedColumnNames[i]);
String longestWordInLabel = findLongestWord(columnLabel);
int lengthLongestWordInLabel = longestWordInLabel.length();
int lengthLongestValue = c.getInt(i);
// The best column width is the largest of:
// a) the largest data value
// b) the largest word in the column title
columnWidths[i] = Math.max(lengthLongestWordInLabel, lengthLongestValue);
}
}
} finally {
c.close();
}
}
return columnWidths;
}
}