<NME> Twitter.java
<BEF> package cgeo.geocaching.twitter;

import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.R;
import cgeo.geocaching.enumerations.LoadFlags;
import cgeo.geocaching.log.LogEntry;
import cgeo.geocaching.log.LogTemplateProvider;
import cgeo.geocaching.log.LogTemplateProvider.LogContext;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.models.Trackable;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Parameters;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.Log;

import ch.boye.httpclientandroidlib.HttpResponse;

import androidx.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;

public final class Twitter {

    private static final int MAX_TWEET_SIZE = 140;

    private Twitter() {
        // Utility class
    }

    public static void postTweetCache(final String geocode, @Nullable final LogEntry logEntry) {
        final Geocache cache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB);
        if (cache != null) {
            postTweet(getStatusMessage(cache, logEntry));
        }
    }

    public static void postTweetTrackable(final String geocode, @Nullable final LogEntry logEntry) {
        final Trackable trackable = DataStore.loadTrackable(geocode);
        if (trackable != null) {
            postTweet(getStatusMessage(trackable, logEntry));
        }
    }

    private static void postTweet(final String status) {
        if (!Settings.isUseTwitter() || !Settings.isTwitterLoginValid()) {
            return;
        }

        try {
            final String trimmed = StringUtils.trim(status);
            final String truncated = StringUtils.length(trimmed) <= MAX_TWEET_SIZE ? trimmed : StringUtils.substring(trimmed, 0, MAX_TWEET_SIZE - 1) + CgeoApplication.getInstance().getString(R.string.ellipsis);
            final Parameters parameters = new Parameters("status", truncated);

            OAuth.signOAuth("api.twitter.com", "/1.1/statuses/update.json", "POST", true, parameters,
                    new OAuthTokens(Settings.getTokenPublic(), Settings.getTokenSecret()), Settings.getTwitterKeyConsumerPublic(), Settings.getTwitterKeyConsumerSecret());
            try {
                Network.completeWithSuccess(Network.postRequest("https://api.twitter.com/1.1/statuses/update.json", parameters));
                Log.i("Tweet posted");
            } catch (final Exception ignored) {
                Log.e("Tweet could not be posted");
            }
        } catch (final Exception e) {
            Log.e("Twitter.postTweet", e);
    private static void appendHashTag(final StringBuilder status, final String tag) {
        if (status.length() + HASH_PREFIX_WITH_BLANK.length() + tag.length() <= MAX_TWEET_SIZE) {
            final String tagWithPrefix = HASH_PREFIX_WITH_BLANK + tag;
            if (status.indexOf(tagWithPrefix, 0) == -1) {
                status.append(tagWithPrefix);
            }
        }
    }

    static String getStatusMessage(Geocache cache) {
        String name = cache.getName();
        if (name.length() > 100) {
            name = name.substring(0, 100) + '…';
            }
        final String url = StringUtils.defaultString(cache.getUrl());
        return fillTemplate(Settings.getCacheTwitterMessage(), name, url);
    }

    static String getStatusMessage(Trackable trackable) {
        String name = trackable.getName();
        if (name.length() > 82) {
            name = name.substring(0, 81) + '…';
        }
        String url = StringUtils.defaultString(trackable.getUrl());
        String status = Settings.getTrackableTwitterMessage();
        return fillTemplate(status, name, url);
    }

    private static String fillTemplate(String template, String name, final String url) {
        String result = StringUtils.replace(template, "[NAME]", name);
        result = StringUtils.replace(result, "[URL]", url);
        StringBuilder builder = new StringBuilder(result);
        appendHashTag(builder, "cgeo");
        appendHashTag(builder, "geocaching");
        return builder.toString();
        appendHashTag(builder, "#geocaching");
        return builder.toString();
    }
}

<MSG> fix #3365: Twitter placeholder not working
<DFF> @@ -12,6 +12,8 @@ import cgeo.geocaching.network.OAuth;
 import cgeo.geocaching.network.Parameters;
 import cgeo.geocaching.settings.Settings;
 import cgeo.geocaching.utils.Log;
+import cgeo.geocaching.utils.LogTemplateProvider;
+import cgeo.geocaching.utils.LogTemplateProvider.LogContext;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 
@@ -64,35 +66,22 @@ public final class Twitter {
     private static void appendHashTag(final StringBuilder status, final String tag) {
         if (status.length() + HASH_PREFIX_WITH_BLANK.length() + tag.length() <= MAX_TWEET_SIZE) {
             final String tagWithPrefix = HASH_PREFIX_WITH_BLANK + tag;
-            if (status.indexOf(tagWithPrefix, 0) == -1) {
+            if (!StringUtils.contains(status, tagWithPrefix)) {
                 status.append(tagWithPrefix);
             }
         }
     }
 
     static String getStatusMessage(Geocache cache) {
-        String name = cache.getName();
-        if (name.length() > 100) {
-            name = name.substring(0, 100) + '…';
-            }
-        final String url = StringUtils.defaultString(cache.getUrl());
-        return fillTemplate(Settings.getCacheTwitterMessage(), name, url);
+        return appendHashTags(LogTemplateProvider.applyTemplates(Settings.getCacheTwitterMessage(), new LogContext(cache)));
     }
 
     static String getStatusMessage(Trackable trackable) {
-        String name = trackable.getName();
-        if (name.length() > 82) {
-            name = name.substring(0, 81) + '…';
-        }
-        String url = StringUtils.defaultString(trackable.getUrl());
-        String status = Settings.getTrackableTwitterMessage();
-        return fillTemplate(status, name, url);
+        return appendHashTags(LogTemplateProvider.applyTemplates(Settings.getTrackableTwitterMessage(), new LogContext(trackable)));
     }
 
-    private static String fillTemplate(String template, String name, final String url) {
-        String result = StringUtils.replace(template, "[NAME]", name);
-        result = StringUtils.replace(result, "[URL]", url);
-        StringBuilder builder = new StringBuilder(result);
+    private static String appendHashTags(final String status) {
+        StringBuilder builder = new StringBuilder(status);
         appendHashTag(builder, "cgeo");
         appendHashTag(builder, "geocaching");
         return builder.toString();

fix #3365: Twitter placeholder not working