/* * Copyright (c) 2012-2016 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ package se.leap.bitmaskclient.base.fragments; import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; import static se.leap.bitmaskclient.R.string.log_fragment_title; import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle; import static se.leap.bitmaskclient.base.utils.ViewHelper.setDefaultActivityBarColor; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.DataSetObserver; import android.os.Bundle; import android.os.Handler; import android.os.Handler.Callback; import android.os.Message; import android.text.SpannableString; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatTextView; import androidx.fragment.app.ListFragment; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.Vector; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.LogItem; import de.blinkt.openvpn.core.OpenVPNManagement; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.VpnStatus.LogListener; import de.blinkt.openvpn.core.VpnStatus.StateListener; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.base.utils.PreferenceHelper; import se.leap.bitmaskclient.base.utils.ViewHelper; public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener { public static final String TAG = LogFragment.class.getSimpleName(); private static final String LOGTIMEFORMAT = "logtimeformat"; private static final String VERBOSITYLEVEL = "verbositylevel"; private SeekBar mLogLevelSlider; private LinearLayout mOptionsLayout; private RadioGroup mTimeRadioGroup; private AppCompatTextView mUpStatus; private AppCompatTextView mDownStatus; private AppCompatTextView mConnectStatus; private boolean mShowOptionsLayout; private CheckBox mClearLogCheckBox; @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { ladapter.setLogLevel(progress + 1); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.radioISO: ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO); break; case R.id.radioNone: ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE); break; case R.id.radioShort: ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT); break; } } @Override public void updateByteCount(long in, long out, long diffIn, long diffOut) { //%2$s/s %1$s - ↑%4$s/s %3$s Resources res = getActivity().getResources(); final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res)); final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); if (mUpStatus != null && mDownStatus != null) { if (getActivity() != null) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { mUpStatus.setText(up); mDownStatus.setText(down); } }); } } } class LogWindowListAdapter implements ListAdapter, LogListener, Callback { private static final int MESSAGE_NEWLOG = 0; private static final int MESSAGE_CLEARLOG = 1; private static final int MESSAGE_NEWTS = 2; private static final int MESSAGE_NEWLOGLEVEL = 3; public static final int TIME_FORMAT_NONE = 0; public static final int TIME_FORMAT_SHORT = 1; public static final int TIME_FORMAT_ISO = 2; private static final int MAX_STORED_LOG_ENTRIES = 1000; private Vector allEntries = new Vector<>(); private Vector currentLevelEntries = new Vector(); private Handler mHandler; private Vector observers = new Vector(); private int mTimeFormat = 0; private int mLogLevel = 3; public LogWindowListAdapter() { initLogBuffer(); if (mHandler == null) { mHandler = new Handler(this); } VpnStatus.addLogListener(this); } private void initLogBuffer() { allEntries.clear(); Collections.addAll(allEntries, VpnStatus.getlogbuffer()); initCurrentMessages(); } String getLogStr() { String str = ""; for (LogItem entry : allEntries) { str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n'; } return str; } private void shareLog() { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr()); shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file)); shareIntent.setType("text/plain"); startActivity(Intent.createChooser(shareIntent, "Send Logfile")); } @Override public void registerDataSetObserver(DataSetObserver observer) { observers.add(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { observers.remove(observer); } @Override public int getCount() { return currentLevelEntries.size(); } @Override public Object getItem(int position) { return currentLevelEntries.get(position); } @Override public long getItemId(int position) { return ((Object) currentLevelEntries.get(position)).hashCode(); } @Override public boolean hasStableIds() { return true; } @Override public View getView(int position, View convertView, ViewGroup parent) { AppCompatTextView v; if (convertView == null) v = new AppCompatTextView(getActivity()); else v = (AppCompatTextView) convertView; LogItem le = currentLevelEntries.get(position); String msg = le.getString(getActivity()); String time = getTime(le, mTimeFormat); msg = time + msg; int spanStart = time.length(); SpannableString t = new SpannableString(msg); v.setText(t); return v; } private String getTime(LogItem le, int time) { if (time != TIME_FORMAT_NONE) { Date d = new Date(le.getLogtime()); java.text.DateFormat timeformat; if (time == TIME_FORMAT_ISO) timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); else timeformat = DateFormat.getTimeFormat(getActivity()); return timeformat.format(d) + " "; } else { return ""; } } @Override public int getItemViewType(int position) { return 0; } @Override public int getViewTypeCount() { return 1; } @Override public boolean isEmpty() { return currentLevelEntries.isEmpty(); } @Override public boolean areAllItemsEnabled() { return true; } @Override public boolean isEnabled(int position) { return true; } @Override public void newLog(LogItem logMessage) { Message msg = Message.obtain(); assert (msg != null); msg.what = MESSAGE_NEWLOG; Bundle bundle = new Bundle(); bundle.putParcelable("logmessage", logMessage); msg.setData(bundle); mHandler.sendMessage(msg); } @Override public boolean handleMessage(Message msg) { // We have been called if (msg.what == MESSAGE_NEWLOG) { LogItem logMessage = msg.getData().getParcelable("logmessage"); if (addLogMessage(logMessage)) for (DataSetObserver observer : observers) { observer.onChanged(); } } else if (msg.what == MESSAGE_CLEARLOG) { for (DataSetObserver observer : observers) { observer.onInvalidated(); } initLogBuffer(); } else if (msg.what == MESSAGE_NEWTS) { for (DataSetObserver observer : observers) { observer.onInvalidated(); } } else if (msg.what == MESSAGE_NEWLOGLEVEL) { initCurrentMessages(); for (DataSetObserver observer : observers) { observer.onChanged(); } } return true; } private void initCurrentMessages() { currentLevelEntries.clear(); for (LogItem li : allEntries) { if (li.getVerbosityLevel() <= mLogLevel || mLogLevel == VpnProfile.MAXLOGLEVEL) currentLevelEntries.add(li); } } /** * @param logmessage * @return True if the current entries have changed */ private boolean addLogMessage(LogItem logmessage) { allEntries.add(logmessage); if (allEntries.size() > MAX_STORED_LOG_ENTRIES) { Vector oldAllEntries = allEntries; allEntries = new Vector(allEntries.size()); for (int i = 50; i < oldAllEntries.size(); i++) { allEntries.add(oldAllEntries.elementAt(i)); } initCurrentMessages(); return true; } else { if (logmessage.getVerbosityLevel() <= mLogLevel) { currentLevelEntries.add(logmessage); return true; } else { return false; } } } void clearLog() { // Actually is probably called from GUI Thread as result of the user // pressing a button. But better safe than sorry VpnStatus.clearLog(); VpnStatus.logInfo(R.string.logCleared); mHandler.sendEmptyMessage(MESSAGE_CLEARLOG); } public void setTimeFormat(int newTimeFormat) { mTimeFormat = newTimeFormat; mHandler.sendEmptyMessage(MESSAGE_NEWTS); } public void setLogLevel(int logLevel) { mLogLevel = logLevel; mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL); } } private LogWindowListAdapter ladapter; private AppCompatTextView mSpeedView; @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.clearlog) { ladapter.clearLog(); return true; } else if (item.getItemId() == R.id.send) { ladapter.shareLog(); } else if (item.getItemId() == R.id.toggle_time) { showHideOptionsPanel(); } return super.onOptionsItemSelected(item); } private void showHideOptionsPanel() { boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE); ObjectAnimator anim; if (optionsVisible) { anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f); anim.addListener(collapseListener); } else { mOptionsLayout.setVisibility(View.VISIBLE); anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f); //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f); } //anim.setInterpolator(new AccelerateInterpolator(1.0f)); //anim.setDuration(300); //mOptionsLayout.startAnimation(anim); anim.start(); } AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { mOptionsLayout.setVisibility(View.GONE); } }; @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.f_log, menu); ViewHelper.tintMenuIcons(getContext(), menu, R.color.colorActionBarTitleFont); if (getResources().getBoolean(R.bool.logSlidersAlwaysVisible)) menu.removeItem(R.id.toggle_time); } @Override public void onResume() { super.onResume(); Intent intent = new Intent(getActivity(), OpenVPNService.class); intent.setAction(OpenVPNService.START_SERVICE); } @Override public void onStart() { super.onStart(); VpnStatus.addStateListener(this); VpnStatus.addByteCountListener(this); } @Override public void onStop() { super.onStop(); VpnStatus.removeStateListener(this); VpnStatus.removeByteCountListener(this); getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat) .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply(); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ListView lv = getListView(); lv.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("Log Entry", ((AppCompatTextView) view).getText()); clipboard.setPrimaryClip(clip); Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show(); return true; } }); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.f_log, container, false); setHasOptionsMenu(true); ladapter = new LogWindowListAdapter(); ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1); int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1); ladapter.setLogLevel(logLevel); setListAdapter(ladapter); mTimeRadioGroup = v.findViewById(R.id.timeFormatRadioGroup); mTimeRadioGroup.setOnCheckedChangeListener(this); if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) { mTimeRadioGroup.check(R.id.radioISO); } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) { mTimeRadioGroup.check(R.id.radioNone); } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) { mTimeRadioGroup.check(R.id.radioShort); } mClearLogCheckBox = v.findViewById(R.id.clearlogconnect); mClearLogCheckBox.setChecked(PreferenceHelper.getClearLog()); mClearLogCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceHelper.setClearLog(isChecked)); mSpeedView = v.findViewById(R.id.speed); mOptionsLayout = v.findViewById(R.id.logOptionsLayout); mLogLevelSlider = v.findViewById(R.id.LogLevelSlider); mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1); mLogLevelSlider.setProgress(logLevel - 1); mLogLevelSlider.setOnSeekBarChangeListener(this); if (getResources().getBoolean(R.bool.logSlidersAlwaysVisible)) mOptionsLayout.setVisibility(View.VISIBLE); mUpStatus = v.findViewById(R.id.speedUp); mDownStatus = v.findViewById(R.id.speedDown); mConnectStatus = v.findViewById(R.id.speedStatus); if (mShowOptionsLayout) mOptionsLayout.setVisibility(View.VISIBLE); setActionBarSubtitle(this, log_fragment_title); setDefaultActivityBarColor(getActivity()); return v; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Scroll to the end of the list end //getListView().setSelection(getListView().getAdapter().getCount()-1); } @Override public void onAttach(Context context) { super.onAttach(context); if (getResources().getBoolean(R.bool.logSlidersAlwaysVisible)) { mShowOptionsLayout = true; if (mOptionsLayout != null) mOptionsLayout.setVisibility(View.VISIBLE); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) { if (isAdded()) { final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity()); getActivity().runOnUiThread(() -> { if (isAdded()) { if (mSpeedView != null) { mSpeedView.setText(cleanLogMessage); } if (mConnectStatus != null) mConnectStatus.setText(cleanLogMessage); } }); } } @Override public void setConnectedVPN(String uuid) { } @Override public void onDestroy() { VpnStatus.removeLogListener(ladapter); super.onDestroy(); } }