<commit_before>package com.nutomic.syncthingandroid.syncthing;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.WebGuiActivity;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 * Holds the native syncthing instance and provides an API to access it.
 */
public class SyncthingService extends Service {

	private static final String TAG = "SyncthingService";

	private static final int NOTIFICATION_ID = 1;

	/**
	 * Path to the native, integrated syncthing binary, relative to the data folder
	 */
	private static final String BINARY_NAME = "lib/libsyncthing.so";

	/**
	 * Interval in ms, at which connections to the web gui are performed on first start
	 * to find out if it's online.
	 */
	private static final long WEB_GUI_POLL_INTERVAL = 100;

	/**
	 * File in the config folder that contains configuration.
	 */
	private static final String CONFIG_FILE = "config.xml";

	/**
	 * Name of the public key file in the data directory.
	 */
	private static final String PUBLIC_KEY_FILE = "cert.pem";

	/**
	 * Name of the private key file in the data directory.
	 */
	private static final String PRIVATE_KEY_FILE = "key.pem";

	private RestApi mApi;

	private final ReentrantLock mNativeLogLock = new ReentrantLock();

	private String mNativeLog = "";

	private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this);

	/**
	 * Callback for when the Syncthing web interface becomes first available after service start.
	 */
	public interface OnWebGuiAvailableListener {
		public void onWebGuiAvailable();
	}

	private LinkedList<OnWebGuiAvailableListener> mOnWebGuiAvailableListeners =
			new LinkedList<OnWebGuiAvailableListener>();

	private boolean mIsWebGuiAvailable = false;

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		return START_STICKY;
	}

	/**
	 * Thrown when execution of the native syncthing binary returns an error.
	 * Prints the syncthing log.
	 */
	public static class NativeExecutionException extends RuntimeException {

		private final String mLog;

		public NativeExecutionException(String message, String log) {
			super(message);
			mLog = log;
		}

		@Override
		public String getMessage() {
			return super.getMessage() + "\n" + mLog;
		}
	}

	/**
	 * Runs the syncthing binary from command line, and prints its output to logcat (on exit).
	 */
	private void runNative() {
		DataOutputStream dos = null;
		int ret = 1;
		Process process = null;
		try	{
			process = Runtime.getRuntime().exec("sh");
			dos = new DataOutputStream(process.getOutputStream());
			// Set home directory to data folder for syncthing to use.
			dos.writeBytes("HOME=" + getFilesDir() + "\n");
			// Call syncthing with -home (as it would otherwise use "~/.config/syncthing/".
			dos.writeBytes(getApplicationInfo().dataDir + "/" + BINARY_NAME + " " +
					"-home " + getFilesDir() + "\n");
			dos.writeBytes("exit\n");
			dos.flush();

			log(process.getInputStream(), Log.INFO);
			log(process.getErrorStream(), Log.WARN);

			ret = process.waitFor();
		}
		catch(IOException e) {
			Log.e(TAG, "Failed to execute syncthing binary or read output", e);
		}
		catch(InterruptedException e) {
			Log.e(TAG, "Failed to execute syncthing binary or read output", e);
		}
		finally {
			process.destroy();
			if (ret != 0) {
				stopSelf();
				// Include the log for Play Store crash reports.
				throw new NativeExecutionException("Syncthing binary returned error code " +
						Integer.toString(ret), mNativeLog);
			}
		}
	}

	/**
	 * Logs the outputs of a stream to logcat and mNativeLog.
	 *
	 * @param is The stream to log.
	 * @param priority The log level, eg Log.INFO or Log.WARN.
	 */
	private void log(final InputStream is, final int priority) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
				String line;
				try {
					while ((line = br.readLine()) != null) {
						mNativeLogLock.lock();
						Log.println(priority, TAG, ": " + line);
						mNativeLog += line + "\n";
						mNativeLogLock.unlock();
					}
				}
				catch (IOException e) {
					// NOTE: This is sometimes called on shutdown, as
					// Process.destroy() closes the stream.
					Log.w(TAG, "Failed to read syncthing command line output", e);
				}
			}
		}).start();
	}

	/**
	 * Polls SYNCTHING_URL until it returns HTTP status OK, then calls all listeners
	 * in mOnWebGuiAvailableListeners and clears it.
	 */
	private class PollWebGuiAvailableTask extends AsyncTask<Void, Void, Void> {
		@Override
		protected Void doInBackground(Void... voids) {
			int status = 0;
			HttpClient httpclient = new DefaultHttpClient();
			HttpHead head = new HttpHead(mApi.getUrl());
			do {
				try {
					Thread.sleep(WEB_GUI_POLL_INTERVAL);
					HttpResponse response = httpclient.execute(head);
					// NOTE: status is not really needed, as HttpHostConnectException is thrown
					// earlier.
					status = response.getStatusLine().getStatusCode();
				}
				catch (HttpHostConnectException e) {
					// We catch this in every call, as long as the service is not online,
					// so we ignore and continue.
				}
				catch (IOException e) {
					Log.w(TAG, "Failed to poll for web interface", e);
				}
				catch (InterruptedException e) {
					Log.w(TAG, "Failed to poll for web interface", e);
				}
			} while(status != HttpStatus.SC_OK);
			return null;
		}

		@Override
		protected void onPostExecute(Void aVoid) {
			Log.i(TAG, "Web GUI has come online at " + mApi.getUrl());
			mIsWebGuiAvailable = true;
			for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) {
				listener.onWebGuiAvailable();
			}
			mOnWebGuiAvailableListeners.clear();
		}
	}

	/**
	 * Move config file, keys, and index files to "official" folder
	 *
	 * Intended to bring the file locations in older installs in line with
	 * newer versions.
	 */
	private void moveConfigFiles() {
		FilenameFilter idxFilter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".idx.gz");
			}
		};

		if (new File(getApplicationInfo().dataDir, PUBLIC_KEY_FILE).exists()) {
			try {
				File publicKey = new File(getApplicationInfo().dataDir, PUBLIC_KEY_FILE);
				publicKey.renameTo(new File(getFilesDir(), PUBLIC_KEY_FILE));
				File privateKey = new File(getApplicationInfo().dataDir, PRIVATE_KEY_FILE);
				privateKey.renameTo(new File(getFilesDir(), PRIVATE_KEY_FILE));
				File config = new File(getApplicationInfo().dataDir, CONFIG_FILE);
				config.renameTo(new File(getFilesDir(), CONFIG_FILE));

				File oldStorageDir = new File(getApplicationInfo().dataDir);
				File[] files = oldStorageDir.listFiles(idxFilter);
				for (File file : files) {
					if (file.isFile()) {
						file.renameTo(new File(getFilesDir(), file.getName()));
					}
				}
			}
			catch (Exception e) {
				Log.e(TAG, "Failed to move config files", e);
			}
		}
	}

	/**
	 * Creates notification, starts native binary.
	 */
	@Override

	public void onCreate() {
		PendingIntent pi = PendingIntent.getActivity(
				this, 0, new Intent(this, WebGuiActivity.class),
				PendingIntent.FLAG_UPDATE_CURRENT);
		Notification n = new NotificationCompat.Builder(this)
				.setContentTitle(getString(R.string.app_name))
				.setSmallIcon(R.drawable.ic_launcher)
				.setContentIntent(pi)
				.build();
		n.flags |= Notification.FLAG_ONGOING_EVENT;
		startForeground(NOTIFICATION_ID, n);

		new Thread(new Runnable() {
			@Override
			public void run() {
				if (isFirstStart(SyncthingService.this)) {
					Log.i(TAG, "App started for the first time. " +
							"Copying default config, keys will be generated automatically");
					copyDefaultConfig();
				}
				moveConfigFiles();<commit_msg>Part of fix for #18 on Gingerbread - call Looper.prepare() to set up the thread.<commit_after>package com.nutomic.syncthingandroid.syncthing;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.IBinder;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.WebGuiActivity;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

/**
 * Holds the native syncthing instance and provides an API to access it.
 */
public class SyncthingService extends Service {

	private static final String TAG = "SyncthingService";

	private static final int NOTIFICATION_ID = 1;

	/**
	 * Path to the native, integrated syncthing binary, relative to the data folder
	 */
	private static final String BINARY_NAME = "lib/libsyncthing.so";

	/**
	 * Interval in ms, at which connections to the web gui are performed on first start
	 * to find out if it's online.
	 */
	private static final long WEB_GUI_POLL_INTERVAL = 100;

	/**
	 * File in the config folder that contains configuration.
	 */
	private static final String CONFIG_FILE = "config.xml";

	/**
	 * Name of the public key file in the data directory.
	 */
	private static final String PUBLIC_KEY_FILE = "cert.pem";

	/**
	 * Name of the private key file in the data directory.
	 */
	private static final String PRIVATE_KEY_FILE = "key.pem";

	private RestApi mApi;

	private final ReentrantLock mNativeLogLock = new ReentrantLock();

	private String mNativeLog = "";

	private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this);

	/**
	 * Callback for when the Syncthing web interface becomes first available after service start.
	 */
	public interface OnWebGuiAvailableListener {
		public void onWebGuiAvailable();
	}

	private LinkedList<OnWebGuiAvailableListener> mOnWebGuiAvailableListeners =
			new LinkedList<OnWebGuiAvailableListener>();

	private boolean mIsWebGuiAvailable = false;

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		return START_STICKY;
	}

	/**
	 * Thrown when execution of the native syncthing binary returns an error.
	 * Prints the syncthing log.
	 */
	public static class NativeExecutionException extends RuntimeException {

		private final String mLog;

		public NativeExecutionException(String message, String log) {
			super(message);
			mLog = log;
		}

		@Override
		public String getMessage() {
			return super.getMessage() + "\n" + mLog;
		}
	}

	/**
	 * Runs the syncthing binary from command line, and prints its output to logcat (on exit).
	 */
	private void runNative() {
		DataOutputStream dos = null;
		int ret = 1;
		Process process = null;
		try	{
			process = Runtime.getRuntime().exec("sh");
			dos = new DataOutputStream(process.getOutputStream());
			// Set home directory to data folder for syncthing to use.
			dos.writeBytes("HOME=" + getFilesDir() + "\n");
			// Call syncthing with -home (as it would otherwise use "~/.config/syncthing/".
			dos.writeBytes(getApplicationInfo().dataDir + "/" + BINARY_NAME + " " +
					"-home " + getFilesDir() + "\n");
			dos.writeBytes("exit\n");
			dos.flush();

			log(process.getInputStream(), Log.INFO);
			log(process.getErrorStream(), Log.WARN);

			ret = process.waitFor();
		}
		catch(IOException e) {
			Log.e(TAG, "Failed to execute syncthing binary or read output", e);
		}
		catch(InterruptedException e) {
			Log.e(TAG, "Failed to execute syncthing binary or read output", e);
		}
		finally {
			process.destroy();
			if (ret != 0) {
				stopSelf();
				// Include the log for Play Store crash reports.
				throw new NativeExecutionException("Syncthing binary returned error code " +
						Integer.toString(ret), mNativeLog);
			}
		}
	}

	/**
	 * Logs the outputs of a stream to logcat and mNativeLog.
	 *
	 * @param is The stream to log.
	 * @param priority The log level, eg Log.INFO or Log.WARN.
	 */
	private void log(final InputStream is, final int priority) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
				String line;
				try {
					while ((line = br.readLine()) != null) {
						mNativeLogLock.lock();
						Log.println(priority, TAG, ": " + line);
						mNativeLog += line + "\n";
						mNativeLogLock.unlock();
					}
				}
				catch (IOException e) {
					// NOTE: This is sometimes called on shutdown, as
					// Process.destroy() closes the stream.
					Log.w(TAG, "Failed to read syncthing command line output", e);
				}
			}
		}).start();
	}

	/**
	 * Polls SYNCTHING_URL until it returns HTTP status OK, then calls all listeners
	 * in mOnWebGuiAvailableListeners and clears it.
	 */
	private class PollWebGuiAvailableTask extends AsyncTask<Void, Void, Void> {
		@Override
		protected Void doInBackground(Void... voids) {
			int status = 0;
			HttpClient httpclient = new DefaultHttpClient();
			HttpHead head = new HttpHead(mApi.getUrl());
			do {
				try {
					Thread.sleep(WEB_GUI_POLL_INTERVAL);
					HttpResponse response = httpclient.execute(head);
					// NOTE: status is not really needed, as HttpHostConnectException is thrown
					// earlier.
					status = response.getStatusLine().getStatusCode();
				}
				catch (HttpHostConnectException e) {
					// We catch this in every call, as long as the service is not online,
					// so we ignore and continue.
				}
				catch (IOException e) {
					Log.w(TAG, "Failed to poll for web interface", e);
				}
				catch (InterruptedException e) {
					Log.w(TAG, "Failed to poll for web interface", e);
				}
			} while(status != HttpStatus.SC_OK);
			return null;
		}

		@Override
		protected void onPostExecute(Void aVoid) {
			Log.i(TAG, "Web GUI has come online at " + mApi.getUrl());
			mIsWebGuiAvailable = true;
			for (OnWebGuiAvailableListener listener : mOnWebGuiAvailableListeners) {
				listener.onWebGuiAvailable();
			}
			mOnWebGuiAvailableListeners.clear();
		}
	}

	/**
	 * Move config file, keys, and index files to "official" folder
	 *
	 * Intended to bring the file locations in older installs in line with
	 * newer versions.
	 */
	private void moveConfigFiles() {
		FilenameFilter idxFilter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".idx.gz");
			}
		};

		if (new File(getApplicationInfo().dataDir, PUBLIC_KEY_FILE).exists()) {
			try {
				File publicKey = new File(getApplicationInfo().dataDir, PUBLIC_KEY_FILE);
				publicKey.renameTo(new File(getFilesDir(), PUBLIC_KEY_FILE));
				File privateKey = new File(getApplicationInfo().dataDir, PRIVATE_KEY_FILE);
				privateKey.renameTo(new File(getFilesDir(), PRIVATE_KEY_FILE));
				File config = new File(getApplicationInfo().dataDir, CONFIG_FILE);
				config.renameTo(new File(getFilesDir(), CONFIG_FILE));

				File oldStorageDir = new File(getApplicationInfo().dataDir);
				File[] files = oldStorageDir.listFiles(idxFilter);
				for (File file : files) {
					if (file.isFile()) {
						file.renameTo(new File(getFilesDir(), file.getName()));
					}
				}
			}
			catch (Exception e) {
				Log.e(TAG, "Failed to move config files", e);
			}
		}
	}

	/**
	 * Creates notification, starts native binary.
	 */
	@Override

	public void onCreate() {
		PendingIntent pi = PendingIntent.getActivity(
				this, 0, new Intent(this, WebGuiActivity.class),
				PendingIntent.FLAG_UPDATE_CURRENT);
		Notification n = new NotificationCompat.Builder(this)
				.setContentTitle(getString(R.string.app_name))
				.setSmallIcon(R.drawable.ic_launcher)
				.setContentIntent(pi)
				.build();
		n.flags |= Notification.FLAG_ONGOING_EVENT;
		startForeground(NOTIFICATION_ID, n);

		new Thread(new Runnable() {
			@Override
			public void run() {
				Looper.prepare();
				if (isFirstStart(SyncthingService.this)) {
					Log.i(TAG, "App started for the first time. " +
							"Copying default config, keys will be generated automatically");
					copyDefaultConfig();
				}
				moveConfigFiles();
{'fraction_non_alphanumeric': 0.10733599169716503, 'fraction_numerical': 0.0014202217730922599, 'mean_word_length': 3.805249343832021, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 20}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3230469', 'n_tokens_mistral': 6352, 'n_tokens_neox': 5453, 'n_words': 1575}