package com.nutomic.syncthingandroid.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.collect.Sets;
import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.service.SyncthingService;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
/**
* Activity that allows selecting a directory in the local file system.
*/
public class FolderPickerActivity extends SyncthingActivity
implements AdapterView.OnItemClickListener, SyncthingService.OnApiChangeListener {
private static final String TAG = "FolderPickerActivity";
public static final String EXTRA_INITIAL_DIRECTORY =
"com.nutomic.syncthingandroid.activities.FolderPickerActivity.INITIAL_DIRECTORY";
public static final String EXTRA_RESULT_DIRECTORY =
"com.nutomic.syncthingandroid.activities.FolderPickerActivity.RESULT_DIRECTORY";
private ListView mListView;
private FileAdapter mFilesAdapter;
private RootsAdapter mRootsAdapter;
/**
* Location of null means that the list of roots is displayed.
*/
private File mLocation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_folder_picker);
mListView = (ListView) findViewById(android.R.id.list);
mListView.setOnItemClickListener(this);
mListView.setEmptyView(findViewById(android.R.id.empty));
mFilesAdapter = new FileAdapter(this);
mRootsAdapter = new RootsAdapter(this);
mListView.setAdapter(mFilesAdapter);
populateRoots();
if (getIntent().hasExtra(EXTRA_INITIAL_DIRECTORY)) {
displayFolder(new File(getIntent().getStringExtra(EXTRA_INITIAL_DIRECTORY)));
} else {
displayRoot();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Toast.makeText(this, R.string.kitkat_external_storage_warning, Toast.LENGTH_LONG)
.show();
}
}
/**
* Reads available storage devices/folders from various APIs and inserts them into
* {@link #mRootsAdapter}.
*/
@SuppressLint("NewApi")
private void populateRoots() {
ArrayList<File> roots = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
roots.addAll(Arrays.asList(getExternalFilesDirs(null)));
roots.remove(getExternalFilesDir(null));
}
roots.add(Environment.getExternalStorageDirectory());
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC));
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
roots.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS));
}
// Add paths that might not be accessible to Syncthing.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
if (sp.getBoolean("advanced_folder_picker", false)) {
Collections.addAll(roots, new File("/storage/").listFiles());
roots.add(new File("/"));
}
// Remove any invalid directories.
Iterator<File> it = roots.iterator();
while (it.hasNext()) {
File f = it.next();
if (f == null || !f.exists() || !f.isDirectory()) {
it.remove();
}
}
mRootsAdapter.addAll(Sets.newTreeSet(roots));
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
super.onServiceConnected(componentName, iBinder);
getService().registerOnApiChangeListener(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
getService().unregisterOnApiChangeListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (mListView.getAdapter() == mRootsAdapter)
return true;
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.folder_picker, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.create_folder:
final EditText et = new EditText(this);
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(R.string.create_folder)
.setView(et)
.setPositiveButton(android.R.string.ok,
(dialogInterface, i) -> createFolder(et.getText().toString())
)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(dialogInterface -> ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE))
.showSoftInput(et, InputMethodManager.SHOW_IMPLICIT));
dialog.show();
return true;
case R.id.select:
Intent intent = new Intent()
.putExtra(EXTRA_RESULT_DIRECTORY, mLocation.getAbsolutePath());
setResult(Activity.RESULT_OK, intent);
finish();
return true;
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Creates a new folder with the given name and enters it.
*/
private void createFolder(String name) {
File newFolder = new File(mLocation, name);
if (newFolder.mkdir()) {
displayFolder(newFolder);
} else {
Toast.makeText(this, R.string.create_folder_failed, Toast.LENGTH_SHORT).show();
}
}
/**
* Refreshes the ListView to show the contents of the folder in {@code }mLocation.peek()}.
*/
private void displayFolder(File folder) {
mLocation = folder;
mFilesAdapter.clear();
File[] contents = mLocation.listFiles();
// In case we don't have read access to the folder, just display nothing.
if (contents == null)
contents = new File[]{};
Arrays.sort(contents, (f1, f2) -> {
if (f1.isDirectory() && f2.isFile())
return -1;
if (f1.isFile() && f2.isDirectory())
return 1;
return f1.getName().compareTo(f2.getName());
});
for (File f : contents) {
mFilesAdapter.add(f);
}
mListView.setAdapter(mFilesAdapter);
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
@SuppressWarnings("unchecked")
ArrayAdapter<File> adapter = (ArrayAdapter<File>) mListView.getAdapter();
File f = adapter.getItem(i);
if (f.isDirectory()) {
displayFolder(f);
invalidateOptions();
}
}
private void invalidateOptions() {
invalidateOptionsMenu();
}
private class FileAdapter extends ArrayAdapter<File> {
public FileAdapter(Context context) {
super(context, R.layout.item_folder_picker);
}
@Override
@NonNull
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
convertView = super.getView(position, convertView, parent);
TextView title = (TextView) convertView.findViewById(android.R.id.text1);
File f = getItem(position);
title.setText(f.getName());
int textColor = (f.isDirectory())
? android.R.color.primary_text_light
: android.R.color.tertiary_text_light;
title.setTextColor(ContextCompat.getColor(getContext(), textColor));
return convertView;
}
}
private class RootsAdapter extends ArrayAdapter<File> {
public RootsAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1);
}
@Override
@NonNull
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
convertView = super.getView(position, convertView, parent);
TextView title = (TextView) convertView.findViewById(android.R.id.text1);
title.setText(getItem(position).getAbsolutePath());
return convertView;
}
public boolean contains(File file) {
for (int i = 0; i < getCount(); i++) {
if (getItem(i).equals(file))
return true;
}
return false;
}
}
/**
* Goes up a directory, up to the list of roots if there are multiple roots.
*
* If we already are in the list of roots, or if we are directly in the only
* root folder, we cancel.
*/
@Override
public void onBackPressed() {
if (!mRootsAdapter.contains(mLocation) && mLocation != null) {
displayFolder(mLocation.getParentFile());
} else if (mRootsAdapter.contains(mLocation) && mRootsAdapter.getCount() > 1) {
displayRoot();
} else {
setResult(Activity.RESULT_CANCELED);
finish();
}
}
@Override
public void onApiChange(SyncthingService.State currentState) {
if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) {
setResult(Activity.RESULT_CANCELED);
finish();
}
}
/**
* Displays a list of all available roots, or if there is only one root, the
* contents of that folder.
*/
private void displayRoot() {
mFilesAdapter.clear();
if (mRootsAdapter.getCount() == 1) {
displayFolder(mRootsAdapter.getItem(0));
} else {
mListView.setAdapter(mRootsAdapter);
mLocation = null;
}
invalidateOptions();
}
}