~williamvds/taskwarrior-android

5f209c8dacb99c7075bae907dadb0dc07050da22 — williamvds 4 years ago 99840de
Replace bravo7.log.Logger with Timber
M app/build.gradle => app/build.gradle +1 -0
@@ 35,6 35,7 @@ androidExtensions {

dependencies {
    //    compile fileTree(dir: 'libs', include: ['*.jar'])
    implementation "com.jakewharton.timber:timber:${versions.timber}"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
    implementation "com.android.support:appcompat-v7:${versions.support}"
    implementation "com.android.support:design:${versions.support}"

M app/src/main/java/kvj/taskw/App.java => app/src/main/java/kvj/taskw/App.java +8 -0
@@ 4,6 4,7 @@ package kvj.taskw;
import android.accounts.Account;

import kvj.taskw.data.Controller;
import timber.log.Timber;

/**
 * Created by vorobyev on 10/4/15.


@@ 64,6 65,13 @@ public class App extends org.kvj.bravo7.ng.App<Controller> {

    @Override
    protected void init() {
        Timber.plant(new Timber.DebugTree() {
            @Override
            protected String createStackElementTag(StackTraceElement element) {
                return super.createStackElementTag(element) + ":" + element.getLineNumber();
            }
        });

        Controller controller = App.controller();
        for (Account acc : controller.accounts()) {
            controller.accountController(controller.accountID(acc)); // This will schedule sync

M app/src/main/java/kvj/taskw/account/AccountAuthenticator.java => app/src/main/java/kvj/taskw/account/AccountAuthenticator.java +8 -8
@@ 18,7 18,8 @@ import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.ViewFinder;
import org.kvj.bravo7.form.impl.widget.SpinnerIntegerAdapter;
import org.kvj.bravo7.form.impl.widget.TextViewCharSequenceAdapter;
import org.kvj.bravo7.log.Logger;

import timber.log.Timber;

import java.util.List;



@@ 33,7 34,6 @@ import kvj.taskw.ui.AppDialog;
public class AccountAuthenticator extends AbstractAccountAuthenticator {

    private final Context context;
    Logger logger = Logger.forInstance(this);

    public static class Service extends android.app.Service {



@@ 51,7 51,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
        logger.d("editProperties", accountType, response);
        Timber.d("editProperties %s %s", accountType, response);
        return null;
    }



@@ 59,7 59,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
    public Bundle addAccount(final AccountAuthenticatorResponse response, String accountType,
                             String authTokenType, String[] requiredFeatures, Bundle options)
        throws NetworkErrorException {
        logger.d("addAccount", accountType, authTokenType, options, response);
        Timber.d("addAccount %s %s %s %s", accountType, authTokenType, options, response);
        Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, new Intent(context, AccountAddDialog.class));
        return bundle;


@@ 68,14 68,14 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
                                     Bundle options) throws NetworkErrorException {
        logger.d("confirmCredentials", account, options, response);
        Timber.d("confirmCredentials %s %s %s", account, options, response);
        return null;
    }

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
                               String authTokenType, Bundle options) throws NetworkErrorException {
        logger.d("getAuthToken", account, authTokenType, options, response);
        Timber.d("getAuthToken %s %s %s %s", account, authTokenType, options, response);
        return null;
    }



@@ 88,14 88,14 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
                                    String authTokenType, Bundle options)
        throws NetworkErrorException {
        logger.d("updateCredentials", account, authTokenType, options, response);
        Timber.d("updateCredentials %s %s %s %s", account, authTokenType, options, response);
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
                              String[] features) throws NetworkErrorException {
        logger.d("hasFeatures", account, response);
        Timber.d("hasFeatures %s %s", account, response);
        return null;
    }


M app/src/main/java/kvj/taskw/data/AccountController.java => app/src/main/java/kvj/taskw/data/AccountController.java +37 -46
@@ 11,7 11,6 @@ import android.text.TextUtils;
import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;
import org.kvj.bravo7.log.Logger;
import org.kvj.bravo7.util.Compat;
import org.kvj.bravo7.util.DataUtil;
import org.kvj.bravo7.util.Listeners;


@@ 42,6 41,8 @@ import java.util.regex.Pattern;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import timber.log.Timber;

import kvj.taskw.App;
import kvj.taskw.sync.SSLHelper;
import kvj.taskw.ui.MainListAdapter;


@@ 185,8 186,6 @@ public class AccountController {
    private boolean active = false;
    private final String socketName;

    Logger logger = Logger.forInstance(this);

    private final LocalServerSocket syncSocket;
    private final File tasksFolder;



@@ 194,24 193,18 @@ public class AccountController {
        public void eat(String line);
    }

    private class ToLogConsumer implements StreamConsumer {

        private final Logger.LoggerLevel level;
        private final String prefix;

        private ToLogConsumer(Logger.LoggerLevel level, String prefix) {
            this.level = level;
            this.prefix = prefix;
    private StreamConsumer errConsumer = new StreamConsumer() {
        @Override
        public void eat(String line) {
            Timber.w("error: %s", line);
        }

    };
    private StreamConsumer outConsumer = new StreamConsumer() {
        @Override
        public void eat(String line) {
            logger.log(level, prefix, line);
            Timber.i("output: %s", line);
        }
    }

    private StreamConsumer errConsumer = new ToLogConsumer(Logger.LoggerLevel.Warning, "ERR:");
    private StreamConsumer outConsumer = new ToLogConsumer(Logger.LoggerLevel.Info, "STD:");
    };

    public AccountController(Controller controller, String folder, String name) {
        this.controller = controller;


@@ 318,7 311,7 @@ public class AccountController {
            try {
                syncSocket.close();
            } catch (Exception e) {
                logger.w(e, "Failed to close socket");
                Timber.w(e, "Failed to close socket");
            }
        }
    }


@@ 338,7 331,7 @@ public class AccountController {
            try {
                return Double.parseDouble(config.values().iterator().next());
            } catch (Exception e) {
                c.logger.w("Failed to parse:", e.getMessage(), config);
                Timber.w(e, "Failed to parse: %s", config);
            }
            return 0.0;
        }


@@ 346,13 339,13 @@ public class AccountController {
        @Override
        protected void finish(AccountController c, Double minutes) {
            if (minutes <= 0) {
                c.logger.d("Ignore schedule - not configured", type);
                Timber.d("Ignore schedule - not configured: %s", type);
                return;
            }
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.SECOND, (int) (minutes * 60.0));
            c.controller.scheduleAlarm(cal.getTime(), c.syncIntent("alarm"));
            c.logger.d("Scheduled:", cal.getTime(), type);
            Timber.d("Scheduled: %s, %s", cal.getTime(), type);
        }
    }



@@ 370,7 363,7 @@ public class AccountController {
        StringAggregator out = new StringAggregator();
        boolean result = callTask(out, err, "rc.taskd.socket=" + socketName, "sync");
        debug("Sync result:", result);
        logger.d("Sync result:", result, "ERR:", err.text(), "OUT:", out.text());
        Timber.d("Sync result: %s\nERR:\n%sOUT:\n%s", result, err.text(), out.text());

        if (result) { // Success
            notifyFactory.create(NotificationChannels.SYNC_SUCCESS);


@@ 474,7 467,6 @@ public class AccountController {
        if (result.isEmpty()) { // Invalid configuration
            result.put("next", "[next] Fail-safe report");
        }
//        logger.d("Reports after sort:", keys, values, defaultReport, result);
        return result;
    }



@@ 544,7 536,7 @@ public class AccountController {
            @Override
            void eat(String key, String value) {
                result.addAll(split2(value, ","));
                logger.d("Parsed priority:", value, result);
                Timber.d("Parsed priority: %s %s", value, result);
            }
        }, errConsumer, "show", "uda.priority.values");
        return result;


@@ 556,7 548,7 @@ public class AccountController {
        try {
            reader = new InputStreamReader(stream, "utf-8");
        } catch (UnsupportedEncodingException e) {
            logger.e("Error opening stream");
            Timber.e("Error opening stream");
            return null;
        }
        Thread thread = new Thread() {


@@ 610,7 602,7 @@ public class AccountController {
                        }
                    }
                } catch (Exception e) {
                    logger.e(e, "Error reading stream");
                    Timber.e(e, "Error reading stream");
                } finally {
                    try {
                        reader.close();


@@ 664,18 656,18 @@ public class AccountController {
            pb.environment().put("TASKRC", new File(tasksFolder, TASKRC).getAbsolutePath());
            pb.environment().put("TASKDATA", new File(tasksFolder, DATA_FOLDER).getAbsolutePath());
            Process p = pb.start();
            logger.d("Calling now:", tasksFolder, args);
            Timber.d("Calling now: %s %s", tasksFolder, args);
//            debug("Execute:", args);
            Thread outThread = readStream(p.getInputStream(), p.getOutputStream(), out);
            Thread errThread = readStream(p.getErrorStream(), null, err);
            int exitCode = p.waitFor();
            logger.d("Exit code:", exitCode, args);
            Timber.d("Exit code %d: %s", exitCode, args);
//            debug("Execute result:", exitCode);
            if (null != outThread) outThread.join();
            if (null != errThread) errThread.join();
            return exitCode;
        } catch (Exception e) {
            logger.e(e, "Failed to execute task");
            Timber.e(e, "Failed to execute task");
            err.eat(e.getMessage());
            debug("Execute failure:");
            debug(e);


@@ 735,13 727,13 @@ public class AccountController {
                    new FileInputStream(fileFromConfig(config.get("taskd.certificate"))),
                    new FileInputStream(fileFromConfig(config.get("taskd.key"))), trustType);
            debug("Credentials loaded");
            logger.d("Connecting to:", this.host, this.port);
            Timber.d("Connecting to %s:%d", host, port);
            this.socket = new LocalServerSocket(name);
        }

        public void accept() throws IOException {
            LocalSocket conn = socket.accept();
            logger.d("New incoming connection");
            Timber.d("New incoming connection");
            new LocalSocketThread(conn).start();
        }



@@ 761,10 753,9 @@ public class AccountController {
                long size = ByteBuffer.wrap(head, 0, 4).order(ByteOrder.BIG_ENDIAN).getInt();
                long bytes = 4;
                byte[] buffer = new byte[1024];
                logger.d("Will transfer:", size);
                Timber.d("Will transfer %d", size);
                while (bytes < size) {
                    int recv = from.read(buffer);
//                logger.d("Actually get:", recv);
                    if (recv == -1) {
                        return bytes;
                    }


@@ 772,7 763,7 @@ public class AccountController {
                    to.flush();
                    bytes += recv;
                }
                logger.d("Transfer done", bytes, size);
                Timber.d("Received %d bytes of %d", bytes, size);
                return bytes;
            }



@@ 800,13 791,13 @@ public class AccountController {
                    InputStream remoteInput = remoteSocket.getInputStream();
                    OutputStream remoteOutput = remoteSocket.getOutputStream();
                    debug("Connected to taskd server");
                    logger.d("Connected, will read first piece", remoteSocket.getSession().getCipherSuite());
                    Timber.d("Connected, will read first piece: %s", remoteSocket.getSession().getCipherSuite());
                    long bread = recvSend(localInput, remoteOutput);
                    long bwrite = recvSend(remoteInput, localOutput);
                    logger.d("Sync success");
                    Timber.d("Sync success");
                    debug("Transfer complete. Bytes sent:", bread, "Bytes received:", bwrite);
                } catch (Exception e) {
                    logger.e(e, "Failed to transfer data");
                    Timber.e(e, "Failed to transfer data");
                    debug("Transfer failure");
                    debug(e);
                } finally {


@@ 829,11 820,11 @@ public class AccountController {
    private LocalServerSocket openLocalSocket(String name) {
        try {
            final Map<String, String> config = taskSettings("taskd.ca", "taskd.certificate", "taskd.key", "taskd.server", "taskd.trust");
            logger.d("Will run with config:", config);
            Timber.d("Will run with config: %s", config);
            debug("taskd.* config:", config);
            if (!config.containsKey("taskd.server")) {
                // Not configured
                logger.d("Sync not configured - give up");
                Timber.d("Sync not configured - give up");
                controller.toastMessage("Sync disabled: no taskd.server value", true);
                debug("taskd.server is empty: sync disabled");
                return null;


@@ 842,7 833,7 @@ public class AccountController {
            try {
                runner = new LocalSocketRunner(name, config);
            } catch (Exception e) {
                logger.e(e, "Error opening socket");
                Timber.e(e, "Error opening socket");
                debug(e);
                controller.toastMessage("Sync disabled: certificate load failure", true);
                return null;


@@ 857,7 848,7 @@ public class AccountController {
                        } catch (IOException e) {
                            debug("Socket accept failed");
                            debug(e);
                            logger.w(e, "Accept failed");
                            Timber.w(e, "Accept failed");
                            return;
                        }
                    }


@@ 867,7 858,7 @@ public class AccountController {
            controller.toastMessage("Sync configured", false);
            return runner.socket; // Close me later on stop
        } catch (Exception e) {
            logger.e(e, "Failed to open local socket");
            Timber.e(e, "Failed to open local socket");
        }
        return null;
    }


@@ 879,7 870,7 @@ public class AccountController {
            query = String.format("(%s)", query);
        }
        String context = taskSetting("context");
        logger.d("taskList context:", context);
        Timber.d("taskList context: %s", context);
        debug("List query:", query, "context:", context);
        if (!TextUtils.isEmpty(context)) { // Have context configured
            String cQuery = taskSetting(String.format("context.%s", context));


@@ 887,7 878,7 @@ public class AccountController {
                debug("Context query:", cQuery);
                query = String.format("(%s) %s", cQuery, query);
            }
            logger.d("Context query:", cQuery, query);
            Timber.d("Context query: %s, %s", cQuery, query);
        }
        final List<Task> result = new ArrayList<>();
        List<String> params = new ArrayList<>();


@@ 903,12 894,12 @@ public class AccountController {
                        JSONObject json = new JSONObject(line);
                        result.add(Task.fromJSON(json, UUID.fromString(id)));
                    } catch (JSONException e) {
                        logger.e(e, "Not JSON object:", line);
                        Timber.e(e, "Not JSON object: %s", line);
                    }
                }
            }
        }, errConsumer, params.toArray(new String[0]));
        logger.d("List for:", query, result.size(), context);
        Timber.d("List for %s: %d %s", query, result.size(), context);
        return result;
    }


M app/src/main/java/kvj/taskw/data/BootReceiver.java => app/src/main/java/kvj/taskw/data/BootReceiver.java +2 -3
@@ 4,7 4,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import org.kvj.bravo7.log.Logger;
import timber.log.Timber;

import kvj.taskw.App;



@@ 14,11 14,10 @@ import kvj.taskw.App;
public class BootReceiver extends BroadcastReceiver {

    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);

    @Override
    public void onReceive(Context context, Intent intent) {
        logger.i("Application started");
        Timber.i("Application started");
        controller.messageShort("Auto-sync timers have started");
    }
}

M app/src/main/java/kvj/taskw/data/Controller.java => app/src/main/java/kvj/taskw/data/Controller.java +10 -8
@@ 35,6 35,8 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;

import timber.log.Timber;

import kvj.taskw.App;
import kvj.taskw.R;
import kvj.taskw.notifications.NotificationChannels;


@@ 88,7 90,7 @@ public class Controller extends org.kvj.bravo7.ng.Controller {
            messageShort("Shortcut added");
            return true;
        } catch (Exception e) {
            logger.d(e, "Failed to add shortcut");
            Timber.d(e, "Failed to add shortcut");
        }
        return false;
    }


@@ 109,7 111,7 @@ public class Controller extends org.kvj.bravo7.ng.Controller {
            file.setExecutable(true, true);
            return file.getAbsolutePath();
        } catch (IOException e) {
            logger.e(e, "Error preparing file");
            Timber.e(e, "Error preparing file");
        }
        return null;
    }


@@ 160,12 162,12 @@ public class Controller extends org.kvj.bravo7.ng.Controller {
    }

    public void addAccount(Activity activity) {
        logger.d("Will add new account");
        Timber.d("Will add new account");
        accountManager.addAccount(App.ACCOUNT_TYPE, null, null, null, activity,
          new AccountManagerCallback<Bundle>() {
              @Override
              public void run(AccountManagerFuture<Bundle> future) {
                  logger.d("Add done");
                  Timber.d("Add done");
              }
          }, null);
    }


@@ 199,28 201,28 @@ public class Controller extends org.kvj.bravo7.ng.Controller {
            File folder = new File(context().getExternalFilesDir(null), folderName);
            if (!folder.exists()) {
                if (!folder.mkdir()) {
                    logger.w("Failed to create folder", name);
                    Timber.w("Failed to create folder '%s'", name);
                    return "Storage access error";
                }
            }
            File taskrc = new File(folder, AccountController.TASKRC);
            if (!taskrc.exists()) {
                if (!taskrc.createNewFile()) {
                    logger.w("Failed to create folder", name);
                    Timber.w("Failed to create folder '%s'", name);
                    return "Storage access error";
                }
            }
            File dataFolder = new File(folder, AccountController.DATA_FOLDER);
            if (!dataFolder.exists()) {
                if (!dataFolder.mkdir()) {
                    logger.w("Failed to create data folder", dataFolder.getAbsolutePath());
                    Timber.w("Failed to create data folder '%s'", dataFolder.getAbsolutePath());
                    return "Storage access error";
                }
            }
            Bundle data = new Bundle();
            data.putString(App.ACCOUNT_FOLDER, folderName);
            if (!accountManager.addAccountExplicitly(new Account(name, App.ACCOUNT_TYPE), "", data)) {
                logger.w("Failed to create account", name);
                Timber.w("Failed to create account '%s'", name);
                return "Account create failure";
            }
            return null;

M app/src/main/java/kvj/taskw/data/SyncIntentReceiver.java => app/src/main/java/kvj/taskw/data/SyncIntentReceiver.java +4 -4
@@ 6,9 6,10 @@ import android.content.Intent;
import android.os.PowerManager;
import android.text.TextUtils;

import org.kvj.bravo7.log.Logger;
import org.kvj.bravo7.util.Tasks;

import timber.log.Timber;

import kvj.taskw.App;

/**


@@ 17,14 18,13 @@ import kvj.taskw.App;
public class SyncIntentReceiver extends BroadcastReceiver {

    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);

    @Override
    public void onReceive(final Context context, final Intent intent) {
        // Lock and run sync
        final PowerManager.WakeLock lock = controller.lock();
        lock.acquire(10*60*1000L);
        logger.d("Sync from receiver", intent.getData());
        Timber.d("Sync from receiver: %s", intent.getData());
        new Tasks.SimpleTask<String>() {

            @Override


@@ 38,7 38,7 @@ public class SyncIntentReceiver extends BroadcastReceiver {

            @Override
            protected void onPostExecute(String s) {
                logger.d("Sync from receiver done:", s);
                Timber.d("Sync from receiver done: %s", s);
                if (null != s) {
                    // Failed
                    controller.messageShort(s);

M app/src/main/java/kvj/taskw/sync/SSLHelper.java => app/src/main/java/kvj/taskw/sync/SSLHelper.java +6 -9
@@ 2,8 2,6 @@ package kvj.taskw.sync;

import android.util.Base64;

import org.kvj.bravo7.log.Logger;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


@@ 30,13 28,13 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import timber.log.Timber;

import kvj.taskw.sync.der.DerInputStream;
import kvj.taskw.sync.der.DerValue;

public class SSLHelper {

    static Logger logger = Logger.forClass(SSLHelper.class);

    protected static byte[] fromStream(InputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];


@@ 101,7 99,6 @@ public class SSLHelper {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        Certificate cert = loadCertificate(certStream);
        keyStore.load(null);
//        logger.d("Keystore:", cert.getPublicKey().getAlgorithm(), cert.getPublicKey().getFormat());
        keyStore.setCertificateEntry("certificate", cert);
        keyStore.setKeyEntry("private-key", loadPrivateKey(keyStream), "".toCharArray(), new Certificate[]{cert});
        kmf.init(keyStore, "".toCharArray());


@@ 126,7 123,7 @@ public class SSLHelper {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        final X509Certificate cert = loadCertificate(stream);
        logger.d("Truststore:", cert.getIssuerDN().getName(), cert.getSubjectDN().getName());
        Timber.d("Truststore: %s %s", cert.getIssuerDN().getName(), cert.getSubjectDN().getName());
        keyStore.setCertificateEntry("ca", cert);
        tmf.init(keyStore);
        TrustManager[] orig = tmf.getTrustManagers();


@@ 144,15 141,15 @@ public class SSLHelper {

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                logger.d("Check server cert:", chain.length, authType, trustType);
                Timber.d("Check server cert: %d %s %s", chain.length, authType, trustType);
                for (X509Certificate c : chain) { // Check every cert
                    logger.d("Check certificate:", c.getIssuerDN().getName(), c.getSubjectDN().getName());
                    Timber.d("Check certificate: %s %s", c.getIssuerDN().getName(), c.getSubjectDN().getName());
                }
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                logger.d("Issuers:");
                Timber.d("Issuers:");
                return new X509Certificate[]{cert};
            }
        };

M app/src/main/java/kvj/taskw/ui/Editor.java => app/src/main/java/kvj/taskw/ui/Editor.java +5 -10
@@ 1,5 1,9 @@
package kvj.taskw.ui;

import java.util.Calendar;
import java.util.Date;
import java.util.List;

import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.os.Bundle;


@@ 15,16 19,11 @@ import android.widget.Spinner;
import android.widget.TimePicker;

import org.jetbrains.annotations.NotNull;

import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.widget.ImageViewIntegerAdapter;
import org.kvj.bravo7.form.impl.widget.SpinnerIntegerAdapter;
import org.kvj.bravo7.form.impl.widget.TextViewCharSequenceAdapter;
import org.kvj.bravo7.log.Logger;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import kvj.taskw.App;
import kvj.taskw.R;


@@ 33,8 32,6 @@ import kvj.taskw.R;
 * Created by kvorobyev on 11/21/15.
 */
public class Editor extends Fragment {

    Logger logger = Logger.forInstance(this);
    private Spinner prioritiesSpinner = null;

    @Override


@@ 100,7 97,6 @@ public class Editor extends Fragment {
                DatePickerDialog dialog = new DatePickerDialog(getContext(), new DatePickerDialog.OnDateSetListener() {
                    @Override
                    public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//                        logger.d("Date set:", year, monthOfYear, dayOfMonth);
                        c.set(Calendar.DAY_OF_MONTH, 1);
                        c.set(Calendar.YEAR, year);
                        c.set(Calendar.MONTH, monthOfYear);


@@ 123,7 119,6 @@ public class Editor extends Fragment {
                TimePickerDialog dialog = new TimePickerDialog(getContext(), new TimePickerDialog.OnTimeSetListener() {
                    @Override
                    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
//                        logger.d("Date set:", year, monthOfYear, dayOfMonth);
                        c.set(Calendar.HOUR_OF_DAY, hourOfDay);
                        c.set(Calendar.MINUTE, minute);
                        c.set(Calendar.SECOND, 0);

M app/src/main/java/kvj/taskw/ui/EditorActivity.java => app/src/main/java/kvj/taskw/ui/EditorActivity.java +6 -7
@@ 9,13 9,14 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.ProgressBar;

import timber.log.Timber;

import kvj.taskw.data.UUIDBundleAdapter;
import org.kvj.bravo7.form.BundleAdapter;
import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.ViewFinder;
import org.kvj.bravo7.form.impl.bundle.StringBundleAdapter;
import org.kvj.bravo7.form.impl.widget.TransientAdapter;
import org.kvj.bravo7.log.Logger;
import org.kvj.bravo7.util.DataUtil;
import org.kvj.bravo7.util.Tasks;



@@ 28,7 29,7 @@ import kvj.taskw.App;
import kvj.taskw.R;
import kvj.taskw.data.AccountController;
import kvj.taskw.data.Controller;
import kvj.taskw.ui.AppActivity;
import kvj.taskw.data.UUIDBundleAdapter;

/**
 * Created by kvorobyev on 11/21/15.


@@ 39,7 40,6 @@ public class EditorActivity extends AppActivity {
    private Editor editor = null;
    private FormController form = new FormController(new ViewFinder.ActivityViewFinder(this));
    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);
    private List<String> priorities = null;
    private AccountController.TaskListener progressListener = null;
    private AccountController ac = null;


@@ 52,7 52,6 @@ public class EditorActivity extends AppActivity {
        editor = (Editor) getSupportFragmentManager().findFragmentById(R.id.editor_editor);
        ProgressBar progressBar = findViewById(R.id.progress);
        setSupportActionBar(toolbar);
        logger.e("OnCreate", savedInstanceState);
        form.add(new TransientAdapter<>(new StringBundleAdapter(), null), App.KEY_ACCOUNT);
        form.add(new TransientAdapter<>(new UUIDBundleAdapter(), null), App.KEY_EDIT_UUID);
        form.add(new TransientAdapter<>(new BundleAdapter<Bundle>() {


@@ 102,7 101,7 @@ public class EditorActivity extends AppActivity {
                editor.show(form);
                Bundle formData = form.getValue(App.KEY_EDIT_DATA);
                List<String> fields = form.getValue(App.KEY_EDIT_DATA_FIELDS);
                logger.d("Edit:", formData, fields);
                Timber.d("Edit: %s %s", formData, fields);
                if (null != formData && null != fields) { // Have data
                    for (String f : fields) { // $COMMENT
                        form.setValue(f, formData.getString(f));


@@ 178,7 177,7 @@ public class EditorActivity extends AppActivity {
            super.onBackPressed();
            return;
        }
        logger.d("Changed:", form.changes());
        Timber.d("Changed: %s", form.changes());
        controller.question(this, "There are some changes, discard?", new Runnable() {

            @Override


@@ 240,7 239,7 @@ public class EditorActivity extends AppActivity {
        }
        UUID uuid = form.getValue(App.KEY_EDIT_UUID);
        boolean completed = form.getValue(App.KEY_EDIT_STATUS, Integer.class) > 0;
        logger.d("Saving change:", uuid, changes, completed);
        Timber.d("Saving change: %s %s %s", uuid, changes, completed);
        if (uuid == null) { // Add new
            return completed? ac.taskLog(changes): ac.taskAdd(changes);
        } else {

M app/src/main/java/kvj/taskw/ui/MainActivity.java => app/src/main/java/kvj/taskw/ui/MainActivity.java +6 -8
@@ 26,13 26,14 @@ import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;

import timber.log.Timber;

import org.jetbrains.annotations.NotNull;
import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.ViewFinder;
import org.kvj.bravo7.form.impl.bundle.StringBundleAdapter;
import org.kvj.bravo7.form.impl.widget.TextViewCharSequenceAdapter;
import org.kvj.bravo7.form.impl.widget.TransientAdapter;
import org.kvj.bravo7.log.Logger;
import org.kvj.bravo7.util.DataUtil;

import java.util.ArrayList;


@@ 48,9 49,6 @@ import kvj.taskw.data.Task;
import kvj.taskw.data.Task.Companion.*;

public class MainActivity extends AppActivity implements Controller.ToastMessageListener {

    Logger logger = Logger.forInstance(this);

    Controller controller = App.controller();
    private AccountController ac = null;
    private Toolbar toolbar = null;


@@ 158,7 156,7 @@ public class MainActivity extends AppActivity implements Controller.ToastMessage
            public void onClick(View v) {
                String input = form.getValue(App.KEY_QUERY);
//                form.setValue(App.KEY_QUERY, input);
                logger.d("Changed filter:", form.getValue(App.KEY_QUERY), input);
                Timber.d("Changed filter: %s %s", form.getValue(App.KEY_QUERY), input);
                reload();
            }
        });


@@ 238,7 236,7 @@ public class MainActivity extends AppActivity implements Controller.ToastMessage
                try {
                    startActivityForResult(intent, App.SETTINGS_REQUEST);
                } catch (Exception e) {
                    logger.e(e, "Failed to edit file");
                    Timber.e(e, "Failed to edit file");
                    controller.messageLong("No suitable plain text editors found");
                }
                break;


@@ 468,7 466,7 @@ public class MainActivity extends AppActivity implements Controller.ToastMessage
            controller.addAccount(this);
            return false;
        } else {
            logger.d("Refresh account:", account);
            Timber.d("Refresh account: %s", account);
            form.setValue(App.KEY_ACCOUNT, account);
            ac = controller.accountController(form); // Should be not null always
        }


@@ 625,7 623,7 @@ public class MainActivity extends AppActivity implements Controller.ToastMessage
        super.onActivityResult(requestCode, resultCode, data);
        if (null == ac) return;
        if (RESULT_OK == resultCode && App.SETTINGS_REQUEST == requestCode) { // Settings were modified
            logger.d("Reload after finish:", requestCode, resultCode);
            Timber.d("Reload after finish: %s %s", requestCode, resultCode);
            refreshAccount(form.getValue(App.KEY_ACCOUNT, String.class));
        }
    }

M app/src/main/java/kvj/taskw/ui/MainList.java => app/src/main/java/kvj/taskw/ui/MainList.java +4 -4
@@ 9,9 9,10 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import timber.log.Timber;

import org.jetbrains.annotations.NotNull;
import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.log.Logger;

import java.util.List;



@@ 29,7 30,6 @@ public class MainList extends Fragment {
    private RecyclerView list = null;
    private ReportInfo info = null;
    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);
    private MainListAdapter adapter = null;
    private String account = null;



@@ 58,7 58,7 @@ public class MainList extends Fragment {

        @Override
        protected ReportInfo background(MainList frag, Void... params) {
            frag.logger.d("Load:", query, report);
            Timber.d("Load: %s %s", query, report);
            return frag.controller.accountController(frag.account).taskReportInfo(report, query);
        }



@@ 84,7 84,7 @@ public class MainList extends Fragment {

        @Override
        protected List<Task> background(MainList frag, Void... params) {
            frag.logger.d("Exec:", frag.info.query);
            Timber.d("Exec: %s", frag.info.query);
            List<Task> list = frag.controller.accountController(frag.account).taskList(frag.info.query);
            frag.info.sort(list); // Sorted according to report spec.
            return list;

M app/src/main/java/kvj/taskw/ui/MainListAdapter.kt => app/src/main/java/kvj/taskw/ui/MainListAdapter.kt +2 -4
@@ 14,7 14,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

import org.kvj.bravo7.log.Logger
import timber.log.Timber;

import kvj.taskw.App
import kvj.taskw.R


@@ 121,8 121,6 @@ class MainListAdapter : RecyclerView.Adapter<MainListAdapter.ViewHolder>() {
    }

    companion object {
        private val logger = Logger.forClass(MainListAdapter::class.java)

        @JvmField
        val formattedFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
        @JvmField


@@ 149,7 147,7 @@ class MainListAdapter : RecyclerView.Adapter<MainListAdapter.ViewHolder>() {
                return formattedISO.format(value)

            } catch (e: Exception) {
                logger.e(e, "Failed to parse Date:", value)
                Timber.e(e, "Failed to parse date '%s'", value)
            }

            return null

M app/src/main/java/kvj/taskw/ui/RunActivity.java => app/src/main/java/kvj/taskw/ui/RunActivity.java +3 -3
@@ 18,6 18,8 @@ import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;

import timber.log.Timber;

import org.jetbrains.annotations.NotNull;
import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.ViewFinder;


@@ 25,7 27,6 @@ import org.kvj.bravo7.form.impl.bundle.ListStringBundleAdapter;
import org.kvj.bravo7.form.impl.bundle.StringBundleAdapter;
import org.kvj.bravo7.form.impl.widget.TextViewCharSequenceAdapter;
import org.kvj.bravo7.form.impl.widget.TransientAdapter;
import org.kvj.bravo7.log.Logger;

import java.util.ArrayList;
import java.util.List;


@@ 42,7 43,6 @@ public class RunActivity extends AppActivity {

    FormController form = new FormController(new ViewFinder.ActivityViewFinder(this));
    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);
    private AccountController ac = null;
    private RunAdapter adapter = null;
    private kvj.taskw.data.AccountController.TaskListener progressListener = null;


@@ 96,7 96,7 @@ public class RunActivity extends AppActivity {
        sendIntent.putExtra(Intent.EXTRA_TEXT, text);
        sendIntent.setType("text/plain");
        if (null != mShareActionProvider) {
            logger.d("Share provider set");
            Timber.d("Share provider set");
            mShareActionProvider.setShareIntent(sendIntent);
        }
    }

M app/src/main/java/kvj/taskw/ui/TextEditor.java => app/src/main/java/kvj/taskw/ui/TextEditor.java +4 -5
@@ 7,12 7,13 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;

import timber.log.Timber;

import org.kvj.bravo7.form.FormController;
import org.kvj.bravo7.form.impl.ViewFinder;
import org.kvj.bravo7.form.impl.bundle.StringBundleAdapter;
import org.kvj.bravo7.form.impl.widget.TextViewCharSequenceAdapter;
import org.kvj.bravo7.form.impl.widget.TransientAdapter;
import org.kvj.bravo7.log.Logger;
import org.kvj.bravo7.util.Tasks;

import java.io.InputStream;


@@ 31,7 32,6 @@ public class TextEditor extends AppActivity {
    FormController form = new FormController(new ViewFinder.ActivityViewFinder(this));
    private Toolbar toolbar = null;
    Controller controller = App.controller();
    Logger logger = Logger.forInstance(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {


@@ 42,7 42,6 @@ public class TextEditor extends AppActivity {
        form.add(new TextViewCharSequenceAdapter(R.id.text_editor_input, null).trackOrigin(true), App.KEY_TEXT_INPUT);
        form.add(new TransientAdapter<>(new StringBundleAdapter(), null), App.KEY_TEXT_TARGET);
        form.load(this, savedInstanceState);
//        logger.d("On create:", form.getValue(App.KEY_TEXT_INPUT), form.getValue(App.KEY_TEXT_TARGET));
        if (null == form.getValue(App.KEY_TEXT_TARGET)) {
            // No data - load from Uri
            loadText(getIntent());


@@ 74,7 73,7 @@ public class TextEditor extends AppActivity {

            @Override
            public void finish(String result) {
                logger.d("File loaded:", uri, result != null);
                Timber.d("File loaded: %s %s", uri, result != null);
                if (null == result) {
                    controller.messageLong("File IO error");
                    TextEditor.this.finish();


@@ 99,7 98,7 @@ public class TextEditor extends AppActivity {
            super.onBackPressed(); // Close
            return;
        }
        logger.d("Changes:", form.changes());
        Timber.d("Changes: %s", form.changes());
        controller.question(this, "There are some changes, discard?", new Runnable() {
            @Override
            public void run() {

M build.gradle => build.gradle +1 -0
@@ 2,6 2,7 @@ buildscript {
    ext {
        versions = [
            'kotlin': '1.3.20',
            'timber': '4.7.1',
            'support': '28.0.0',
            'constraintLayout': '1.1.3',
        ]

M cli => cli +1 -1
@@ 1,1 1,1 @@
Subproject commit e20bb3b1b043b6108228297a8283c99fccb1971d
Subproject commit dbea30e70d6d33d0fb22cbdbfcf073a6d04921cd