package com.nutomic.syncthingandroid.activities; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.support.v7.widget.SwitchCompat; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.common.base.Objects; import com.google.gson.Gson; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.fragments.dialog.KeepVersionsDialogFragment; import com.nutomic.syncthingandroid.model.Device; import com.nutomic.syncthingandroid.model.Folder; import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.util.TextWatcherAdapter; import java.util.List; import static android.support.v4.view.MarginLayoutParamsCompat.setMarginEnd; import static android.support.v4.view.MarginLayoutParamsCompat.setMarginStart; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import static android.view.Gravity.CENTER_VERTICAL; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.nutomic.syncthingandroid.service.SyncthingService.State.ACTIVE; import static java.lang.String.valueOf; /** * Shows folder details and allows changing them. */ public class FolderActivity extends SyncthingActivity implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener { public static final String EXTRA_IS_CREATE = "com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE"; public static final String EXTRA_FOLDER_ID = "com.nutomic.syncthingandroid.activities.FolderActivity.FOLDER_ID"; public static final String EXTRA_FOLDER_LABEL = "com.nutomic.syncthingandroid.activities.FolderActivity.FOLDER_LABEL"; public static final String EXTRA_DEVICE_ID = "com.nutomic.syncthingandroid.activities.FolderActivity.DEVICE_ID"; private static final int DIRECTORY_REQUEST_CODE = 234; private static final String TAG = "EditFolderFragment"; public static final String KEEP_VERSIONS_DIALOG_TAG = "KeepVersionsDialogFragment"; private static final String IS_SHOWING_DELETE_DIALOG = "DELETE_FOLDER_DIALOG_STATE"; private static final String IS_SHOW_DISCARD_DIALOG = "DISCARD_FOLDER_DIALOG_STATE"; private Folder mFolder; private EditText mLabelView; private EditText mIdView; private TextView mPathView; private SwitchCompat mFolderMasterView; private ViewGroup mDevicesContainer; private TextView mVersioningKeepView; private boolean mIsCreateMode; private boolean mFolderNeedsToUpdate; private Dialog mDeleteDialog; private Dialog mDiscardDialog; private final KeepVersionsDialogFragment mKeepVersionsDialogFragment = new KeepVersionsDialogFragment(); private final TextWatcher mTextWatcher = new TextWatcherAdapter() { @Override public void afterTextChanged(Editable s) { mFolder.label = mLabelView.getText().toString(); mFolder.id = mIdView.getText().toString(); mFolder.path = mPathView.getText().toString(); mFolderNeedsToUpdate = true; } }; private final CompoundButton.OnCheckedChangeListener mCheckedListener = new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton view, boolean isChecked) { switch (view.getId()) { case R.id.master: mFolder.type = (isChecked) ? "readonly" : "readwrite"; mFolderNeedsToUpdate = true; break; case R.id.device_toggle: Device device = (Device) view.getTag(); if (isChecked) { mFolder.addDevice(device.deviceID); } else { mFolder.removeDevice(device.deviceID); } mFolderNeedsToUpdate = true; break; } } }; private final KeepVersionsDialogFragment.OnValueChangeListener mOnValueChangeListener = new KeepVersionsDialogFragment.OnValueChangeListener() { @Override public void onValueChange(int intValue) { if (intValue == 0) { mFolder.versioning = new Folder.Versioning(); mVersioningKeepView.setText(R.string.off); } else { mFolder.versioning.type = "simple"; mFolder.versioning.params.put("keep", valueOf(intValue)); mVersioningKeepView.setText(valueOf(intValue)); } mFolderNeedsToUpdate = true; } }; private final View.OnClickListener mPathViewClickListener = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FolderActivity.this, FolderPickerActivity.class); if (!TextUtils.isEmpty(mFolder.path)) { intent.putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, mFolder.path); } startActivityForResult(intent, DIRECTORY_REQUEST_CODE); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_folder); mIsCreateMode = getIntent().getBooleanExtra(EXTRA_IS_CREATE, false); setTitle(mIsCreateMode ? R.string.create_folder : R.string.edit_folder); registerOnServiceConnectedListener(this); mLabelView = (EditText) findViewById(R.id.label); mIdView = (EditText) findViewById(R.id.id); mPathView = (TextView) findViewById(R.id.directory); mFolderMasterView = (SwitchCompat) findViewById(R.id.master); mVersioningKeepView = (TextView) findViewById(R.id.versioningKeep); mDevicesContainer = (ViewGroup) findViewById(R.id.devicesContainer); mPathView.setOnClickListener(mPathViewClickListener); findViewById(R.id.versioningContainer).setOnClickListener(v -> mKeepVersionsDialogFragment.show(getFragmentManager(), KEEP_VERSIONS_DIALOG_TAG)); if (mIsCreateMode) { if (savedInstanceState != null) { mFolder = new Gson().fromJson(savedInstanceState.getString("folder"), Folder.class); if (savedInstanceState.getBoolean(IS_SHOW_DISCARD_DIALOG)){ showDiscardDialog(); } } if (mFolder == null) { initFolder(); } // Open keyboard on label view in edit mode. mLabelView.requestFocus(); } else { prepareEditMode(); } if (savedInstanceState != null){ if (savedInstanceState.getBoolean(IS_SHOWING_DELETE_DIALOG)){ showDeleteDialog(); } } } @Override public void onDestroy() { super.onDestroy(); if (getService() != null) { getService().unregisterOnApiChangeListener(this); } mLabelView.removeTextChangedListener(mTextWatcher); mIdView.removeTextChangedListener(mTextWatcher); mPathView.removeTextChangedListener(mTextWatcher); } @Override public void onPause() { super.onPause(); // We don't want to update every time a TextView's character changes, // so we hold off until the view stops being visible to the user. if (mFolderNeedsToUpdate) { updateFolder(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(IS_SHOWING_DELETE_DIALOG, mDeleteDialog != null && mDeleteDialog.isShowing()); if (mDeleteDialog != null) { mDeleteDialog.cancel(); } if (mIsCreateMode){ outState.putBoolean(IS_SHOW_DISCARD_DIALOG, mDiscardDialog != null && mDiscardDialog.isShowing()); if(mDiscardDialog != null){ mDiscardDialog.cancel(); } } } /** * Save current settings in case we are in create mode and they aren't yet stored in the config. */ @Override public void onServiceConnected() { getService().registerOnApiChangeListener(this); } @Override public void onApiChange(SyncthingService.State currentState) { if (currentState != ACTIVE) { finish(); return; } if (!mIsCreateMode) { List<Folder> folders = getApi().getFolders(); String passedId = getIntent().getStringExtra(EXTRA_FOLDER_ID); mFolder = null; for (Folder currentFolder : folders) { if (currentFolder.id.equals(passedId)) { mFolder = currentFolder; break; } } if (mFolder == null) { Log.w(TAG, "Folder not found in API update, maybe it was deleted?"); finish(); return; } } if (getIntent().hasExtra(EXTRA_DEVICE_ID)) { mFolder.addDevice(getIntent().getStringExtra(EXTRA_DEVICE_ID)); mFolderNeedsToUpdate = true; } updateViewsAndSetListeners(); } private void updateViewsAndSetListeners() { mLabelView.removeTextChangedListener(mTextWatcher); mIdView.removeTextChangedListener(mTextWatcher); mPathView.removeTextChangedListener(mTextWatcher); mFolderMasterView.setOnCheckedChangeListener(null); mKeepVersionsDialogFragment.setOnValueChangeListener(null); // Update views mLabelView.setText(mFolder.label); mIdView.setText(mFolder.id); mPathView.setText(mFolder.path); mFolderMasterView.setChecked(Objects.equal(mFolder.type, "readonly")); List<Device> devicesList = getApi().getDevices(false); mDevicesContainer.removeAllViews(); if (devicesList.isEmpty()) { addEmptyDeviceListView(); } else { for (Device n : devicesList) { addDeviceViewAndSetListener(n, getLayoutInflater()); } } boolean versioningEnabled = Objects.equal(mFolder.versioning.type, "simple"); int versions = 0; if (versioningEnabled) { versions = Integer.valueOf(mFolder.versioning.params.get("keep")); mVersioningKeepView.setText(valueOf(versions)); } else { mVersioningKeepView.setText(R.string.off); } mKeepVersionsDialogFragment.setValue(versions); // Keep state updated mLabelView.addTextChangedListener(mTextWatcher); mIdView.addTextChangedListener(mTextWatcher); mPathView.addTextChangedListener(mTextWatcher); mFolderMasterView.setOnCheckedChangeListener(mCheckedListener); mKeepVersionsDialogFragment.setOnValueChangeListener(mOnValueChangeListener); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.folder_settings, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.create).setVisible(mIsCreateMode); menu.findItem(R.id.remove).setVisible(!mIsCreateMode); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.create: if (TextUtils.isEmpty(mFolder.id)) { Toast.makeText(this, R.string.folder_id_required, Toast.LENGTH_LONG) .show(); return true; } if (TextUtils.isEmpty(mFolder.path)) { Toast.makeText(this, R.string.folder_path_required, Toast.LENGTH_LONG) .show(); return true; } getApi().addFolder(mFolder); finish(); return true; case R.id.remove: showDeleteDialog(); return true; case android.R.id.home: onBackPressed(); return true; default: return super.onOptionsItemSelected(item); } } private void showDeleteDialog(){ mDeleteDialog = createDeleteDialog(); mDeleteDialog.show(); } private Dialog createDeleteDialog(){ return new AlertDialog.Builder(this) .setMessage(R.string.remove_folder_confirm) .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> { getApi().removeFolder(mFolder.id); finish(); }) .setNegativeButton(android.R.string.no, null) .create(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && requestCode == DIRECTORY_REQUEST_CODE) { mFolder.path = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY); mPathView.setText(mFolder.path); mFolderNeedsToUpdate = true; } } private void initFolder() { mFolder = new Folder(); mFolder.id = getIntent().getStringExtra(EXTRA_FOLDER_ID); mFolder.label = getIntent().getStringExtra(EXTRA_FOLDER_LABEL); if (Build.VERSION.SDK_INT != Build.VERSION_CODES.M) { // Scan every 3 days (in case inotify dropped some changes) mFolder.rescanIntervalS = 259200; } else { // FileObserver is broken on Marshmallow. // https://github.com/syncthing/syncthing-android/issues/787 mFolder.rescanIntervalS = 60; } mFolder.versioning = new Folder.Versioning(); } private void prepareEditMode() { mIdView.clearFocus(); mIdView.setFocusable(false); mIdView.setEnabled(false); mPathView.setEnabled(false); } private void addEmptyDeviceListView() { int height = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(WRAP_CONTENT, height); int dividerInset = getResources().getDimensionPixelOffset(R.dimen.material_divider_inset); int contentInset = getResources().getDimensionPixelOffset(R.dimen.abc_action_bar_content_inset_material); setMarginStart(params, dividerInset); setMarginEnd(params, contentInset); TextView emptyView = new TextView(mDevicesContainer.getContext()); emptyView.setGravity(CENTER_VERTICAL); emptyView.setText(R.string.devices_list_empty); mDevicesContainer.addView(emptyView, params); } private void addDeviceViewAndSetListener(Device device, LayoutInflater inflater) { inflater.inflate(R.layout.item_device_form, mDevicesContainer); SwitchCompat deviceView = (SwitchCompat) mDevicesContainer.getChildAt(mDevicesContainer.getChildCount()-1); deviceView.setChecked(mFolder.getDevice(device.deviceID) != null); deviceView.setText(device.getDisplayName()); deviceView.setTag(device); deviceView.setOnCheckedChangeListener(mCheckedListener); } private void updateFolder() { if (!mIsCreateMode) { getApi().editFolder(mFolder); } } @Override public void onBackPressed() { if (mIsCreateMode) { showDiscardDialog(); } else { super.onBackPressed(); } } private void showDiscardDialog(){ mDiscardDialog = createDiscardDialog(); mDiscardDialog.show(); } private Dialog createDiscardDialog() { return new AlertDialog.Builder(this) .setMessage(R.string.dialog_discard_changes) .setPositiveButton(android.R.string.ok, (dialog, which) -> finish()) .setNegativeButton(android.R.string.cancel, null) .create(); } }