package com.nutomic.syncthingandroid.syncthing; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.activities.SettingsActivity; import com.nutomic.syncthingandroid.fragments.DeviceFragment; import com.nutomic.syncthingandroid.fragments.FolderFragment; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.util.concurrent.TimeUnit; /** * Run by the syncthing service to convert syncthing events into local broadcasts. * * It uses {@link RestApi#getEvents} to read the pending events and wait for new events. */ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListener, Runnable, RestApi.OnReceiveEventListener { private static final String TAG = "EventProcessor"; private static final String PREF_LAST_SYNC_ID = "last_sync_id"; /** * Minimum interval in seconds at which the events are polled from syncthing and processed. * This intervall will not wake up the device to save battery power. */ public static final long EVENT_UPDATE_INTERVAL = TimeUnit.SECONDS.toMillis(15); /** * Use the MainThread for all callbacks and message handling * or we have to track down nasty threading problems. */ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private volatile long mLastEventId = 0; private volatile boolean mShutdown = true; private final Context mContext; private final RestApi mApi; public EventProcessor(Context context, RestApi api) { mContext = context; mApi = api; } @Override public void run() { // Restore the last event id if the event processor may have been restartet. if (mLastEventId == 0) { mLastEventId = PreferenceManager.getDefaultSharedPreferences(mContext) .getLong(PREF_LAST_SYNC_ID, 0); } // First check if the event number ran backwards. // If that's the case we've to start at zero because syncthing was restarted. mApi.getEvents(0, 1, new RestApi.OnReceiveEventListener() { @Override public void onEvent(String eventType, JSONObject data) throws JSONException { } @Override public void onDone(long lastId) { if (lastId < mLastEventId) mLastEventId = 0; Log.d(TAG, "Reading events starting with id " + mLastEventId); mApi.getEvents(mLastEventId, 0, EventProcessor.this); } }); } /** * Performs the actual event handling. */ @Override public void onEvent(String type, JSONObject data) throws JSONException { switch (type) { case "DeviceRejected": String deviceId = data.getString("device"); Log.d(TAG, "Unknwon device " + deviceId + " wants to connect"); Intent intent = new Intent(mContext, SettingsActivity.class) .setAction(SettingsActivity.ACTION_DEVICE_SETTINGS) .putExtra(SettingsActivity.EXTRA_IS_CREATE, true) .putExtra(DeviceFragment.EXTRA_DEVICE_ID, deviceId); // HACK: Use a random, deterministic ID to make multiple PendingIntents // distinguishable int requestCode = deviceId.hashCode(); PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent, 0); String title = mContext.getString(R.string.device_rejected, deviceId.substring(0, 7)); notify(title, pi); break; case "FolderRejected": deviceId = data.getString("device"); String folderId = data.getString("folder"); String folderLabel = data.getString("folderLabel"); Log.d(TAG, "Device " + deviceId + " wants to share folder " + folderId); intent = new Intent(mContext, SettingsActivity.class) .setAction(SettingsActivity.ACTION_FOLDER_SETTINGS) .putExtra(SettingsActivity.EXTRA_IS_CREATE, true) .putExtra(FolderFragment.EXTRA_DEVICE_ID, deviceId) .putExtra(FolderFragment.EXTRA_FOLDER_ID, folderId) .putExtra(FolderFragment.EXTRA_FOLDER_LABEL, folderLabel); // HACK: Use a random, deterministic ID to make multiple PendingIntents // distinguishable requestCode = (deviceId + folderId + folderLabel).hashCode(); pi = PendingIntent.getActivity(mContext, requestCode, intent, 0); String deviceName = null; for (RestApi.Device d : mApi.getDevices(false)) { if (d.deviceID.equals(deviceId)) deviceName = RestApi.getDeviceDisplayName(d); } title = mContext.getString(R.string.folder_rejected, deviceName, folderLabel.isEmpty() ? folderId : folderLabel + " (" + folderId + ")"); notify(title, pi); break; case "ItemFinished": File updatedFile = new File(data.getString("folderpath"), data.getString("item")); Log.i(TAG, "Notified media scanner about " + updatedFile.toString()); mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(updatedFile))); break; case "Ping": // Ignored. break; default: Log.i(TAG, "Unhandled event " + type); } } @Override public void onDone(long id) { if (mLastEventId < id) { mLastEventId = id; // Store the last EventId in case we get killed final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); //noinspection CommitPrefEdits sp.edit().putLong(PREF_LAST_SYNC_ID, mLastEventId).apply(); } synchronized (mMainThreadHandler) { if (!mShutdown) { mMainThreadHandler.removeCallbacks(this); mMainThreadHandler.postDelayed(this, EVENT_UPDATE_INTERVAL); } } } @Override public void onWebGuiAvailable() { Log.d(TAG, "WebGUI available. Starting event processor."); // Remove all pending callbacks and add a new one. This makes sure that only one // event poller is running at any given time. synchronized (mMainThreadHandler) { mShutdown = false; mMainThreadHandler.removeCallbacks(this); mMainThreadHandler.postDelayed(this, EVENT_UPDATE_INTERVAL); } } public void shutdown() { Log.d(TAG, "Shutdown event processor."); synchronized (mMainThreadHandler) { mShutdown = true; mMainThreadHandler.removeCallbacks(this); } } private void notify(String text, PendingIntent pi) { NotificationManager nm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); Notification n = new NotificationCompat.Builder(mContext) .setContentTitle(mContext.getString(R.string.app_name)) .setContentText(text) .setStyle(new NotificationCompat.BigTextStyle() .bigText(text)) .setContentIntent(pi) .setSmallIcon(R.drawable.ic_stat_notify) .setAutoCancel(true) .build(); // HACK: Use a random, deterministic ID between 1000 and 2000 to avoid duplicate // notifications. int notificationId = 1000 + text.hashCode() % 1000; nm.notify(notificationId, n); } }