/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org) * Copyright (C) 2013-2015 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.log; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.graphics.Bitmap; import android.graphics.Color; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.design.widget.Snackbar; import android.support.v4.app.NavUtils; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.io.File; import ca.rmen.android.networkmonitor.Constants; import ca.rmen.android.networkmonitor.R; import ca.rmen.android.networkmonitor.app.bus.NetMonBus; import ca.rmen.android.networkmonitor.app.dbops.backend.DBOpIntentService; import ca.rmen.android.networkmonitor.app.dbops.backend.export.HTMLExport; import ca.rmen.android.networkmonitor.app.dbops.ui.Share; import ca.rmen.android.networkmonitor.app.dialog.ChoiceDialogFragment; import ca.rmen.android.networkmonitor.app.dialog.ConfirmDialogFragment.DialogButtonListener; import ca.rmen.android.networkmonitor.app.dialog.DialogFragmentFactory; import ca.rmen.android.networkmonitor.app.dialog.PreferenceDialog; import ca.rmen.android.networkmonitor.app.prefs.FilterColumnActivity; import ca.rmen.android.networkmonitor.app.prefs.NetMonPreferences; import ca.rmen.android.networkmonitor.app.prefs.SelectFieldsActivity; import ca.rmen.android.networkmonitor.app.prefs.SortPreferences; import ca.rmen.android.networkmonitor.app.prefs.SortPreferences.SortOrder; import ca.rmen.android.networkmonitor.util.Log; public class LogActivity extends AppCompatActivity implements DialogButtonListener, ChoiceDialogFragment.DialogItemListener { private static final String TAG = Constants.TAG + LogActivity.class.getSimpleName(); private WebView mWebView; private Dialog mDialog; private Menu mMenu; private boolean mDBOpInProgress; private static final int REQUEST_CODE_CLEAR = 1; private static final int REQUEST_CODE_SELECT_FIELDS = 2; private static final int REQUEST_CODE_FILTER_COLUMN = 3; private static final int ID_ACTION_SHARE = 1; private static final int ID_ACTION_CLEAR = 2; @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "onCreate " + savedInstanceState); super.onCreate(savedInstanceState); setContentView(R.layout.log); if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); mWebView = (WebView) findViewById(R.id.web_view); assert mWebView != null; mWebView.setBackgroundColor(Color.TRANSPARENT); mWebView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { //noinspection deprecation mWebView.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { mWebView.getViewTreeObserver().removeOnGlobalLayoutListener(this); } loadHTMLFile(); } }); } @Override protected void onPause() { Log.v(TAG, "onPause"); if (mDialog != null) mDialog.dismiss(); PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); NetMonBus.getBus().unregister(this); super.onPause(); } @Override protected void onResume() { Log.v(TAG, "onResume"); super.onResume(); if (mDialog != null) mDialog.show(); PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); NetMonBus.getBus().register(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.log, menu); mMenu = menu; return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { // Only show the menu item to clear filters if we have filters. menu.findItem(R.id.action_reset_filters).setVisible(NetMonPreferences.getInstance(this).hasColumnFilters()); menu.findItem(R.id.action_clear).setEnabled(!mDBOpInProgress); MenuItem menuItem = menu.findItem(R.id.action_freeze_header); // Freezing the table header only seems to work on kitkat+. // Don't know why. But we'll hide this feature on older versions. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { menuItem.setVisible(false); } else { boolean freezeHeader = NetMonPreferences.getInstance(this).getFreezeHtmlTableHeader(); if (freezeHeader) menuItem.setTitle(R.string.action_unfreeze_header); else menuItem.setTitle(R.string.action_freeze_header); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: NavUtils.navigateUpFromSameTask(this); return true; case R.id.action_share: DialogFragmentFactory.showChoiceDialog(this, getString(R.string.export_choice_title), getResources().getStringArray(R.array.export_choices), -1, ID_ACTION_SHARE); return true; case R.id.action_refresh: loadHTMLFile(); return true; case R.id.action_clear: DialogFragmentFactory.showConfirmDialog(this, getString(R.string.action_clear), getString(R.string.confirm_logs_clear), ID_ACTION_CLEAR, null); return true; case R.id.action_select_fields: Intent intentSelectFields = new Intent(this, SelectFieldsActivity.class); startActivityForResult(intentSelectFields, REQUEST_CODE_SELECT_FIELDS); return true; case R.id.action_filter: mDialog = PreferenceDialog.showFilterRecordCountChoiceDialog(this, mPreferenceChoiceDialogListener); return true; case R.id.action_cell_id_format: mDialog = PreferenceDialog.showCellIdFormatChoiceDialog(this, mPreferenceChoiceDialogListener); return true; case R.id.action_reset_filters: DialogFragmentFactory.showConfirmDialog(this, getString(R.string.clear_filters_confirm_dialog_title), getString(R.string.clear_filters_confirm_dialog_message), R.id.action_reset_filters, null); return true; case R.id.action_freeze_header: boolean newFreezeHeaderSetting = !NetMonPreferences.getInstance(this).getFreezeHtmlTableHeader(); NetMonPreferences.getInstance(this).setFreezeHtmlTableHeader(newFreezeHeaderSetting); loadHTMLFile(); return true; } return super.onOptionsItemSelected(item); } /** * Read the data from the DB, export it to an HTML file, and load the HTML file in the WebView. */ private void loadHTMLFile() { Log.v(TAG, "loadHTMLFile"); final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar); assert progressBar != null; progressBar.setVisibility(View.VISIBLE); startRefreshIconAnimation(); final boolean freezeHeader = NetMonPreferences.getInstance(this).getFreezeHtmlTableHeader(); final String fixedTableHeight; if (freezeHeader) { // I've come to the following calculation by trial and error. // I've noticed that when entering the web view, the density is equal to the scale/zoom, but I'm // not sure if it's the density or zoom which really matters in this calculation. // We subtract 100px from the scaled webview height to account for the table header. fixedTableHeight = ((mWebView.getHeight() / getResources().getDisplayMetrics().density) - 100) + "px"; } else { fixedTableHeight = null; } AsyncTask<Void, Void, File> asyncTask = new AsyncTask<Void, Void, File>() { @Override protected File doInBackground(Void... params) { Log.v(TAG, "loadHTMLFile:doInBackground"); // Export the DB to the HTML file. HTMLExport htmlExport = new HTMLExport(LogActivity.this, false, fixedTableHeight); int recordCount = NetMonPreferences.getInstance(LogActivity.this).getFilterRecordCount(); return htmlExport.export(recordCount, null); } @SuppressLint("SetJavaScriptEnabled") @Override protected void onPostExecute(File result) { Log.v(TAG, "loadHTMLFile:onPostExecute, result=" + result); if (isFinishing()) { Log.v(TAG, "finishing, ignoring loadHTMLFile result"); return; } WebView webView = mWebView; if (webView == null) { Log.v(TAG, "Must be destroyed or destroying, we have no webview, ignoring loadHTMLFile result"); return; } if (result == null) { Snackbar.make(webView, R.string.error_reading_log, Snackbar.LENGTH_LONG).show(); return; } // Load the exported HTML file into the WebView. // Save our current horizontal scroll position so we can keep our // horizontal position after reloading the page. final int oldScrollX = webView.getScrollX(); webView.getSettings().setUseWideViewPort(true); webView.getSettings().setBuiltInZoomControls(true); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("file://" + result.getAbsolutePath()); webView.setWebViewClient(new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); Log.v(TAG, "onPageStarted"); // Javascript hack to scroll back to our old X position. // http://stackoverflow.com/questions/6855715/maintain-webview-content-scroll-position-on-orientation-change if (oldScrollX > 0) { String jsScrollX = "javascript:window:scrollTo(" + oldScrollX + " / window.devicePixelRatio,0);"; view.loadUrl(jsScrollX); } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); progressBar.setVisibility(View.GONE); stopRefreshIconAnimation(); } @Override @TargetApi(Build.VERSION_CODES.N) public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return loadUrl(request.getUrl().toString()) || super.shouldOverrideUrlLoading(view, request); } @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return loadUrl(url) || super.shouldOverrideUrlLoading(view, url); } private boolean loadUrl(String url) { Log.v(TAG, "url: " + url); // If the user clicked on one of the column names, let's update // the sorting preference (column name, ascending or descending order). if (url.startsWith(HTMLExport.URL_SORT)) { NetMonPreferences prefs = NetMonPreferences.getInstance(LogActivity.this); SortPreferences oldSortPreferences = prefs.getSortPreferences(); // The new column used for sorting will be the one the user tapped on. String newSortColumnName = url.substring(HTMLExport.URL_SORT.length()); SortOrder newSortOrder = oldSortPreferences.sortOrder; // If the user clicked on the column which is already used for sorting, // toggle the sort order between ascending and descending. if (newSortColumnName.equals(oldSortPreferences.sortColumnName)) { if (oldSortPreferences.sortOrder == SortOrder.DESC) newSortOrder = SortOrder.ASC; else newSortOrder = SortOrder.DESC; } // Update the sorting preferences (our shared preference change listener will be notified // and reload the page). prefs.setSortPreferences(new SortPreferences(newSortColumnName, newSortOrder)); return true; } // If the user clicked on the filter icon, start the filter activity for this column. else if (url.startsWith(HTMLExport.URL_FILTER)) { Intent intent = new Intent(LogActivity.this, FilterColumnActivity.class); String columnName = url.substring(HTMLExport.URL_FILTER.length()); intent.putExtra(FilterColumnActivity.EXTRA_COLUMN_NAME, columnName); startActivityForResult(intent, REQUEST_CODE_FILTER_COLUMN); return true; } else { return false; } } }); } }; asyncTask.execute(); } @Override public void onDestroy() { Log.v(TAG, "onDestroy"); if (mWebView != null) { if (Build.VERSION.SDK_INT >= 11) mWebView.getSettings().setDisplayZoomControls(false); mWebView.removeAllViews(); ((ViewGroup) mWebView.getParent()).removeView(mWebView); mWebView.destroy(); mWebView = null; } super.onDestroy(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.v(TAG, "onActivityResult: requestCode = " + requestCode + ", resultCode = " + resultCode + ", data " + data); super.onActivityResult(requestCode, resultCode, data); if ((requestCode == REQUEST_CODE_CLEAR || requestCode == REQUEST_CODE_SELECT_FIELDS || requestCode == REQUEST_CODE_FILTER_COLUMN) && resultCode == RESULT_OK) loadHTMLFile(); } @SuppressWarnings("unused") @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onDBOperationStarted(NetMonBus.DBOperationStarted event) { Log.d(TAG, "onDBOperationStarted() called with " + "event = [" + event + "]"); mDBOpInProgress = true; supportInvalidateOptionsMenu(); } @SuppressWarnings("unused") @Subscribe public void onDBOperationEnded(NetMonBus.DBOperationEnded event) { Log.d(TAG, "onDBOperationEnded() called with " + "event = [" + event + "]"); mDBOpInProgress = false; supportInvalidateOptionsMenu(); if (event.isDataChanged) loadHTMLFile(); } /** * Reload the page when the user accepts a preference choice dialog. */ private final PreferenceDialog.PreferenceChoiceDialogListener mPreferenceChoiceDialogListener = new PreferenceDialog.PreferenceChoiceDialogListener() { @Override public void onPreferenceValueSelected(final String value) { loadHTMLFile(); } @Override public void onCancel() {} }; /** * Refresh the screen when certain shared preferences change. */ private final OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = (sharedPreferences, key) -> { if (key.equals(NetMonPreferences.PREF_SORT_COLUMN_NAME) || key.equals(NetMonPreferences.PREF_SORT_ORDER)) loadHTMLFile(); }; @Override public void onOkClicked(int actionId, Bundle extras) { // The user confirmed to clear the logs. Let's do that and refresh the screen. if (actionId == R.id.action_reset_filters) { NetMonPreferences.getInstance(this).resetColumnFilters(); loadHTMLFile(); } else if (actionId == ID_ACTION_CLEAR) { Log.v(TAG, "Clicked ok to clear log"); DBOpIntentService.startActionPurge(this, 0); } } @Override public void onCancelClicked(int actionId, Bundle extras) {} @Override public void onItemSelected(int actionId, int which) { // The user picked a file format to export. if (actionId == ID_ACTION_SHARE) { String[] exportChoices = getResources().getStringArray(R.array.export_choices); String selectedShareFormat = exportChoices[which]; Share.share(this, selectedShareFormat); } } private void startRefreshIconAnimation() { Log.v(TAG, "startRefreshIconAnimation"); if(mMenu == null) return; // This is null when we first enter the activity and the page loads. MenuItem menuItemRefresh = mMenu.findItem(R.id.action_refresh); if(menuItemRefresh == null) return; View refreshIcon = View.inflate(this, R.layout.refresh_icon, null); Animation rotation = AnimationUtils.loadAnimation(this, R.anim.rotate); rotation.setRepeatCount(Animation.INFINITE); refreshIcon.startAnimation(rotation); MenuItemCompat.setActionView(menuItemRefresh, refreshIcon); } private void stopRefreshIconAnimation() { Log.v(TAG, "stopRefreshIconAnimation"); if(mMenu == null) return; MenuItem menuItemRefresh = mMenu.findItem(R.id.action_refresh); if(menuItemRefresh == null) return; View refreshIcon = MenuItemCompat.getActionView(menuItemRefresh); if (refreshIcon != null) { refreshIcon.clearAnimation(); MenuItemCompat.setActionView(menuItemRefresh, null); } } }