Commit 596b826d authored by Thomas Guillem's avatar Thomas Guillem

DebugLog: use a service with a thread

- Remove dependency to LibVLC log stuff.

- Use a thread (via Logcat class) in a service: that way, logs are not lost and
  are always updated.

- Run the service in a separate process: it's not killed if VLC crashes.

- Use a ListView to show the logs: easily scrollable.

- Add a button to save the log in sdcard.

- There is a notification to notify that logs are being recorded, and to access
  the logs.
parent 62b78e67
......@@ -61,7 +61,12 @@
android:name=".gui.BrowserActivity"
android:label="@string/mediafiles"
android:theme="@style/Theme.VLC.NoTitleBar" />
<activity android:name=".gui.DebugLogActivity" />
<activity android:name=".gui.DebugLogActivity"
android:launchMode="singleTop" />
<service android:name=".gui.DebugLogService"
android:process=":logger" />
<activity
android:name=".gui.NativeCrashActivity"
android:process=":NativeCrashActivity"
......
......@@ -23,24 +23,35 @@
android:text="@string/stop_logging" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/copy_to_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.50"
android:text="@string/copy_to_clipboard" />
<Button
android:id="@+id/save_to_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.50"
android:text="@string/dump_logcat" />
</LinearLayout>
<Button
android:id="@+id/clear_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/clear_log" />
<Button
android:id="@+id/copy_to_clipboard"
<ListView
android:id="@+id/log_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/copy_to_clipboard" />
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:textIsSelectable="true" />
android:paddingTop="10dip" />
</LinearLayout>
\ No newline at end of file
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:typeface="monospace" />
......@@ -290,6 +290,8 @@
<string name="start_logging">Start logging</string>
<string name="stop_logging">Stop logging</string>
<string name="clear_log">Clear log</string>
<string name="log_service_title">VLC logs recording</string>
<string name="log_service_text">Open the log console</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
<string name="copied_to_clipboard">Copied log to clipboard.</string>
<string name="quit">Quit and restart application</string>
......
/*****************************************************************************
* DebugLogActivity.java
*****************************************************************************
* Copyright © 2013 VLC authors and VideoLAN
* Copyright © 2013-2015 VLC authors and VideoLAN
* Copyright © 2013 Edward Wang
*
* This program is free software; you can redistribute it and/or modify
......@@ -20,85 +20,171 @@
*****************************************************************************/
package org.videolan.vlc.gui;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.LibVlcException;
import org.videolan.vlc.R;
import org.videolan.vlc.VLCApplication;
import org.videolan.vlc.util.Logcat;
import org.videolan.vlc.util.VLCInstance;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class DebugLogActivity extends Activity {
public class DebugLogActivity extends Activity implements DebugLogService.Client.Callback {
public final static String TAG = "VLC/DebugLogActivity";
private DebugLogService.Client mClient = null;
private Button mStartButton = null;
private Button mStopButton = null;
private Button mCopyButton = null;
private Button mClearButton = null;
private Button mSaveButton = null;
private ListView mLogView;
private ArrayList<String> mLogList = null;
private ArrayAdapter<String> mLogAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.debug_log);
final LibVLC instance;
try {
instance = VLCInstance.getLibVlcInstance();
} catch (LibVlcException e) { return; }
mStartButton = (Button)findViewById(R.id.start_log);
mStopButton = (Button)findViewById(R.id.stop_log);
mLogView = (ListView) findViewById(R.id.log_list);
mCopyButton = (Button)findViewById(R.id.copy_to_clipboard);
mClearButton = (Button)findViewById(R.id.clear_log);
mSaveButton = (Button)findViewById(R.id.save_to_file);
final Button startLog = (Button)findViewById(R.id.start_log);
final Button stopLog = (Button)findViewById(R.id.stop_log);
mClient = new DebugLogService.Client(this, this);
startLog.setEnabled(! instance.isDebugBuffering());
stopLog.setEnabled(instance.isDebugBuffering());
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
setOptionsButtonsEnabled(false);
startLog.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
instance.startDebugBuffer();
startLog.setEnabled(false);
stopLog.setEnabled(true);
}
});
stopLog.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
instance.stopDebugBuffer();
stopLog.setEnabled(false);
startLog.setEnabled(true);
}
});
Button clearLog = (Button)findViewById(R.id.clear_log);
clearLog.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
instance.clearBuffer();
updateTextView(instance);
}
});
mStartButton.setOnClickListener(mStartClickListener);
mStopButton.setOnClickListener(mStopClickListener);
mClearButton.setOnClickListener(mClearClickListener);
mSaveButton.setOnClickListener(mSaveClickListener);
updateTextView(instance);
mCopyButton.setOnClickListener(mCopyClickListener);
}
Button copyToClipboard = (Button)findViewById(R.id.copy_to_clipboard);
copyToClipboard.setEnabled(((TextView)findViewById(R.id.textview)).getText().length() > 0);
copyToClipboard.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
copyTextToClipboard();
Toast.makeText(DebugLogActivity.this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
});
@Override
protected void onDestroy() {
mClient.release();
super.onDestroy();
}
private View.OnClickListener mStartClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
mClient.start();
}
};
private View.OnClickListener mStopClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mStartButton.setEnabled(false);
mStopButton.setEnabled(false);
mClient.stop();
}
};
private void setOptionsButtonsEnabled(boolean enabled) {
mClearButton.setEnabled(enabled);
mCopyButton.setEnabled(enabled);
mSaveButton.setEnabled(enabled);
}
private View.OnClickListener mClearClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mClient.clear();
if (mLogList != null) {
mLogList.clear();
mLogAdapter.notifyDataSetChanged();
}
setOptionsButtonsEnabled(false);
}
};
private View.OnClickListener mSaveClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mClient.save();
}
};
@SuppressWarnings("deprecation")
private void copyTextToClipboard() {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(((TextView)findViewById(R.id.textview)).getText());
private View.OnClickListener mCopyClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
final StringBuffer buffer = new StringBuffer();
for (String line : mLogList)
buffer.append(line+"\n");
android.text.ClipboardManager clipboard = (android.text.ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(buffer);
Toast.makeText(DebugLogActivity.this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
};
@Override
public void onStarted(List<String> logList) {
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
if (logList.size() > 0)
setOptionsButtonsEnabled(true);
mLogList = new ArrayList<String>(logList);
mLogAdapter = new ArrayAdapter<String>(this, R.layout.debug_log_item, mLogList);
mLogView.setAdapter(mLogAdapter);
mLogView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
if (mLogList.size() > 0)
mLogView.setSelection(mLogList.size() - 1);
}
@Override
public void onStopped() {
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
}
@Override
public void onLog(String msg) {
if (mLogList != null) {
mLogList.add(msg);
mLogAdapter.notifyDataSetChanged();
setOptionsButtonsEnabled(true);
}
}
private void updateTextView(final LibVLC instance) {
TextView textView = (TextView)findViewById(R.id.textview);
textView.setText(instance.getBufferContent());
@Override
public void onSaved(boolean success, String path) {
if (success) {
Toast.makeText(
this,
String.format(
VLCApplication.getAppResources().getString(R.string.dump_logcat_success),
path), Toast.LENGTH_LONG)
.show();
} else {
Toast.makeText(this,
R.string.dump_logcat_failure,
Toast.LENGTH_LONG).show();
}
}
}
/*****************************************************************************
* DebugLogService.java
*****************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
package org.videolan.vlc.gui;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.LinkedList;
import java.util.List;
import org.videolan.vlc.R;
import org.videolan.vlc.util.Logcat;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.text.format.DateFormat;
import android.util.Log;
public class DebugLogService extends Service implements Logcat.Callback, Runnable {
private static final int MSG_STARTED = 0;
private static final int MSG_STOPPED = 1;
private static final int MSG_ONLOG = 2;
private static final int MSG_SAVED = 3;
private static final int MAX_LINES = 20000;
private Logcat mLogcat = null;
private LinkedList<String> mLogList = new LinkedList<String>();
private Thread mSaveThread = null;
private String mSaveFilename = null;
private final RemoteCallbackList<IDebugLogServiceCallback> mCallbacks = new RemoteCallbackList<IDebugLogServiceCallback>();
private final IBinder mBinder = new DebugLogServiceStub(this);
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
static class DebugLogServiceStub extends IDebugLogService.Stub {
private DebugLogService mService;
DebugLogServiceStub(DebugLogService service) {
mService = service;
}
public void start() {
mService.start();
}
public void stop() {
mService.stop();
}
public void clear() {
mService.clear();
}
public void save() {
mService.save();
}
public void registerCallback(IDebugLogServiceCallback cb) {
mService.registerCallback(cb);
}
public void unregisterCallback(IDebugLogServiceCallback cb) {
mService.unregisterCallback(cb);
}
}
private synchronized void sendMessage(int what, String str) {
int i = mCallbacks.beginBroadcast();
while (i > 0) {
i--;
final IDebugLogServiceCallback cb = mCallbacks.getBroadcastItem(i);
try {
switch (what) {
case MSG_STOPPED:
cb.onStopped();
break;
case MSG_STARTED: {
cb.onStarted(mLogList);
break;
}
case MSG_ONLOG:
cb.onLog(str);
break;
case MSG_SAVED:
cb.onSaved(str != null ? true : false, str);
break;
}
} catch (RemoteException e) {
}
}
mCallbacks.finishBroadcast();
}
@Override
public synchronized void onLog(String log) {
if (mLogList.size() > MAX_LINES)
mLogList.remove(0);
mLogList.add(log);
sendMessage(MSG_ONLOG, log);
}
public synchronized void start() {
if (mLogcat != null)
return;
clear();
mLogcat = new Logcat();
mLogcat.start(this);
final Intent debugLogIntent = new Intent(this, DebugLogActivity.class);
debugLogIntent.setAction("android.intent.action.MAIN");
debugLogIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent pi = PendingIntent.getActivity(this, 0, debugLogIntent, 0);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentTitle(getResources().getString(R.string.log_service_title));
builder.setContentText(getResources().getString(R.string.log_service_text));
builder.setSmallIcon(R.drawable.ic_stat_vlc);
builder.setContentIntent(pi);
final Notification notification = builder.build();
startForeground(R.string.log_service_title, notification);
startService(new Intent(this, DebugLogService.class));
sendMessage(MSG_STARTED, null);
}
public synchronized void stop() {
mLogcat.stop();
mLogcat = null;
sendMessage(MSG_STOPPED, null);
stopForeground(true);
stopSelf();
}
public synchronized void clear() {
mLogList.clear();
}
/* mSaveThread */
@Override
public void run() {
final CharSequence timestamp = DateFormat.format(
"yyyyMMdd_kkmmss", System.currentTimeMillis());
final String filename = Environment.getExternalStorageDirectory().getPath() + "/vlc_logcat_" + timestamp + ".log";
boolean saved = true;
FileOutputStream fos = null;
OutputStreamWriter output = null;
BufferedWriter bw = null;
try {
fos = new FileOutputStream(filename);
output = new OutputStreamWriter(fos);
bw = new BufferedWriter(output);
synchronized (this) {
for (String line : mLogList) {
bw.write(line);
bw.newLine();
}
}
} catch (FileNotFoundException e) {
saved = false;
} catch (IOException ioe) {
saved = false;
} finally {
try {
if (bw != null)
bw.close();
if (output != null)
output.close();
if (fos != null)
fos.close();
} catch (IOException e) {
saved = false;
}
}
synchronized (this) {
mSaveThread = null;
sendMessage(MSG_SAVED, saved ? filename : null);
}
}
public synchronized void save() {
if (mSaveThread != null) {
try {
mSaveThread.join();
} catch (InterruptedException e) {}
mSaveThread = null;
}
mSaveThread = new Thread(this);
mSaveThread.start();
}
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
private void registerCallback(IDebugLogServiceCallback cb) {
if (cb != null) {
mCallbacks.register(cb);
sendMessage(mLogcat != null ? MSG_STARTED : MSG_STOPPED, null);
}
}
private void unregisterCallback(IDebugLogServiceCallback cb) {
if (cb != null)
mCallbacks.unregister(cb);
}
public static class Client {
public interface Callback {
public void onStarted(List<String> lostList);
public void onStopped();
public void onLog(String msg);
public void onSaved(boolean success, String path);
}
private boolean mBound = false;
private final Context mContext;
private Callback mCallback;
private IDebugLogService mIDebugLogService;
private Handler mHandler;
private final IDebugLogServiceCallback.Stub mICallback = new IDebugLogServiceCallback.Stub() {
@Override
public void onStopped() throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onStopped();
}
});
}
@Override
public void onStarted(final List<String> logList) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onStarted(logList);
}
});
}
@Override
public void onLog(final String msg) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onLog(msg);
}
});
}
@Override
public void onSaved(final boolean success, final String path) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onSaved(success, path);
}
});
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
mIDebugLogService = IDebugLogService.Stub.asInterface(service);
try {
mIDebugLogService.registerCallback(mICallback);
} catch (RemoteException e) {
release();
mContext.stopService(new Intent(mContext, DebugLogService.class));
mCallback.onStopped();
}
}
}
@Override