Results 1 to 3 of 3
  1. #1
    Join Date
    Oct 2005
    Seattle, WA

    Peer code review

    Well I am not sure if I would call it a "peer" code review. I have been using my C++ skills to hack my way through Java and the Android SDK. I am looking for some nice person with a bit of free time and a little more knowledge of Java and the Android SDK than I do. I have made a small app, and would like someone to tear it apart. I would like to know it there are any do's and don'ts particular to Java that I am not aware of.

    The app is fairly straight forward. It is designed to let you test various filters on the accelerometer events. On some devices the accelerometer will constantly send events even while the device is motionless. This can make it difficult to determine if the device is truly motionless. This simple app allows you to try different filtering technique to compensate for the jitter. The data is displayed graphically so you can see the effects in real time. It looks sort of like a heart rate monitor for each axis.

    package ae.test.accelerometertester;
    import java.util.LinkedList;
    import android.hardware.Sensor;
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.os.Bundle;
    import android.view.Display;
    import android.view.View;
    import android.view.ViewTreeObserver.OnGlobalLayoutListener;
    import android.view.WindowManager;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemSelectedListener;
    import android.widget.CheckBox;
    import android.widget.EditText;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import android.widget.RadioButton;
    import android.widget.Spinner;
    import android.widget.TextView;
    import android.view.ViewTreeObserver;
    public class MainActivity extends Activity implements OnItemSelectedListener{
        private static final int FILTER_OFF = 0;
        private static final int FILTER_LOW = 1;
        private static final int FILTER_IIR = 2;
        private static final int X_INDEX = 0;
        private static final int Y_INDEX = 1;
        private static final int Z_INDEX = 2;
        // total nanoseconds in 1 second
        private static final long NANOSECOND = 1000000000L;
        // This value is the total time being displayed from across the width of the screen  
        private static final float MAX_TIME_PERIOD = 3.0f;
        // This value is used to amplify the small jitters to make them more visible
        private static final float MAGNITUDE = 100.0F;
        // The top and bottom borders of the bitmap
        private static final int BORDER_HEIGHT = 5;
        // This controls how far from center the filter paths are drawn. A value of 0
        // will draw the filtered paths directly on top of the raw paths.
        private static final int FILTERED_PATH_OFFSET = 10;
        private SensorManager sensorManager;
        private SensorEventListener sensorListener;
        private Sensor accelSensor;
        private RadioButton offRadioButton;
        private RadioButton lowPassRadioButton;
        private RadioButton iirRadioButton;
        private TextView lowPassAlphaTextView;
        private EditText lowPassEditText;
        private TextView iirAlphaTextView;
        private EditText iirEditText;
        private CheckBox avgCheckBox;
        private Spinner avgSpinner;
        private CheckBox fpsCheckBox;
        private Spinner fpsSpinner;
        private int fpsArray[];
        private int fps;
        private CheckBox deadzoneCheckBox;
        private TextView deadzoneTextView;
        private EditText deadzoneEditText;
        private float deadzone;
        private float lowAlpha;
        private float iirAlpha;
        private LinkedList<Data> sensorData;
        private int avgMax;
        private int avgArray[];
        private int currentFilter;
        private long lastEvent;
        private ImageView img;
        private Bitmap bitmap;
        private Canvas canvas;
        private Paint separatorPaint;
        private Paint pathPaint;
        private LinkedList<Data> rawDataList;
        private LinkedList<Data> filteredDataList;
        private int bitmapWidth;
        private int bitmapHeight;
        private float separatorInterval;
        private float centerLineInterval;
        /* class Data
         * Description: This represents the accelerometer event. It contains the x, y, and z
         *                 value of the event as well as the time stamp.
        class Data {
            public float[] data;
            public long timeStamp;
            public Data(float xval, float yval, float zval, long time){
                data = new float[3];
                data[X_INDEX] = xval;
                data[Y_INDEX] = yval;
                data[Z_INDEX] = zval;
                timeStamp = time;
            public Data(Data in){
                data =;
                timeStamp = in.timeStamp;
        public void onCreate(Bundle savedInstanceState) {
            // Get an instance of the WindowManager
            WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
            Display display = windowManager.getDefaultDisplay();
            bitmapWidth = display.getWidth();
            bitmapHeight = 450;
            separatorPaint = new Paint();
            pathPaint = new Paint();
            sensorData = new LinkedList<Data>();
            rawDataList = new LinkedList<Data>();
            filteredDataList = new LinkedList<Data>();        
            lastEvent = 0L;
            // Get an instance of the SensorManager
            sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
            accelSensor  = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            sensorListener = new AccelListener();
        /* Name: findViewControls()
         * Description: Initializes all view controls associated with activity_main.xml
         * Parameters: None
        private void findViewControls(){
            offRadioButton = (RadioButton)findViewById(;
            lowPassRadioButton = (RadioButton)findViewById(;
            iirRadioButton = (RadioButton)findViewById(;
            avgCheckBox = (CheckBox)findViewById(;
            lowPassAlphaTextView = (TextView)findViewById(;
            lowPassEditText = (EditText)findViewById(;
            iirAlphaTextView = (TextView)findViewById(;
            iirEditText = (EditText)findViewById(;
            avgSpinner = (Spinner)findViewById(;
            fpsCheckBox = (CheckBox)findViewById(;
            fpsSpinner = (Spinner)findViewById(;
            deadzoneCheckBox = (CheckBox)findViewById(;
            deadzoneTextView = (TextView)findViewById(;
            deadzoneEditText = (EditText)findViewById(;
        /* Name: initViewControls()
         * Description: Set default values for view controls associated with activity_main.xml
         * Parameters: None
        private void initViewControls(){
            currentFilter = FILTER_OFF;
            lowAlpha = 0.8f;
            iirAlpha = 0.15f;
            int spinnervalues[] = {3, 5, 10, 15, 20};
            avgArray = spinnervalues;
            avgMax = avgArray[1];
            int fpsValues[] = {1, 3, 5, 10, 20, 40};
            fpsArray = fpsValues;
            fps = fpsArray[2];
            deadzone = 0.1f;
        /* Name: createImageView()
         * Description: Creates and initializes the ImageView, Bitmap, and canvas that the 
         *                 accelerometer data is drawn inside. The bitmap height is devices 
         *                 dependent. The bitmap cannot be built until the layout has been
         *                 inflated.  A ViewTreeObserver is used to listen for this event.
         *                 The bitmap is then created and drawn.
         * Parameters: None
        private void createImageView() {
            final LinearLayout layout = (LinearLayout)findViewById(;
            img = new ImageView(this);
            ViewTreeObserver vto = layout.getViewTreeObserver(); 
            vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 
                public void onGlobalLayout() { 
                    bitmapHeight = layout.getMeasuredHeight(); 
                    bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
                    canvas = new Canvas(bitmap);
                    separatorInterval = (float)(bitmapHeight -  (BORDER_HEIGHT*2)) / MAX_TIME_PERIOD;
                    centerLineInterval = separatorInterval / 2.0f;
        /* Name: onResume()
         * Description: Overrides The sensor listener is
         *                 registered here. This allows the app to turn the listener on when
         *                 the app is brought to the front.
         * Parameters: None
        protected void onResume() {
            sensorManager.registerListener(sensorListener, accelSensor, SensorManager.SENSOR_DELAY_UI);
        /* Name: onPause()
         * Description: Overrides The sensor Listener is
         *                 unregistered at this point. This prevents the app from being active
         *                 when it gets moved to the background. 
         * Parameters: None
        protected void onPause() {
        //protected void onSaveInstanceState(Bundle outState){
        //    super.onSaveInstanceState(outState);
        /* Name: onItemSelected()
         * Description: Overrides 
         *                 android.widget.AdapterView.OnItemSelectedListener#onItemSelected().
         *                 Handles spinners selection for avgSpinner and fpsSpinner.
         * Parameters: default (see android documentation)
        public void onItemSelected(AdapterView<?> parent, View v, int position,
          long id) {     
             switch (parent.getId()) {
                 avgMax = avgArray[avgSpinner.getSelectedItemPosition()];
                 fps = fpsArray[fpsSpinner.getSelectedItemPosition()];
        /* Name: onNothingSelected()
         * Description: Overrides 
         *                 android.widget.AdapterView.OnItemSelectedListener#onNothingSelected().
         *                 This must be implemented, however it is not used.
        public void onNothingSelected(AdapterView<?> arg0) {
        /* Name: setButtonClicked()
         * Description: Converts an EditText value into a float and updates a TextView with
         *                 this float value. The values are capped between 0.0f and 1.0f.
         * Parameters: EditText     et    where float value is to be retrieved from
         *                 TexView        tv    where et value is diplayed.  
        private float setButtonClicked(EditText et, TextView tv) {
            String sAlpha = et.getText().toString().trim();
            float alpha = Float.valueOf(sAlpha).floatValue();
            if (alpha < 0.0f) alpha = 0.0f;
            if (alpha > 1.0f) alpha = 1.0f;
            return alpha;
        /* Name: mainClickHandler()
         * Description: Handles all button presses on activity_main.xml
         * Parameters: View     v        the view that was clicked
        public void mainClickHandler(View v){
            switch (v.getId()) {
                lowAlpha = setButtonClicked(lowPassEditText, lowPassAlphaTextView);
                iirAlpha = setButtonClicked(iirEditText, iirAlphaTextView);
                deadzone = setButtonClicked(deadzoneEditText, deadzoneTextView);
        /* Name setRadioButton()
         * Description: Controls switching between the radio buttons. If one is clicked, 
         *                 it will be selected while the others will be unselected.
        private void setRadioButton(int filtertype){
            if (filtertype == FILTER_OFF) {
                currentFilter = FILTER_OFF;
            } else offRadioButton.setChecked(false);
            if (filtertype == FILTER_LOW) {
                currentFilter = FILTER_LOW;
            } else lowPassRadioButton.setChecked(false);
            if (filtertype == FILTER_IIR) {
                currentFilter = FILTER_IIR;
            } else iirRadioButton.setChecked(false);
        /* Name: updateSensorData()
         * Description: Adds new sensor data to sensorData while ensuring that sensorData
         *                 does not grow too large.
         * Parameters: Data     data        New data to be added to sensorData
        private void updateSensorData(Data data) {
            while (sensorData.size() > avgMax) {
        /* Name: deadZone()
         * Description: Processes incoming data using a dead zone filter. If the dead zone
         *                 filter is being used, the delta value is found for each axis of
         *                 inData. If the delta value greater than the dead zone threshold
         *                 then the inData value is passed out, else the previous value is
         *                 passed out. each axis is considered individually.
         * Parameters: Data     inData        data element to be filtered.
        protected Data deadZone(Data inData){
            Data ouput = new Data(inData);
            if (deadzoneCheckBox.isChecked()){
                Data prevData = sensorData.getLast();
                float deltaX = Math.abs([X_INDEX] -[X_INDEX]);
                float deltaY = Math.abs([Y_INDEX] -[Y_INDEX]);
                float deltaZ = Math.abs([Z_INDEX] -[Z_INDEX]);
                if (deltaX < deadzone)[X_INDEX] =[X_INDEX];
                if (deltaY < deadzone)[Y_INDEX] =[Y_INDEX];
                if (deltaZ < deadzone)[Z_INDEX] =[Z_INDEX];
            return ouput;
        /* Name: lowPass()
         * Description: This is a low pass filter. It works by only allowing a value to
         *                 change by a percentage (alpha) of difference between the previous
         *                 value and the new value.
         *                 output = previous  + alpha * (in - previous)
         * Parameters: Data        data    this is the latest sensor value.
        private Data lowPass(Data data){      
            Data prev = sensorData.getLast();
            Data output  = new Data(0.0f, 0.0f, 0.0f, data.timeStamp);
  [X_INDEX] =[X_INDEX] + lowAlpha * ([X_INDEX] -[X_INDEX]);
  [Y_INDEX] =[Y_INDEX] + lowAlpha * ([Y_INDEX] -[Y_INDEX]);
  [Z_INDEX] =[Z_INDEX] + lowAlpha * ([Z_INDEX] -[Z_INDEX]);
            return output;
        /* Name: iirFilter()
         * Description: This is a infinite impulse response filter. It works by combining a 
         *                 percentage (alpha) of previous value with the opposite percentage of
         *                 the new value.  
         *                 output = previous * alpha + (1 - alpha) * in
         * Parameters: Data        data    this is the latest sensor value.
        private Data iirFilter(Data data){
            Data prev = sensorData.getLast();
            Data output  = new Data(0.0f, 0.0f, 0.0f, data.timeStamp);
  [X_INDEX] =[X_INDEX] * iirAlpha + (1.0f - iirAlpha) *[X_INDEX];
  [Y_INDEX] =[Y_INDEX] * iirAlpha + (1.0f - iirAlpha) *[Y_INDEX];
  [Z_INDEX] =[Z_INDEX] * iirAlpha + (1.0f - iirAlpha) *[Z_INDEX];
            return output;
        /* Name: avgFilter()
         * Description: Rolling average filter. This outputs the average value for
         *                 all sensorData elements. The size of sensorData is controlled
         *                 by updataSensorData()
         * Parameters: None
        protected Data avgFilter(){
            Data output  = new Data(0.0f, 0.0f, 0.0f, 0L);
            int count = 0;
            int max = sensorData.size();
            while (count < max) {
                Data next = sensorData.get(count);
      [X_INDEX] +=[X_INDEX];
      [Y_INDEX] +=[Y_INDEX];
      [Z_INDEX] +=[Z_INDEX];
                if (count == max - 1){
                    output.timeStamp = next.timeStamp;
  [X_INDEX] /= (float)max;
  [Y_INDEX] /= (float)max;
  [Z_INDEX] /= (float)max;
            return output;
        /* Name getFiltered()
         * Description: Takes the sensor data and send it to the appropriate filters
         *                 New returns the updated data.
        private Data getFiltered(Data inData){
            Data output = new Data(0.0f, 0.0f, 0.0f, inData.timeStamp);
            if (currentFilter == FILTER_OFF) {
                output = inData;
            if (currentFilter == FILTER_LOW){
                output = lowPass(inData);
            if (currentFilter == FILTER_IIR){
                output = iirFilter(inData);
            return output;
        /* Name: updateSurface()
         * Description: Redraws the sensor data bitmap. RawDataList and FilteredDataList are
         *                 designed to have the same number of elements so the size of one is
         *                 used to draw the paths of both.
         * Parameters: None
        public void updateSurface(){
            if (canvas != null){
                canvas.drawARGB(255, 0, 0, 0);
                int numPaths = rawDataList.size() - 1;
                if (numPaths > 0){
                    int currentPath = 0;
                    while (currentPath < numPaths){
        /* Name: drawCenterLines()
         * Description: Draws three line segments which represent the centerline of each axis.
         *                 The number of data inputs per second is also printed the the upper
         *                 left of the bitmap.
         * Parameters: int        numPaths        this should be the current size of the 
         *                                         rawDataList - 1
        private void drawCenterLines(int numPaths){
            canvas.drawText(Integer.toString(numPaths/(int)MAX_TIME_PERIOD), 10, 60, separatorPaint);
            Path centerLines = new Path();
            centerLines.moveTo(0.0f, centerLineInterval + BORDER_HEIGHT);
            centerLines.lineTo(bitmapWidth, centerLineInterval + BORDER_HEIGHT);
            centerLines.moveTo(0.0f, centerLineInterval + separatorInterval + BORDER_HEIGHT);
            centerLines.lineTo(bitmapWidth, centerLineInterval + separatorInterval + BORDER_HEIGHT);
            centerLines.moveTo(0.0f, centerLineInterval +(separatorInterval*2.0f) + BORDER_HEIGHT);
            centerLines.lineTo(bitmapWidth, centerLineInterval +(separatorInterval*2.0f) + BORDER_HEIGHT);
            canvas.drawPath(centerLines, separatorPaint);
        /* Name: dawAxisSeparators()
         * Description: Draws the lines that separate each axis on the bitmap
         * Parameters: None
        private void drawAxisSeparators(){
            Path separator = new Path();
            separator.moveTo(0.0f, separatorInterval + BORDER_HEIGHT);
            separator.lineTo(bitmapWidth, separatorInterval + BORDER_HEIGHT);
            separator.moveTo(0.0f, (separatorInterval*2.0f) + BORDER_HEIGHT);
            separator.lineTo(bitmapWidth, (separatorInterval*2.0f) + BORDER_HEIGHT);
            canvas.drawPath(separator, separatorPaint);
        /* Name: drawRawPath()
         * Description: Draws a single path segment for each axis in the appropriate color.
         * Parameters: int     currentPath        the next path to be drawn
        private void drawRawPath(int currentPath){
            Path path = getPath(rawDataList, currentPath, X_INDEX, 0);
            if (path != null){
                canvas.drawPath(path, pathPaint);
            path = getPath(rawDataList, currentPath, Y_INDEX, 0);
            if (path != null){
                canvas.drawPath(path, pathPaint);
            path = getPath(rawDataList, currentPath, Z_INDEX, 0);
            if (path != null){
                canvas.drawPath(path, pathPaint);
        /* Name: drawFilteredPath()
         * Description: Draws a single path segment for each axis in the appropriate color.
         *                 The path will only be drawn if one or more of the filters have been
         *                 activated.
         * Parameters: int     currentPath        the next path to be drawn
        private void drawFilteredPath(int currentPath){
            if (currentFilter != FILTER_OFF || deadzoneCheckBox.isChecked() || 
                    fpsCheckBox.isChecked() || avgCheckBox.isChecked()){
                Path path = getPath(filteredDataList, currentPath, X_INDEX, FILTERED_PATH_OFFSET);
                if (path != null){
                    canvas.drawPath(path, pathPaint);
                path = getPath(filteredDataList, currentPath, Y_INDEX, FILTERED_PATH_OFFSET);
                if (path != null){
                    canvas.drawPath(path, pathPaint);
                path = getPath(filteredDataList, currentPath, Z_INDEX, FILTERED_PATH_OFFSET);
                if (path != null){
                    canvas.drawPath(path, pathPaint);
        /* Name: validateTimeFrame()
         * Description: The maximum time frame to be recorded is set by MAX_TIME_PERIOD.
         *                 rawDataList and filteredDataList are FIFO list. If the total 
         *                 time represented by these list exceeds the maximum, then elements
         *                 are removed one at a time from each list until the time frames 
         *                 are within the maximum.
         * Parameters: None
        private void validateTimeFrame(){
            long timeFrame = rawDataList.getLast().timeStamp - rawDataList.getFirst().timeStamp;
            while (timeFrame > NANOSECOND * (long)MAX_TIME_PERIOD ){
                timeFrame = rawDataList.getLast().timeStamp - rawDataList.getFirst().timeStamp;
        /* Name: getPath()
         * Description: returns a path between the two points represented by list[pathIndex]
         *                 and list[pathIndex + 1].
         * Parameters: LinkedList<Data>     list    the list to be processed
         *                 int        pathIndex            the first point of the path
         *                 int        axis                value between 0-2 representing the x, y,
         *                                             or z axis
        private Path getPath(LinkedList<Data> list, int pathIndex, int axis, int offset){
            Path path = null;
            if (pathIndex < list.size() - 1) {
                Data point1 = list.get(pathIndex);
                Data point2 = list.get(pathIndex + 1);
                path = new Path();
                long startTime = list.getFirst().timeStamp;
                long startDelta = point1.timeStamp - startTime;
                float pixelsPerSecond = bitmapWidth / MAX_TIME_PERIOD;
                float xPixelsPerNano = pixelsPerSecond / NANOSECOND;
                float xpos = (float)startDelta * xPixelsPerNano;
                float yDelta =[axis];
                if (axis == Z_INDEX){
                    yDelta -= SensorManager.GRAVITY_EARTH;
                yDelta *= MAGNITUDE;
                if (yDelta > centerLineInterval) yDelta = centerLineInterval;
                if (yDelta < -centerLineInterval) yDelta = -centerLineInterval;
                float ypos = BORDER_HEIGHT + offset + centerLineInterval + (separatorInterval * axis) + yDelta;
                long moveDelta = point2.timeStamp - startTime;
                float xmove = (float)moveDelta * xPixelsPerNano;
                float ymoveDelta =[axis];
                if (axis == Z_INDEX){
                    ymoveDelta -= SensorManager.GRAVITY_EARTH;
                ymoveDelta *= MAGNITUDE;
                if (ymoveDelta > centerLineInterval) ymoveDelta = centerLineInterval;
                if (ymoveDelta < -centerLineInterval) ymoveDelta = -centerLineInterval;
                float ymove = BORDER_HEIGHT + offset + centerLineInterval + (separatorInterval * axis) + ymoveDelta;
                path.moveTo(xpos, ypos);
                path.lineTo(xmove, ymove);
            return path;
        /* class AccelListener
         * Description: This class processes the accelerometer events
        class AccelListener implements SensorEventListener {
            /* Name: onSensorChanged()
             * Description: Overrides 
             *                 android.hardware.SensorEventListener#onSensorChanged().
             *                 Each new event is added to rawDataList. It is then 
             *                 processed by any filter that has been activated.
             * Parameters: SensorEvent    event    a new sensor event
            public void onSensorChanged(SensorEvent event) {
                if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER)
                Data rawData = new Data(event.values[X_INDEX], event.values[Y_INDEX], event.values[Z_INDEX], event.timestamp);
                Data filteredData = new Data(event.values[X_INDEX], event.values[Y_INDEX], event.values[Z_INDEX], event.timestamp);
            /* Name processFilterData()
             * Description: processes data from latest event through any filter that has 
             *                 been selected. The filters are applied in this order.
             *                     1. fps
             *                     2. dead zone
             *                     3. low pass / iir
             *                     4. rolling average
            private Data processFilteredData(Data filteredData){ 
                if (!fpsCheckBox.isChecked()){
                    filteredData = deadZone(filteredData);
                    filteredData = getFiltered(filteredData);
                    if (avgCheckBox.isChecked()){
                        filteredData = avgFilter();
                    lastEvent = filteredData.timeStamp;
                else {
                    long deltaTime = filteredData.timeStamp - lastEvent;
                    long timeInterval = NANOSECOND / (long)fps;
                    if (deltaTime > timeInterval){
                        filteredData = deadZone(filteredData);
                        filteredData = getFiltered(filteredData);
                        if (avgCheckBox.isChecked()){
                            filteredData = avgFilter();
                        lastEvent = filteredData.timeStamp;
                    else {
                        long timeStamp = filteredData.timeStamp;
                        filteredData = new Data(filteredDataList.getLast());
                        filteredData.timeStamp = timeStamp;
                return filteredData;
            /* Name onAccuracyChanged()
             * Description: Overrides 
             *                 android.hardware.SensorEventListener#onAccuracyChanged().
             *                 This is a required implementation, however it is not used
             * Parameters: default (see android documentation)
            public void onAccuracyChanged(Sensor sensor, int accuracy) {
    Attached Thumbnails Attached Thumbnails Click image for larger version. 

Name:	acceltester01.jpg 
Views:	619 
Size:	42.7 KB 
ID:	73503  

  2. #2
    Join Date
    Feb 2012
    Hello, I am a Java programer for like 10 years, so quite experienced, but not with the Android SDK.
    By looking at the code I can tell you only one thing - you are not doing objective programing, just functional.
    You have one huge class that does a ton of stuff in a lot of functions.
    Don't get me wrong, that is mistake even many senior java programers do and your code is not bad at all.
    All I say, you should spend more time on class design.
    When creating a java application, you must somehow decide in what classes to put data and logic. If you use just functions in a signle class for entire logic, then it is not OOP. Think in application layers and separate code by them - some classes will just display data, some will just create them (irrelevant of HOW it will be presented). Then think about the domain layer - that's where your main logic is. What classes are there and what behaviour they have? How do they relate together?

    If you need enlightment, go for Domain Driven Development book by Martin Fowler, 2004. I read it recently and I am sorry I did not do earlier.

  3. #3
    Join Date
    Oct 2005
    Seattle, WA
    Thanks for the feedback! I would not call myself an experienced OOP programmer, so I appreciate the advice and insight. I will check out the book too.

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts