diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9baa18993..7e200215d 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -50,13 +50,6 @@ android:name=".gui.RepoSettingsActivity" /> - - - diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/FolderPickerActivity.java b/src/main/java/com/nutomic/syncthingandroid/gui/FolderPickerActivity.java deleted file mode 100644 index 802852e16..000000000 --- a/src/main/java/com/nutomic/syncthingandroid/gui/FolderPickerActivity.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.nutomic.syncthingandroid.gui; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.Environment; -import android.os.IBinder; -import android.support.v7.app.ActionBarActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -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 com.nutomic.syncthingandroid.R; -import com.nutomic.syncthingandroid.syncthing.SyncthingService; -import com.nutomic.syncthingandroid.syncthing.SyncthingServiceBinder; - -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.util.Arrays; -import java.util.Stack; - -/** - * Activity that allows selecting a directory in the local file system. - */ -public class FolderPickerActivity extends ActionBarActivity - implements AdapterView.OnItemClickListener, SyncthingService.OnApiChangeListener { - - private static final String TAG = "FolderPickerActivity"; - - public static final String EXTRA_INITIAL_DIRECTORY = "initial_directory"; - - public static final String EXTRA_RESULT_DIRECTORY = "result_directory"; - - private ListView mListView; - - private FileAdapter mAdapter; - - private File mLocation; - - private SyncthingService mSyncthingService; - - private final ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - SyncthingServiceBinder binder = (SyncthingServiceBinder) service; - mSyncthingService = binder.getService(); - mSyncthingService.registerOnApiChangeListener(FolderPickerActivity.this); - } - - public void onServiceDisconnected(ComponentName className) { - mSyncthingService = null; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.folder_picker_activity); - mListView = (ListView) findViewById(android.R.id.list); - mListView.setOnItemClickListener(this); - mListView.setEmptyView(findViewById(android.R.id.empty)); - mAdapter = new FileAdapter(this); - mListView.setAdapter(mAdapter); - - mLocation = new File(getIntent().getStringExtra(EXTRA_INITIAL_DIRECTORY)); - refresh(); - - bindService(new Intent(this, SyncthingService.class), - mSyncthingServiceConnection, Context.BIND_AUTO_CREATE); - } - - @Override - public void onDestroy() { - super.onDestroy(); - unbindService(mSyncthingServiceConnection); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // 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, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - createFolder(et.getText().toString()); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - dialog.setOnShowListener(new DialogInterface.OnShowListener() { - @Override - public void onShow(DialogInterface 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); - newFolder.mkdir(); - mLocation = newFolder; - refresh(); - } - - /** - * Refreshes the ListView to show the contents of the folder in {@code }mLocation.peek()}. - */ - private void refresh() { - mAdapter.clear(); - File[] contents = mLocation.listFiles(new FileFilter() { - @Override - public boolean accept(File file) { - return file.isDirectory(); - } - }); - Arrays.sort(contents); - for (File f : contents) { - mAdapter.add(f); - } - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - mLocation = mAdapter.getItem(i); - refresh(); - } - - private class FileAdapter extends ArrayAdapter { - - public FileAdapter(Context context) { - super(context, android.R.layout.simple_list_item_1); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); - } - - TextView title = (TextView) convertView.findViewById(android.R.id.text1); - title.setText(getItem(position).getName()); - return convertView; - } - } - - @Override - public void onBackPressed() { - if (!mLocation.equals(Environment.getExternalStorageDirectory())) { - mLocation = mLocation.getParentFile(); - refresh(); - } - else { - setResult(Activity.RESULT_CANCELED); - finish(); - } - } - - @Override - public void onApiChange(boolean isAvailable) { - if (!isAvailable) { - setResult(Activity.RESULT_CANCELED); - finish(); - } - } - -} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/RepoSettingsActivity.java b/src/main/java/com/nutomic/syncthingandroid/gui/RepoSettingsActivity.java index e5d6e2e20..50f0e5368 100644 --- a/src/main/java/com/nutomic/syncthingandroid/gui/RepoSettingsActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/gui/RepoSettingsActivity.java @@ -5,19 +5,25 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.ComponentName; +import android.content.ContentUris; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; +import android.database.Cursor; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.os.storage.StorageManager; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; +import android.provider.DocumentsContract; +import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; @@ -28,7 +34,9 @@ import com.nutomic.syncthingandroid.syncthing.SyncthingServiceBinder; import com.nutomic.syncthingandroid.util.ExtendedCheckBoxPreference; +import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; /** @@ -279,15 +287,22 @@ else if (preference.equals(mVersioningKeep)) { @Override public boolean onPreferenceClick(Preference preference) { if (preference.equals(mDirectory)) { - Intent intent = new Intent(this, FolderPickerActivity.class) - .putExtra(FolderPickerActivity.EXTRA_INITIAL_DIRECTORY, - (mRepo.Directory.length() != 0) - ? mRepo.Directory - : Environment.getExternalStorageDirectory().getAbsolutePath()); + Intent intent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + } + else { + intent = new Intent(Intent.ACTION_GET_CONTENT); + } + intent.addCategory(Intent.CATEGORY_OPENABLE) + .setType(DocumentsContract.Document.MIME_TYPE_DIR) + .putExtra(Intent.EXTRA_LOCAL_ONLY, true); startActivityForResult(intent, DIRECTORY_REQUEST_CODE); + return true; } else if (preference.equals(mNodes) && mSyncthingService.getApi().getNodes().isEmpty()) { Toast.makeText(this, R.string.no_nodes, Toast.LENGTH_SHORT).show(); + return true; } else if (preference.equals(mDelete)) { new AlertDialog.Builder(this) @@ -308,10 +323,20 @@ public void onClick(DialogInterface dialogInterface, int i) { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK && requestCode == DIRECTORY_REQUEST_CODE) { - mRepo.Directory = data.getStringExtra(FolderPickerActivity.EXTRA_RESULT_DIRECTORY); + if (requestCode == DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + Uri uri = data.getData(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getContentResolver().takePersistableUriPermission(uri, data.getFlags() + & (Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); + //mRepo.Directory = getPath(uri); + mRepo.Directory = uri.toString(); + } + else { + // TODO: properly get path (content:// URI) + mRepo.Directory = uri.getPath(); + } mDirectory.setSummary(mRepo.Directory); - repoUpdated(); } } @@ -321,4 +346,141 @@ private void repoUpdated() { } } + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders.
+ *
+ * Callers should check whether the path is local before assuming it + * represents a local file. + * + * @param uri The Uri to query. + * @author paulburke + */ + @TargetApi(19) + public String getPath(final Uri uri) { + + // DocumentProvider + if (DocumentsContract.isDocumentUri(this, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + + if ("primary".equalsIgnoreCase(split[0])) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + * @author paulburke + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + * @author paulburke + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + * @author paulburke + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + }/** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + * @author paulburke + */ + public String getDataColumn(Uri uri, String selection, + String[] selectionArgs) { + Cursor cursor = null; + + try { + cursor = getContentResolver().query(uri, new String[]{"_data"}, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow("_data"); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + } diff --git a/src/main/res/layout/folder_picker_activity.xml b/src/main/res/layout/folder_picker_activity.xml deleted file mode 100644 index 83c003de5..000000000 --- a/src/main/res/layout/folder_picker_activity.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/src/main/res/menu/folder_picker.xml b/src/main/res/menu/folder_picker.xml deleted file mode 100644 index 399a07da2..000000000 --- a/src/main/res/menu/folder_picker.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index fd126763f..52bcd329b 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -200,21 +200,6 @@ Please report any problems you encounter. Syncthing Version - - - - - Folder Picker - - - Directory is Empty - - - Create new Folder - - - Select Folder -