org.anothermonitor.ServiceReader.java Source code

Java tutorial

Introduction

Here is the source code for org.anothermonitor.ServiceReader.java

Source

/* 
 * 2010-2015 (C) Antonio Redondo
 * http://antonioredondo.com
 * https://github.com/AntonioRedondo/AnotherMonitor
 *
 * Code under the terms of the GNU General Public License v3.
 *
 */

package org.anothermonitor;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder;
import android.os.Debug;
import android.os.Debug.MemoryInfo;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.widget.Toast;

import com.kotemana.cpumemmonitor.R;

public class ServiceReader extends Service {

    private boolean /*threadSuspended, */ recording, firstRead = true, topRow = true;
    private int memTotal, pId, intervalRead, intervalUpdate, intervalWidth, maxSamples = 2000;
    private long workT, totalT, workAMT, total, totalBefore, work, workBefore, workAM, workAMBefore;
    private String s;
    private String[] sa;
    private List<Float> cpuTotal, cpuAM;
    private List<Integer> memoryAM;
    private List<Map<String, Object>> mListSelected; // Integer       C.pId
    // String       C.pName
    // Integer    C.work
    // Integer    C.workBefore
    // List<Sring> C.finalValue
    private List<String> memUsed, memAvailable, memFree, cached, threshold;
    private ActivityManager am;
    private Debug.MemoryInfo[] amMI;
    private ActivityManager.MemoryInfo mi;
    private NotificationManager mNM;
    private Notification mNotificationRead, mNotificationRecord;
    private BufferedReader reader;
    private BufferedWriter mW;
    private File mFile;
    private SharedPreferences mPrefs;
    private Runnable readRunnable = new Runnable() { // http://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html
        @Override
        public void run() {
            // The service makes use of an explicit Thread instead of a Handler because with the Threat the code is executed more synchronously.
            // However the ViewGraphic is drawed with a Handler because the drawing code must be executed in the UI thread.
            Thread thisThread = Thread.currentThread();
            while (readThread == thisThread) {
                read();
                try {
                    Thread.sleep(intervalRead);
                    /*               synchronized (this) {
                                      while (readThread == thisThread && threadSuspended)
                                         wait();
                                   }*/
                } catch (InterruptedException e) {
                    break;
                }

                // The Runnable can be suspended and resumed with the below code:
                //              threadSuspended = !threadSuspended;
                //              if (!threadSuspended)
                //                  notify();
            }
        }

        /*      public synchronized void stop() {
                 readThread = null;
                 notify();
              }*/

    };
    private volatile Thread readThread = new Thread(readRunnable, C.readThread);
    private BroadcastReceiver receiverStartRecord = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            startRecord();
            sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
        }
    }, receiverStopRecord = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            stopRecord();
            sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
        }
    }, receiverClose = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
            sendBroadcast(new Intent(C.actionFinishActivity));
            stopSelf();
        }
    };

    class ServiceReaderDataBinder extends Binder {
        ServiceReader getService() {
            return ServiceReader.this;
        }
    }

    @Override
    public void onCreate() {
        cpuTotal = new ArrayList<Float>(maxSamples);
        cpuAM = new ArrayList<Float>(maxSamples);
        memoryAM = new ArrayList<Integer>(maxSamples);
        memUsed = new ArrayList<String>(maxSamples);
        memAvailable = new ArrayList<String>(maxSamples);
        memFree = new ArrayList<String>(maxSamples);
        cached = new ArrayList<String>(maxSamples);
        threshold = new ArrayList<String>(maxSamples);

        pId = Process.myPid();

        am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        amMI = am.getProcessMemoryInfo(new int[] { pId });
        mi = new ActivityManager.MemoryInfo();

        mPrefs = getSharedPreferences(getString(R.string.app_name) + C.prefs, MODE_PRIVATE);
        intervalRead = mPrefs.getInt(C.intervalRead, C.defaultIntervalRead);
        intervalUpdate = mPrefs.getInt(C.intervalUpdate, C.defaultIntervalUpdate);
        intervalWidth = mPrefs.getInt(C.intervalWidth, C.defaultIntervalWidth);

        readThread.start();

        //      LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter(Constants.anotherMonitorEvent));
        registerReceiver(receiverStartRecord, new IntentFilter(C.actionStartRecord));
        registerReceiver(receiverStopRecord, new IntentFilter(C.actionStopRecord));
        registerReceiver(receiverClose, new IntentFilter(C.actionClose));

        mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        PendingIntent contentIntent = TaskStackBuilder.create(this)
                //            .addParentStack(ActivityMain.class)
                //            .addNextIntent(new Intent(this, ActivityMain.class))
                .addNextIntentWithParentStack(new Intent(this, ActivityMain.class))
                .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent pIStartRecord = PendingIntent.getBroadcast(this, 0, new Intent(C.actionStartRecord),
                PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent pIStopRecord = PendingIntent.getBroadcast(this, 0, new Intent(C.actionStopRecord),
                PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent pIClose = PendingIntent.getBroadcast(this, 0, new Intent(C.actionClose),
                PendingIntent.FLAG_UPDATE_CURRENT);

        mNotificationRead = new NotificationCompat.Builder(this).setContentTitle(getString(R.string.app_name))
                .setContentText(getString(R.string.notify_read2))
                //            .setTicker(getString(R.string.notify_read))
                .setSmallIcon(R.drawable.icon_bw)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon, null)).setWhen(0) // Removes the time
                .setOngoing(true).setContentIntent(contentIntent) // PendingIntent.getActivity(this, 0, new Intent(this, ActivityMain.class), 0)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notify_read2)))
                .addAction(R.drawable.icon_circle_sb, getString(R.string.menu_record), pIStartRecord)
                .addAction(R.drawable.icon_times_ai, getString(R.string.menu_close), pIClose).build();

        mNotificationRecord = new NotificationCompat.Builder(this).setContentTitle(getString(R.string.app_name))
                .setContentText(getString(R.string.notify_record2)).setTicker(getString(R.string.notify_record))
                .setSmallIcon(R.drawable.icon_recording_bw)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon_recording, null))
                .setWhen(0).setOngoing(true).setContentIntent(contentIntent)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notify_record2)))
                .addAction(R.drawable.icon_stop_sb, getString(R.string.menu_stop_record), pIStopRecord)
                .addAction(R.drawable.icon_times_ai, getString(R.string.menu_close), pIClose).build();

        //      mNM.notify(0, mNotificationRead);
        startForeground(10, mNotificationRead); // If not the AM service will be killed easily when a heavy-use memory app (like a browser or Google Maps) goes in the foreground
    }

    @Override
    public void onDestroy() {
        if (recording)
            stopRecord();
        mNM.cancelAll();

        unregisterReceiver(receiverStartRecord);
        unregisterReceiver(receiverStopRecord);
        unregisterReceiver(receiverClose);

        try {
            readThread.interrupt();
        } catch (Exception e) {
            e.printStackTrace();
        }
        synchronized (this) {
            readThread = null;
            notify();
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new ServiceReaderDataBinder();
    }

    @SuppressLint("NewApi")
    @SuppressWarnings("unchecked")
    private void read() {
        try {
            reader = new BufferedReader(new FileReader("/proc/meminfo"));
            s = reader.readLine();
            while (s != null) {
                // Memory is limited as far as we know
                while (memFree.size() >= maxSamples) {
                    cpuTotal.remove(cpuTotal.size() - 1);
                    cpuAM.remove(cpuAM.size() - 1);
                    memoryAM.remove(memoryAM.size() - 1);

                    memUsed.remove(memUsed.size() - 1);
                    memAvailable.remove(memAvailable.size() - 1);
                    memFree.remove(memFree.size() - 1);
                    cached.remove(cached.size() - 1);
                    threshold.remove(threshold.size() - 1);
                }
                if (mListSelected != null && !mListSelected.isEmpty()) {
                    List<Integer> l = (List<Integer>) ((Map<String, Object>) mListSelected.get(0))
                            .get(C.pFinalValue);
                    if (l != null && l.size() >= maxSamples)
                        for (Map<String, Object> m : mListSelected) {
                            ((List<Integer>) m.get(C.pFinalValue)).remove(l.size() - 1);
                            ((List<Integer>) m.get(C.pTPD)).remove(((List<Integer>) m.get(C.pTPD)).size() - 1);
                        }
                }
                if (mListSelected != null && !mListSelected.isEmpty()) {
                    for (Map<String, Object> m : mListSelected) {
                        List<Integer> l = (List<Integer>) m.get(C.pFinalValue);
                        if (l == null)
                            break;
                        while (l.size() >= maxSamples)
                            l.remove(l.size() - 1);
                        l = (List<Integer>) m.get(C.pTPD);
                        while (l.size() >= maxSamples)
                            l.remove(l.size() - 1);
                    }
                }

                // Memory values. Percentages are calculated in the ActivityMain class.
                if (firstRead && s.startsWith("MemTotal:")) {
                    memTotal = Integer.parseInt(s.split("[ ]+", 3)[1]);
                    firstRead = false;
                } else if (s.startsWith("MemFree:"))
                    memFree.add(0, s.split("[ ]+", 3)[1]);
                else if (s.startsWith("Cached:"))
                    cached.add(0, s.split("[ ]+", 3)[1]);

                s = reader.readLine();
            }
            reader.close();

            // http://stackoverflow.com/questions/3170691/how-to-get-current-memory-usage-in-android
            am.getMemoryInfo(mi);
            if (mi == null) { // Sometimes mi is null
                memUsed.add(0, String.valueOf(0));
                memAvailable.add(0, String.valueOf(0));
                threshold.add(0, String.valueOf(0));
            } else {
                memUsed.add(0, String.valueOf(memTotal - mi.availMem / 1024));
                memAvailable.add(0, String.valueOf(mi.availMem / 1024));
                threshold.add(0, String.valueOf(mi.threshold / 1024));
            }

            memoryAM.add(amMI[0].getTotalPrivateDirty());
            //         Log.d("TotalPrivateDirty", String.valueOf(amMI[0].getTotalPrivateDirty()));
            //         Log.d("TotalPrivateClean", String.valueOf(amMI[0].getTotalPrivateClean()));
            //         Log.d("TotalPss", String.valueOf(amMI[0].getTotalPss()));
            //         Log.d("TotalSharedDirty", String.valueOf(amMI[0].getTotalSharedDirty()));

            //         CPU usage percents calculation. It is possible negative values or values higher than 100% may appear.
            //         http://stackoverflow.com/questions/1420426
            //         http://kernel.org/doc/Documentation/filesystems/proc.txt
            reader = new BufferedReader(new FileReader("/proc/stat"));
            sa = reader.readLine().split("[ ]+", 9);
            work = Long.parseLong(sa[1]) + Long.parseLong(sa[2]) + Long.parseLong(sa[3]);
            total = work + Long.parseLong(sa[4]) + Long.parseLong(sa[5]) + Long.parseLong(sa[6])
                    + Long.parseLong(sa[7]);
            reader.close();

            reader = new BufferedReader(new FileReader("/proc/" + pId + "/stat"));
            sa = reader.readLine().split("[ ]+", 18);
            workAM = Long.parseLong(sa[13]) + Long.parseLong(sa[14]) + Long.parseLong(sa[15])
                    + Long.parseLong(sa[16]);
            reader.close();

            if (mListSelected != null && !mListSelected.isEmpty()) {
                int[] arrayPIds = new int[mListSelected.size()];
                synchronized (mListSelected) {
                    int n = 0;
                    for (Map<String, Object> p : mListSelected) {
                        try {
                            if (p.get(C.pDead) == null) {
                                reader = new BufferedReader(new FileReader("/proc/" + p.get(C.pId) + "/stat"));
                                arrayPIds[n] = Integer.valueOf((String) p.get(C.pId));
                                ++n;
                                sa = reader.readLine().split("[ ]+", 18);
                                p.put(C.work, (float) Long.parseLong(sa[13]) + Long.parseLong(sa[14])
                                        + Long.parseLong(sa[15]) + Long.parseLong(sa[16]));
                                reader.close();
                            }
                        } catch (FileNotFoundException e) {
                            p.put(C.pDead, Boolean.TRUE);
                            Intent intent = new Intent(C.actionDeadProcess);
                            intent.putExtra(C.process, (Serializable) p);
                            sendBroadcast(intent);
                        }
                    }
                }

                MemoryInfo[] mip = am.getProcessMemoryInfo(arrayPIds);
                int n = 0;
                for (Map<String, Object> entry : mListSelected) {
                    List<Integer> l = (List<Integer>) entry.get(C.pTPD);
                    if (l == null) {
                        l = new ArrayList<Integer>();
                        entry.put(C.pTPD, l);
                    }
                    if (entry.get(C.pDead) == null) {
                        //                  if (mip[n].getTotalPrivateDirty() !=0
                        //                        && mip[n].getTotalPss() != 0
                        //                        && mip[n].getTotalSharedDirty() != 0) // To avoid dead processes
                        l.add(0, mip[n].getTotalPrivateDirty());
                        ++n;
                    } else
                        l.add(0, 0);
                }
                //            Log.d("MemoryInfo entries", String.valueOf(mip.length));
                //            Log.d("List Selected entries", String.valueOf(mListSelected.size()));

                //            Log.d("TotalSharedClean", String.valueOf(mi[0].getTotalSharedClean()));
                //            Log.d("TotalSharedDirty", String.valueOf(mi[0].getTotalSharedDirty()));
                //            Log.d("TotalPrivateClean", String.valueOf(mi[0].getTotalPrivateClean()));
                //            Log.d("TotalPrivateDirty", String.valueOf(mi[0].getTotalPrivateDirty()));
                //            Log.d("TotalPss", String.valueOf(mi[0].getTotalPss()));
                //            Log.d("Pss", String.valueOf(Debug.getPss()));
                //            Log.d("GlobalAllocSize", String.valueOf(Debug.getGlobalAllocSize()));
                //            Log.d("NativeHeapSize", String.valueOf(Debug.getNativeHeapSize()/1024));
                //            Log.d("NativeHeapAllocatedSize", String.valueOf(Debug.getNativeHeapAllocatedSize()/1024));
            }

            if (totalBefore != 0) {
                totalT = total - totalBefore;
                workT = work - workBefore;
                workAMT = workAM - workAMBefore;

                cpuTotal.add(0, restrictPercentage(workT * 100 / (float) totalT));
                cpuAM.add(0, restrictPercentage(workAMT * 100 / (float) totalT));

                if (mListSelected != null && !mListSelected.isEmpty()) {
                    int workPT = 0;
                    List<Float> l;

                    synchronized (mListSelected) {
                        for (Map<String, Object> p : mListSelected) {
                            if (p.get(C.workBefore) == null)
                                break;
                            l = (List<Float>) p.get(C.pFinalValue);
                            if (l == null) {
                                l = new ArrayList<Float>();
                                p.put(C.pFinalValue, l);
                            }
                            while (l.size() >= maxSamples)
                                l.remove(l.size() - 1);

                            workPT = (int) ((Float) p.get(C.work) - (Float) p.get(C.workBefore));
                            l.add(0, restrictPercentage(workPT * 100 / (float) totalT));
                        }
                    }
                }
            }

            totalBefore = total;
            workBefore = work;
            workAMBefore = workAM;

            if (mListSelected != null && !mListSelected.isEmpty())
                for (Map<String, Object> p : mListSelected)
                    p.put(C.workBefore, p.get(C.work));

            reader.close();

            if (recording)
                record();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private float restrictPercentage(float percentage) {
        if (percentage > 100)
            return 100;
        else if (percentage < 0)
            return 0;
        else
            return percentage;
    }

    @SuppressWarnings("unchecked")
    private void record() {
        if (mW == null) {

            File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monitor");
            dir.mkdirs();
            mFile = new File(dir, new StringBuilder().append(getString(R.string.app_name)).append("Record-")
                    .append(getDate()).append(".csv").toString());

            try {
                mW = new BufferedWriter(new FileWriter(mFile));
            } catch (IOException e) {
                notifyError(e);
                return;
            }
        }

        try {
            if (topRow) {
                StringBuilder sb = new StringBuilder().append(getString(R.string.app_name))
                        .append(" Record,Starting date and time:,").append(getDate())
                        .append(",Read interval (ms):,").append(intervalRead).append(",MemTotal (kB),")
                        .append(memTotal).append("\nTotal CPU usage (%),Monitor (Pid ").append(Process.myPid())
                        .append(") CPU usage (%),Monitor Memory (kB)");
                if (mListSelected != null && !mListSelected.isEmpty())
                    for (Map<String, Object> p : mListSelected)
                        sb.append(",").append(p.get(C.pAppName)).append(" (Pid ").append(p.get(C.pId))
                                .append(") CPU usage (%)").append(",").append(p.get(C.pAppName))
                                .append(" Memory (kB)");

                sb.append(
                        ",,Memory used (kB),Memory available (MemFree+Cached) (kB),MemFree (kB),Cached (kB),Threshold (kB)");

                mW.write(sb.toString());
                mNM.notify(10, mNotificationRecord);
                topRow = false;
            }

            StringBuilder sb = new StringBuilder().append("\n").append(cpuTotal.get(0)).append(",")
                    .append(cpuAM.get(0)).append(",").append(memoryAM.get(0));
            if (mListSelected != null && !mListSelected.isEmpty())
                for (Map<String, Object> p : mListSelected) {
                    if (p.get(C.pDead) != null)
                        sb.append(",DEAD,DEAD");
                    else
                        sb.append(",").append(((List<Integer>) p.get(C.pFinalValue)).get(0)).append(",")
                                .append(((List<Integer>) p.get(C.pTPD)).get(0));
                }
            sb.append(",").append(",").append(memUsed.get(0)).append(",").append(memAvailable.get(0)).append(",")
                    .append(memFree.get(0)).append(",").append(cached.get(0)).append(",").append(threshold.get(0));

            mW.write(sb.toString());
        } catch (IOException e) {
            notifyError(e);
            return;
        }
    }

    void stopRecord() {
        recording = false;
        sendBroadcast(new Intent(C.actionSetIconRecord));
        try {
            mW.flush();
            mW.close();
            mW = null;

            // http://stackoverflow.com/questions/13737261/nexus-4-not-showing-files-via-mtp
            //         MediaScannerConnection.scanFile(this, new String[] { mFile.getAbsolutePath() }, null, null);
            // http://stackoverflow.com/questions/5739140/mediascannerconnection-produces-android-app-serviceconnectionleaked
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).setData(Uri.fromFile(mFile)));

            Toast.makeText(this,
                    new StringBuilder().append(getString(R.string.app_name)).append("Record-").append(getDate())
                            .append(".csv ").append(getString(R.string.notify_toast_saved)),
                    Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, getString(R.string.notify_toast_error) + " " + e.getMessage(), Toast.LENGTH_LONG)
                    .show();
        }
        topRow = true;
        mNM.notify(10, mNotificationRead);
    }

    void notifyError(final IOException e) {
        e.printStackTrace();
        if (mW != null)
            stopRecord();
        else {
            recording = false;
            sendBroadcast(new Intent(C.actionSetIconRecord));

            // http://stackoverflow.com/questions/3875184/cant-create-handler-inside-thread-that-has-not-called-looper-prepare
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(ServiceReader.this,
                            getString(R.string.notify_toast_error_2) + " " + e.getMessage(), Toast.LENGTH_LONG)
                            .show();
                }
            });

            mNM.notify(10, mNotificationRead);
        }
    }

    private String getDate() {
        Calendar c = Calendar.getInstance();
        DecimalFormat df = new DecimalFormat("00");
        return new StringBuilder().append(df.format(c.get(Calendar.YEAR))).append("-")
                .append(df.format(c.get(Calendar.MONTH) + 1)).append("-").append(df.format(c.get(Calendar.DATE)))
                .append("-").append(df.format(c.get(Calendar.HOUR_OF_DAY))).append("-")
                .append(df.format(c.get(Calendar.MINUTE))).append("-").append(df.format(c.get(Calendar.SECOND)))
                .toString();
    }

    void setIntervals(int intervalRead, int intervalUpdate, int intervalWidth) {
        this.intervalRead = intervalRead;
        this.intervalUpdate = intervalUpdate;
        this.intervalWidth = intervalWidth;
    }

    List<Map<String, Object>> getProcesses() {
        return mListSelected != null && !mListSelected.isEmpty() ? mListSelected : null;
    }

    void addProcess(Map<String, Object> process) {
        // Integer      C.pId
        // String      C.pName
        // Integer      C.work
        // Integer      C.workBefore
        // List<Sring> C.finalValue
        if (mListSelected == null)
            mListSelected = Collections.synchronizedList(new ArrayList<Map<String, Object>>());
        mListSelected.add(process);
    }

    void removeProcess(Map<String, Object> process) {
        synchronized (mListSelected) {
            Iterator<Map<String, Object>> i = mListSelected.iterator();
            while (i.hasNext())
                if (i.next().get(C.pId).equals(process.get(C.pId))) {
                    i.remove();
                    Log.i(getString(R.string.w_processes_dead_notification), (String) process.get(C.pName));
                }
        }
    }

    void startRecord() {
        recording = true;
        sendBroadcast(new Intent(C.actionSetIconRecord));
    }

    boolean isRecording() {
        return recording;
    }

    int getIntervalRead() {
        return intervalRead;
    }

    int getIntervalUpdate() {
        return intervalUpdate;
    }

    int getIntervalWidth() {
        return intervalWidth;
    }

    List<Float> getCPUTotalP() {
        return cpuTotal;
    }

    List<Float> getCPUAMP() {
        return cpuAM;
    }

    List<Integer> getMemoryAM() {
        return memoryAM;
    }

    int getMemTotal() {
        return memTotal;
    }

    List<String> getMemUsed() {
        return memUsed;
    }

    List<String> getMemAvailable() {
        return memAvailable;
    }

    List<String> getMemFree() {
        return memFree;
    }

    List<String> getCached() {
        return cached;
    }

    List<String> getThreshold() {
        return threshold;
    }
}