[Android] [PATCH 2/3] DebugLog: use a service with a thread

Thomas Guillem thomas at gllm.fr
Mon Feb 9 19:31:56 CET 2015


- 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.

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

- There is a notification to notify that logs are being recorded, and to access
  the logs.
---
 vlc-android/AndroidManifest.xml                    |   6 +-
 vlc-android/res/layout/debug_log.xml               |  11 +-
 vlc-android/res/layout/debug_log_item.xml          |   5 +
 vlc-android/res/values/strings.xml                 |   2 +
 .../src/org/videolan/vlc/gui/DebugLogActivity.java | 161 +++++++----
 .../src/org/videolan/vlc/gui/DebugLogService.java  | 297 +++++++++++++++++++++
 .../src/org/videolan/vlc/gui/IDebugLogService.aidl |  32 +++
 .../videolan/vlc/gui/IDebugLogServiceCallback.aidl |  28 ++
 8 files changed, 483 insertions(+), 59 deletions(-)
 create mode 100644 vlc-android/res/layout/debug_log_item.xml
 create mode 100644 vlc-android/src/org/videolan/vlc/gui/DebugLogService.java
 create mode 100644 vlc-android/src/org/videolan/vlc/gui/IDebugLogService.aidl
 create mode 100644 vlc-android/src/org/videolan/vlc/gui/IDebugLogServiceCallback.aidl

diff --git a/vlc-android/AndroidManifest.xml b/vlc-android/AndroidManifest.xml
index bc5de67..59b5241 100644
--- a/vlc-android/AndroidManifest.xml
+++ b/vlc-android/AndroidManifest.xml
@@ -61,7 +61,11 @@
             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" />
+
         <activity
             android:name=".gui.NativeCrashActivity"
             android:process=":NativeCrashActivity"
diff --git a/vlc-android/res/layout/debug_log.xml b/vlc-android/res/layout/debug_log.xml
index eaf27a7..071d0d9 100644
--- a/vlc-android/res/layout/debug_log.xml
+++ b/vlc-android/res/layout/debug_log.xml
@@ -36,11 +36,10 @@
         android:enabled="false"
         android:text="@string/copy_to_clipboard" />
 
-    <TextView
-        android:id="@+id/textview"
+    <ListView
+        android:id="@+id/log_list"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:scrollbars="vertical"
-        android:textIsSelectable="true" />
+        android:layout_height="wrap_content"
+        android:paddingTop="10dip" />
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/vlc-android/res/layout/debug_log_item.xml b/vlc-android/res/layout/debug_log_item.xml
new file mode 100644
index 0000000..e6e74c9
--- /dev/null
+++ b/vlc-android/res/layout/debug_log_item.xml
@@ -0,0 +1,5 @@
+<?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" />
diff --git a/vlc-android/res/values/strings.xml b/vlc-android/res/values/strings.xml
index 666b1b6..f485878 100644
--- a/vlc-android/res/values/strings.xml
+++ b/vlc-android/res/values/strings.xml
@@ -291,6 +291,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\'s logs are being recorded</string>
+    <string name="log_service_text">Click to view the logs</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>
diff --git a/vlc-android/src/org/videolan/vlc/gui/DebugLogActivity.java b/vlc-android/src/org/videolan/vlc/gui/DebugLogActivity.java
index c3c1586..33fc037 100644
--- a/vlc-android/src/org/videolan/vlc/gui/DebugLogActivity.java
+++ b/vlc-android/src/org/videolan/vlc/gui/DebugLogActivity.java
@@ -1,7 +1,7 @@
 /*****************************************************************************
  * 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,142 @@
  *****************************************************************************/
 package org.videolan.vlc.gui;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+
 import org.videolan.libvlc.LibVLC;
 import org.videolan.libvlc.LibVlcException;
 import org.videolan.vlc.R;
+import org.videolan.vlc.util.Logcat;
 import org.videolan.vlc.util.VLCInstance;
 
 import android.app.Activity;
 import android.os.Bundle;
 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 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);
 
-        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);
+        mCopyButton.setEnabled(false);
+        mClearButton.setEnabled(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);
 
-        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 View.OnClickListener mClearClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mClient.clear();
+            if (mLogList != null) {
+                mLogList.clear();
+                mLogAdapter.notifyDataSetChanged();
+            }
+            mClearButton.setEnabled(false);
+            mCopyButton.setEnabled(false);
+        }
+    };
+
     @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(String[] logList) {
+        mStartButton.setEnabled(false);
+        mStopButton.setEnabled(true);
+        if (logList.length > 0) {
+            mCopyButton.setEnabled(true);
+            mClearButton.setEnabled(true);
+        }
+        mLogList = new ArrayList<String>(Arrays.asList(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);
+        }
     }
 
-    private void updateTextView(final LibVLC instance) {
-        TextView textView = (TextView)findViewById(R.id.textview);
-        textView.setText(instance.getBufferContent());
+    @Override
+    public void onStopped() {
+        mStartButton.setEnabled(true);
+        mStopButton.setEnabled(false);
+    }
+
+    @Override
+    public void onLog(String msg) {
+        if (mLogList != null) {
+            mLogList.add(msg);
+            mLogAdapter.notifyDataSetChanged();
+            mCopyButton.setEnabled(true);
+            mClearButton.setEnabled(true);
+        }
     }
 }
diff --git a/vlc-android/src/org/videolan/vlc/gui/DebugLogService.java b/vlc-android/src/org/videolan/vlc/gui/DebugLogService.java
new file mode 100644
index 0000000..bc9548e
--- /dev/null
+++ b/vlc-android/src/org/videolan/vlc/gui/DebugLogService.java
@@ -0,0 +1,297 @@
+/*****************************************************************************
+ * 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.util.ArrayList;
+
+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.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.support.v4.app.NotificationCompat;
+
+public class DebugLogService extends Service implements Logcat.Callback {
+
+    private static final int MSG_STARTED = 0;
+    private static final int MSG_STOPPED = 1;
+    private static final int MSG_ONLOG = 2;
+
+    private Logcat mLogcat = null;
+    private ArrayList<String> mLogList = new ArrayList<String>();
+    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 registerCallback(IDebugLogServiceCallback cb) {
+            mService.registerCallback(cb);
+        }
+        public void unregisterCallback(IDebugLogServiceCallback cb) {
+            mService.unregisterCallback(cb);
+        }
+    }
+
+    private synchronized void sendMessage(int what, String log) {
+        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: {
+                    String[] logs = mLogList.toArray(new String[mLogList.size()]);
+                    cb.onStarted(logs);
+                    break;
+                } case MSG_ONLOG:
+                    cb.onLog(log);
+                    break;
+                }
+            } catch (RemoteException e) {
+            }
+        }
+        mCallbacks.finishBroadcast();
+    }
+
+    @Override
+    public synchronized void onLog(String log) {
+        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();
+    }
+
+    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(String[] lostList);
+            public void onStopped();
+            public void onLog(String msg);
+        }
+
+        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 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);
+                    }
+                });
+            }
+        };
+
+        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
+            public void onServiceDisconnected(ComponentName name) {
+                release();
+                mContext.stopService(new Intent(mContext, DebugLogService.class));
+                mCallback.onStopped();
+            }
+        };
+
+        public Client(Context context, Callback cb) throws IllegalArgumentException {
+            if (context == null | cb == null)
+                throw new IllegalArgumentException("Context and Callback can't be null");
+
+            mContext = context;
+            mCallback = cb;
+            mHandler = new Handler(Looper.getMainLooper());
+            mBound = mContext.bindService(new Intent(mContext, DebugLogService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
+        }
+
+        public boolean start() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.start();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+
+        public boolean stop() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.stop();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+
+        public boolean clear() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.clear();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+
+        public void release() {
+            if (mBound) {
+                synchronized (this) {
+                    if (mIDebugLogService != null && mICallback != null) {
+                        try {
+                            mIDebugLogService.unregisterCallback(mICallback);
+                        } catch (RemoteException e) {
+                        }
+                        mIDebugLogService = null;
+                    }
+                }
+                mBound = false;
+                mContext.unbindService(mServiceConnection);
+            }
+            mHandler.removeCallbacksAndMessages(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/vlc-android/src/org/videolan/vlc/gui/IDebugLogService.aidl b/vlc-android/src/org/videolan/vlc/gui/IDebugLogService.aidl
new file mode 100644
index 0000000..6bd34d9
--- /dev/null
+++ b/vlc-android/src/org/videolan/vlc/gui/IDebugLogService.aidl
@@ -0,0 +1,32 @@
+/*****************************************************************************
+ * IDebugLogService.aidl
+ *****************************************************************************
+ * 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 org.videolan.vlc.gui.IDebugLogServiceCallback;
+
+interface IDebugLogService
+{
+    void start();
+    void stop();
+    void clear();
+    void registerCallback(IDebugLogServiceCallback cb);
+    void unregisterCallback(IDebugLogServiceCallback cb);
+}
+
diff --git a/vlc-android/src/org/videolan/vlc/gui/IDebugLogServiceCallback.aidl b/vlc-android/src/org/videolan/vlc/gui/IDebugLogServiceCallback.aidl
new file mode 100644
index 0000000..60ad9df
--- /dev/null
+++ b/vlc-android/src/org/videolan/vlc/gui/IDebugLogServiceCallback.aidl
@@ -0,0 +1,28 @@
+/*****************************************************************************
+ * IDebugLogServiceCallback.aidl
+ *****************************************************************************
+ * 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;
+
+interface IDebugLogServiceCallback
+{
+    void onStarted(out String[] logList);
+    void onStopped();
+    void onLog(String msg);
+}
-- 
2.1.3



More information about the Android mailing list