diff options
17 files changed, 1125 insertions, 6 deletions
diff --git a/android/source/AndroidManifest.xml b/android/source/AndroidManifest.xml index d49771ab9d3f..25e824074ac9 100644 --- a/android/source/AndroidManifest.xml +++ b/android/source/AndroidManifest.xml @@ -113,6 +113,16 @@ </intent-filter> </activity> + <activity android:name=".storage.external.BrowserSelectorActivity" + android:theme="@style/LibreOfficeTheme"> + </activity> + + <activity android:name=".storage.external.DirectoryBrowserActivity" + android:label="@string/directory_browser_label" + android:theme="@style/LibreOfficeTheme" + android:windowSoftInputMode="stateHidden"> + </activity> + </application> </manifest> diff --git a/android/source/res/drawable/ic_menu_back.png b/android/source/res/drawable/ic_menu_back.png Binary files differnew file mode 100644 index 000000000000..d3191caffd13 --- /dev/null +++ b/android/source/res/drawable/ic_menu_back.png diff --git a/android/source/res/layout/activity_directory_browser.xml b/android/source/res/layout/activity_directory_browser.xml new file mode 100644 index 000000000000..b03c6bbb1224 --- /dev/null +++ b/android/source/res/layout/activity_directory_browser.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/fragment_container" + android:layout_width="match_parent" android:layout_height="match_parent"> + +</FrameLayout>
\ No newline at end of file diff --git a/android/source/res/layout/fragment_directory_browser.xml b/android/source/res/layout/fragment_directory_browser.xml new file mode 100644 index 000000000000..fcf7fc6c9b47 --- /dev/null +++ b/android/source/res/layout/fragment_directory_browser.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#FFFFFF"> + + <LinearLayout + android:id="@+id/browser_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/up_image" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:src="@drawable/ic_menu_back" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:contentDescription="@string/up_description"/> + + <EditText + android:id="@+id/directory_header" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:singleLine="true" + android:scrollHorizontally="true" + android:textAppearance="?android:attr/textAppearanceMedium" + android:inputType="text"/> + + <Button + android:id="@+id/directory_search_button" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:text="@string/search_label"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/browser_footer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_alignParentBottom="true"> + + <Button + android:id="@+id/cancel_button" + android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_weight="1" + android:text="@string/cancel_label"/> + + <Button + android:id="@+id/confirm_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/confirm_label"/> + </LinearLayout> + + <ListView + android:id="@+id/directory_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/browser_header" + android:layout_above="@id/browser_footer"> + </ListView> + + +</RelativeLayout>
\ No newline at end of file diff --git a/android/source/res/values/strings.xml b/android/source/res/values/strings.xml index 403f5b900c3f..c4d1bae404c5 100644 --- a/android/source/res/values/strings.xml +++ b/android/source/res/values/strings.xml @@ -51,12 +51,18 @@ <string name="close_document_locations">Close document locations</string> <string name="local_documents">Local documents</string> <string name="local_file_system">Local file system</string> + <string name="external_sd_file_system">External SD</string> + <string name="otg_file_system">OTG device (experimental)</string> <string name="owncloud">ownCloud</string> <string name="owncloud_wrong_connection">Cannot connect to ownCloud server. Check your configuration.</string> <string name="owncloud_unauthorized">Cannot log into ownCloud server. Check your configuration.</string> <string name="owncloud_unspecified_error">Unspecified error connecting to ownCloud server. Check your configuration and/or try later.</string> + <string name="ext_document_provider_error">Invalid root file. Check your configuration.</string> + <string name="legacy_extsd_missing_error">Invalid root file. Check your external sd card and/or configuration.</string> + <string name="otg_missing_error">Invalid root file. Check your OTG device and/or configuration.</string> + <!-- Edit action names --> <string name="action_bold">Bold</string> <string name="action_underline">Underline</string> @@ -75,6 +81,10 @@ <!-- Document provider settings --> <string name="storage_provider_settings">Storage provider settings</string> <string name="owncloud_settings">ownCloud settings</string> + <string name="physical_storage_settings">Physical storage settings</string> + <string name="external_sd_path">External SD path</string> + <string name="otg_device_path">OTG device path</string> + <string name="otg_warning">Experimental Feature: Use only if OTG device is writable.</string> <string name="server_url">Server URL</string> <string name="server_url_and_port">URL and port of the ownCloud server.</string> <string name="user_name">User name</string> @@ -82,5 +92,13 @@ <string name="action_undo">Undo</string> <string name="action_redo">Redo</string> + <!-- Directory browser strings --> + <string name="up_description">To parent directory</string> + <string name="confirm_label">Confirm</string> + <string name="cancel_label">Cancel</string> + <string name="search_label">Go</string> + <string name="directory_browser_label">Choose Directory</string> + <string name="bad_directory">Invalid directory path</string> + </resources> diff --git a/android/source/res/xml/documentprovider_preferences.xml b/android/source/res/xml/documentprovider_preferences.xml index a359d14c4460..4a66c6377847 100644 --- a/android/source/res/xml/documentprovider_preferences.xml +++ b/android/source/res/xml/documentprovider_preferences.xml @@ -23,4 +23,17 @@ android:title="@string/password" android:defaultValue="" /> </PreferenceCategory> + <PreferenceCategory + android:title="@string/physical_storage_settings"> + <PreferenceScreen + android:title="@string/external_sd_path" + android:key="pref_extsd_path_uri"> + </PreferenceScreen> + <PreferenceScreen + android:title="@string/otg_device_path" + android:key="pref_otg_path_uri" + android:summary="@string/otg_warning"> + </PreferenceScreen> + </PreferenceCategory> + </PreferenceScreen> diff --git a/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java b/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java index b8c05341d18a..f73a2b0d543a 100644 --- a/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java +++ b/android/source/src/java/org/libreoffice/storage/DocumentProviderFactory.java @@ -12,12 +12,16 @@ package org.libreoffice.storage; import java.util.HashSet; import java.util.Set; +import org.libreoffice.storage.external.ExtsdDocumentsProvider; +import org.libreoffice.storage.external.LegacyExtSDDocumentsProvider; +import org.libreoffice.storage.external.OTGDocumentsProvider; import org.libreoffice.storage.local.LocalDocumentsDirectoryProvider; import org.libreoffice.storage.local.LocalDocumentsProvider; import org.libreoffice.storage.owncloud.OwnCloudProvider; import android.content.Context; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Build; /** * Keeps the instances of the available IDocumentProviders in the system. @@ -29,6 +33,8 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; * DocumentProviderFactory.getInstance(). */ public final class DocumentProviderFactory { + public static int EXTSD_PROVIDER_INDEX = 2; + public static int OTG_PROVIDER_INDEX = 3; /** * Private factory instance for the Singleton pattern. @@ -56,10 +62,19 @@ public final class DocumentProviderFactory { instance = new DocumentProviderFactory(); // initialize document providers list - instance.providers = new IDocumentProvider[3]; + instance.providers = new IDocumentProvider[5]; instance.providers[0] = new LocalDocumentsDirectoryProvider(0); instance.providers[1] = new LocalDocumentsProvider(1); - instance.providers[2] = new OwnCloudProvider(2, context); + instance.providers[OTG_PROVIDER_INDEX] = new OTGDocumentsProvider(OTG_PROVIDER_INDEX, context); + instance.providers[4] = new OwnCloudProvider(4, context); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + instance.providers[EXTSD_PROVIDER_INDEX] + = new ExtsdDocumentsProvider(EXTSD_PROVIDER_INDEX, context); + } else { + instance.providers[EXTSD_PROVIDER_INDEX] + = new LegacyExtSDDocumentsProvider(EXTSD_PROVIDER_INDEX, context); + } // initialize document provider names list instance.providerNames = new String[instance.providers.length]; diff --git a/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java b/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java index e98534a44756..7b2160a3f509 100644 --- a/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java +++ b/android/source/src/java/org/libreoffice/storage/DocumentProviderSettingsActivity.java @@ -12,25 +12,30 @@ package org.libreoffice.storage; import java.util.Set; import org.libreoffice.R; +import org.libreoffice.storage.external.BrowserSelectorActivity; import android.app.Activity; +import android.content.Intent; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; public class DocumentProviderSettingsActivity extends Activity { public static final String KEY_PREF_OWNCLOUD_SERVER = "pref_server_url"; public static final String KEY_PREF_OWNCLOUD_USER_NAME = "pref_user_name"; public static final String KEY_PREF_OWNCLOUD_PASSWORD = "pref_password"; + public static final String KEY_PREF_EXTERNAL_SD_PATH_URI = "pref_extsd_path_uri"; + public static final String KEY_PREF_OTG_PATH_URI = "pref_otg_path_uri"; private Set<OnSharedPreferenceChangeListener> listeners; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Display the fragment as the main content. getFragmentManager().beginTransaction() .replace(android.R.id.content, new SettingsFragment()).commit(); @@ -39,7 +44,6 @@ public class DocumentProviderSettingsActivity extends Activity { @Override protected void onResume() { super.onResume(); - listeners = DocumentProviderFactory.getInstance().getChangeListeners(); for (OnSharedPreferenceChangeListener listener : listeners) { PreferenceManager.getDefaultSharedPreferences(this) @@ -50,7 +54,6 @@ public class DocumentProviderSettingsActivity extends Activity { @Override protected void onPause() { super.onPause(); - for (OnSharedPreferenceChangeListener listener : listeners) { PreferenceManager.getDefaultSharedPreferences(this) .unregisterOnSharedPreferenceChangeListener(listener); @@ -61,9 +64,39 @@ public class DocumentProviderSettingsActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Load the preferences from an XML resource addPreferencesFromResource(R.xml.documentprovider_preferences); + + PreferenceScreen extSDPreference = + (PreferenceScreen)findPreference(KEY_PREF_EXTERNAL_SD_PATH_URI); + PreferenceScreen otgPreference = + (PreferenceScreen)findPreference(KEY_PREF_OTG_PATH_URI); + + extSDPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startBrowserSelectorActivity(KEY_PREF_EXTERNAL_SD_PATH_URI, + BrowserSelectorActivity.MODE_EXT_SD); + return true; + } + }); + otgPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startBrowserSelectorActivity(KEY_PREF_OTG_PATH_URI, + BrowserSelectorActivity.MODE_OTG); + return true; + } + }); + } + + private void startBrowserSelectorActivity(String prefKey, String mode) { + Intent i = new Intent(getActivity(), BrowserSelectorActivity.class); + i.putExtra(BrowserSelectorActivity.PREFERENCE_KEY_EXTRA, prefKey); + i.putExtra(BrowserSelectorActivity.MODE_EXTRA, mode); + startActivity(i); + } + } } diff --git a/android/source/src/java/org/libreoffice/storage/IOUtils.java b/android/source/src/java/org/libreoffice/storage/IOUtils.java new file mode 100644 index 000000000000..0cb7b2e0ecec --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/IOUtils.java @@ -0,0 +1,59 @@ +package org.libreoffice.storage; + +import android.content.Context; +import android.util.Log; + +import org.libreoffice.R; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * File IO related methods. + */ +public class IOUtils { + private static final int BUFFER_SIZE = 1024 * 8; + private static final String LOGTAG = IOUtils.class.getSimpleName(); + + public static File getFileFromURIString(String URIpath) throws IllegalArgumentException{ + try{ + return new File(new URI(URIpath)); + } catch (URISyntaxException e) { + //should not happen as all URIs are system generated + Log.wtf(LOGTAG, e.getReason()); + return null; + } + } + + public static boolean isInvalidFile(File f) { + return f == null || !f.exists() || f.getTotalSpace() == 0 + || !f.canRead() || !f.canWrite(); + } + + public static int copy(InputStream input, OutputStream output) throws Exception { + byte[] buffer = new byte[BUFFER_SIZE]; + + BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); + BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); + + int count = 0, n = 0; + try { + while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { + out.write(buffer, 0, n); + count += n; + } + out.flush(); + } finally { + if (out != null) out.close(); + if (in != null) in.close(); + } + + return count; + } + +} diff --git a/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java b/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java new file mode 100644 index 000000000000..fe12804e620c --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/BrowserSelectorActivity.java @@ -0,0 +1,152 @@ +package org.libreoffice.storage.external; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.UriPermission; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; + +import org.libreoffice.storage.DocumentProviderFactory; + +import java.util.Set; + +/** + * Activity to select which directory browser to use. + * Android 5+ will use the DocumentTree intent to locate a browser. + * Android 4+ & OTG will use the internal directory browser. + */ +public class BrowserSelectorActivity extends AppCompatActivity { + public static final String PREFERENCE_KEY_EXTRA = "org.libreoffice.pref_key_extra"; + public static final String MODE_EXTRA = "org.libreoffice.mode_extra"; + public static final String MODE_OTG = "OTG"; + public static final String MODE_EXT_SD = "EXT_SD"; + + private static final String LOGTAG = BrowserSelectorActivity.class.getSimpleName(); + private static final int REQUEST_DOCUMENT_TREE = 1; + private static final int REQUEST_INTERNAL_BROWSER = 2; + private Set<SharedPreferences.OnSharedPreferenceChangeListener> listeners; + private String preferenceKey; + private SharedPreferences preferences; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + preferenceKey = getIntent().getStringExtra(PREFERENCE_KEY_EXTRA); + preferences = PreferenceManager.getDefaultSharedPreferences(this); + String mode = getIntent().getStringExtra(MODE_EXTRA); + + if(mode.equals(MODE_EXT_SD)) { + findSDCard(); + } else if (mode.equals(MODE_OTG)) { + findOTGDevice(); + } + } + + private void findOTGDevice() { + useInternalBrowser(DocumentProviderFactory.OTG_PROVIDER_INDEX); + } + + private void findSDCard() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + useDocumentTreeBrowser(); + } else { + useInternalBrowser(DocumentProviderFactory.EXTSD_PROVIDER_INDEX); + } + } + + private void useInternalBrowser(int providerIndex) { + IExternalDocumentProvider provider = + (IExternalDocumentProvider) DocumentProviderFactory.getInstance() + .getProvider(providerIndex); + String previousDirectoryPath = preferences.getString(preferenceKey, provider.guessRootURI()); + Intent i = new Intent(this, DirectoryBrowserActivity.class); + i.putExtra(DirectoryBrowserActivity.DIRECTORY_PATH_EXTRA, previousDirectoryPath); + startActivityForResult(i, REQUEST_INTERNAL_BROWSER); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void useDocumentTreeBrowser() { + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(i, REQUEST_DOCUMENT_TREE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + //listeners are registered here as onActivityResult is called before onResume + super.onActivityResult(requestCode, resultCode, data); + + registerListeners(); + if(resultCode == RESULT_OK) { + switch(requestCode) { + case REQUEST_DOCUMENT_TREE: + Uri treeUri = data.getData(); + preferences.edit() + .putString(preferenceKey, treeUri.toString()) + .apply(); + + updatePersistedUriPermission(treeUri); + getContentResolver().takePersistableUriPermission(treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + break; + + case REQUEST_INTERNAL_BROWSER: + Uri fileUri = data.getData(); + preferences.edit() + .putString(preferenceKey, fileUri.toString()) + .apply(); + break; + default: + } + } + unregisterListeners(); + Log.d(LOGTAG, "Preference saved: " + + preferences.getString(preferenceKey, "Directory not saved.")); + finish(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void updatePersistedUriPermission(Uri treeUri) { + freePreviousUriPermissions(); + + //TODO: Use non-emulator Android 5+ device to check if needed + /*this.grantUriPermission(this.getPackageName(), + treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION); */ + + getContentResolver().takePersistableUriPermission(treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void freePreviousUriPermissions() { + ContentResolver cr = getContentResolver(); + for (UriPermission uriPermission : cr.getPersistedUriPermissions()) { + cr.releasePersistableUriPermission(uriPermission.getUri(), 0); + } + } + + private void registerListeners() { + listeners = DocumentProviderFactory.getInstance().getChangeListeners(); + for (SharedPreferences.OnSharedPreferenceChangeListener listener : listeners) { + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(listener); + } + } + + private void unregisterListeners() { + for (SharedPreferences.OnSharedPreferenceChangeListener listener : listeners) { + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(listener); + } + } +} diff --git a/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java new file mode 100644 index 000000000000..224526adb17b --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserActivity.java @@ -0,0 +1,42 @@ +package org.libreoffice.storage.external; + + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; + +import org.libreoffice.R; + +/** + * Container for DirectoryBrowserFragment + */ +public class DirectoryBrowserActivity extends AppCompatActivity { + public static final String DIRECTORY_PATH_EXTRA = "org.libreoffie.directory_path_extra"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent data = getIntent(); + String initialPath = data.getStringExtra(DIRECTORY_PATH_EXTRA); + + setContentView(R.layout.activity_directory_browser); + FragmentManager fm = getFragmentManager(); + Fragment fragment = DirectoryBrowserFragment.newInstance(initialPath); + fm.beginTransaction() + .add(R.id.fragment_container, fragment) + .commit(); + } + + @Override + public void onBackPressed() { + FragmentManager fm = getFragmentManager(); + if(fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); + } else { + super.onBackPressed(); + } + } +} diff --git a/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java new file mode 100644 index 000000000000..27cbfbce14be --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/DirectoryBrowserFragment.java @@ -0,0 +1,199 @@ +package org.libreoffice.storage.external; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import org.libreoffice.R; +import org.libreoffice.storage.IOUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; + +/** + * A simple directory browser. + */ +public class DirectoryBrowserFragment extends Fragment { + private static final String LOGTAG = DirectoryBrowserFragment.class.getSimpleName(); + private static final String INITIAL_PATH_URI_KEY = "initial_path"; + private File currentDirectory; + private FileArrayAdapter directoryAdapter; + + public static DirectoryBrowserFragment newInstance(String initialPathURI) { + Bundle args = new Bundle(); + args.putString(INITIAL_PATH_URI_KEY, initialPathURI); + DirectoryBrowserFragment fragment = new DirectoryBrowserFragment(); + fragment.setArguments(args); + Log.d(LOGTAG, "Saved path: " + initialPathURI); + + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String initialPathURI = getArguments().getString(INITIAL_PATH_URI_KEY); + setupCurrentDirectory(initialPathURI); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_directory_browser, container, false); + + final EditText directoryHeader = (EditText)v.findViewById(R.id.directory_header); + Button directorySearchButton = (Button)v.findViewById(R.id.directory_search_button); + Button positiveButton = (Button)v.findViewById(R.id.confirm_button); + Button negativeButton = (Button)v.findViewById(R.id.cancel_button); + ImageView upImage = (ImageView)v.findViewById(R.id.up_image); + ListView directoryListView = (ListView) v.findViewById(R.id.directory_list); + + directoryHeader.setText(currentDirectory.getPath()); + directorySearchButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String currentPath = currentDirectory.getAbsolutePath(); + String enteredPath = directoryHeader.getText().toString(); + File testDirectory = new File(enteredPath); + if(enteredPath.equals(currentPath)) ; + else if (isInvalidFileDirectory(testDirectory)) { + Toast.makeText(getActivity(), R.string.bad_directory, Toast.LENGTH_SHORT) + .show(); + } + else { + changeDirectory(testDirectory); + } + } + }); + + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent data = new Intent(); + data.setData(Uri.fromFile(currentDirectory)); + getActivity().setResult(Activity.RESULT_OK, data); + getActivity().finish(); + } + }); + + negativeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().setResult(Activity.RESULT_CANCELED, null); + getActivity().finish(); + } + }); + + upImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + changeDirectory(currentDirectory.getParentFile()); + } + }); + + directoryAdapter = new FileArrayAdapter(getActivity(), new ArrayList<File>()); + directoryAdapter.populateFileList(currentDirectory); + directoryListView.setAdapter(directoryAdapter); + directoryListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + changeDirectory(directoryAdapter.getItem(position)); + } + }); + + return v; + } + + private void changeDirectory(File destination) { + if(destination == null) { + Toast.makeText(getActivity(), "Unable to go further.", Toast.LENGTH_SHORT) + .show(); + } else { + Fragment fragment = DirectoryBrowserFragment.newInstance(destination.toURI().toString()); + getActivity().getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, fragment) + .addToBackStack(null) + .commit(); + } + } + + private void setupCurrentDirectory(String initialPathURI) { + File initialDirectory = null; + if(initialPathURI != null && !initialPathURI.isEmpty()) { + initialDirectory = IOUtils.getFileFromURIString(initialPathURI); + } + + if(isInvalidFileDirectory(initialDirectory)) { + initialDirectory = Environment.getExternalStorageDirectory(); + } + currentDirectory = initialDirectory; + } + + private boolean isInvalidFileDirectory(File f) { + return f == null || !f.exists() || !f.isDirectory() ||!f.canRead(); + } + + private class FileArrayAdapter extends ArrayAdapter<File> { + private Comparator<File> caseInsensitiveNaturalOrderComparator; + + public FileArrayAdapter(Context context, ArrayList<File> files) { + super(context, 0, files); + caseInsensitiveNaturalOrderComparator = new AlphabeticalFileComparator(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = getActivity().getLayoutInflater() + .inflate(android.R.layout.simple_list_item_1, null); + } + + File f = this.getItem(position); + TextView tv = (TextView) convertView.findViewById(android.R.id.text1); + tv.setText(f.getName()); + + return convertView; + } + + public void sortAlphabetically() { + this.sort(caseInsensitiveNaturalOrderComparator); + } + + public void populateFileList(File directory) { + for(File f : directory.listFiles()){ + if(f.isDirectory()){ + directoryAdapter.add(f); + } + } + directoryAdapter.sortAlphabetically(); + } + } + + private class AlphabeticalFileComparator implements Comparator<File> { + @Override + public int compare(File lhs, File rhs) { + String lhsName = lhs.getName(); + String rhsName = rhs.getName(); + + return lhsName.compareToIgnoreCase(rhsName); + } + } +} diff --git a/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java b/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java new file mode 100644 index 000000000000..638111068ddd --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/ExternalFile.java @@ -0,0 +1,149 @@ +package org.libreoffice.storage.external; + +import android.content.Context; +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import org.libreoffice.storage.IFile; +import org.libreoffice.storage.IOUtils; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Implementation of IFile for the external file system, for Android 4.4+ + * + * Uses the DocumentFile class. + * + * The DocumentFile class obfuscates the path of the files it wraps, + * preventing usage of LOK's documentLoad method. A copy of the DocumentFile's contents + * will be created in the cache when files are opened, allowing use of documentLoad. + */ +public class ExternalFile implements IFile{ + private final static String LOGTAG = "ExternalFile"; + + private ExtsdDocumentsProvider provider; + private DocumentFile docFile; + private File duplicateFile; + private Context context; + + public ExternalFile(ExtsdDocumentsProvider provider, DocumentFile docFile, Context context) { + this.provider = provider; + this.context = context; + this.docFile = docFile; + } + + @Override + public URI getUri() { + try{ + return new URI(docFile.toString()); + } catch (URISyntaxException e) { + Log.e(LOGTAG, e.getMessage(), e.getCause()); + return null; + } + } + + @Override + public String getName() { + return docFile.getName(); + } + + @Override + public boolean isDirectory() { + return docFile.isDirectory(); + } + + @Override + public long getSize() { + return docFile.length(); + } + + @Override + public Date getLastModified() { + return new Date(docFile.lastModified()); + } + + @Override + public List<IFile> listFiles() { + List<IFile> children = new ArrayList<IFile>(); + for (DocumentFile child : docFile.listFiles()) { + children.add(new ExternalFile(provider, child, context)); + } + return children; + } + + @Override + public List<IFile> listFiles(FileFilter filter) { + // TODO: no filtering yet + return listFiles(); + } + + @Override + public IFile getParent() { + // this is the root node + if(docFile.getParentFile() == null) return null; + + return new ExternalFile(provider, docFile.getParentFile(), context); + } + + @Override + public File getDocument() { + if(isDirectory()) { + return null; + } else { + duplicateFile = duplicateInCache(); + return duplicateFile; + } + } + + private File duplicateInCache() { + try{ + InputStream istream = context.getContentResolver(). + openInputStream(docFile.getUri()); + + File storageFolder = provider.getCacheDir(); + File fileCopy = new File(storageFolder, docFile.getName()); + OutputStream ostream = new FileOutputStream(fileCopy); + + IOUtils.copy(istream, ostream); + return fileCopy; + } catch (Exception e) { + Log.e(LOGTAG, e.getMessage(), e.getCause()); + return null; + } + } + + @Override + public void saveDocument(File file) { + try{ + OutputStream ostream = context.getContentResolver(). + openOutputStream(docFile.getUri()); + InputStream istream = new FileInputStream(file); + + IOUtils.copy(istream, ostream); + + } catch (Exception e) { + Log.e(LOGTAG, e.getMessage(), e.getCause()); + } + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (!(object instanceof ExternalFile)) + return false; + ExternalFile file = (ExternalFile) object; + return file.getUri().equals(getUri()); + } + +} diff --git a/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java new file mode 100644 index 000000000000..09e993bd1eb2 --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/ExtsdDocumentsProvider.java @@ -0,0 +1,152 @@ +package org.libreoffice.storage.external; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.support.v4.provider.DocumentFile; + +import org.libreoffice.R; +import org.libreoffice.storage.DocumentProviderSettingsActivity; +import org.libreoffice.storage.IFile; + +import java.io.File; +import java.net.URI; + +/** + * Implementation of IDocumentProvider for the external file system, for android 4.4+ + * + * The DocumentFile class is required when accessing files in external storage + * for Android 4.4+. The ExternalFile class is used to handle this. + * + * Android 4.4 & 5+ use different types of root directory paths, + * 5 using a DirectoryTree Uri and 4.4 using a normal File path. + * As such, different methods are required to obtain the rootDirectory IFile. + * 4.4 has to guess the location of the rootDirectory as well. + */ +public class ExtsdDocumentsProvider implements IExternalDocumentProvider, + OnSharedPreferenceChangeListener{ + private static final String LOGTAG = ExtsdDocumentsProvider.class.getSimpleName(); + + private int id; + private File cacheDir; + private Context context; + private String rootPathURI; + + public ExtsdDocumentsProvider(int id, Context context) { + this.id = id; + this.context = context; + setupRootPathUri(); + setupCache(); + } + + private void setupRootPathUri() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String rootURIGuess = guessRootURI(); + rootPathURI = preferences.getString( + DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI, rootURIGuess); + } + + //Android 4.4 specific + @TargetApi(Build.VERSION_CODES.KITKAT) + public String guessRootURI() { + File[] options = context.getExternalFilesDirs(null); + File internalSD = Environment.getExternalStorageDirectory(); + String internalSDPath = internalSD.getAbsolutePath(); + + for (File option: options) { + String optionPath = option.getAbsolutePath(); + if(!optionPath.contains(internalSDPath)) + return option.toURI().toString(); + } + + return ""; + } + + private void setupCache() { + // TODO: probably we should do smarter cache management + cacheDir = new File(context.getExternalCacheDir(), "externalFiles"); + if (cacheDir.exists()) { + deleteRecursive(cacheDir); + } + cacheDir.mkdirs(); + } + + private static void deleteRecursive(File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + deleteRecursive(child); + } + file.delete(); + } + + public File getCacheDir() { + return cacheDir; + } + + @Override + public IFile getRootDirectory() { + if(android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + return android4RootDirectory(); + } else { + return android5RootDirectory(); + } + } + + private ExternalFile android4RootDirectory() { + try{ + File f = new File(new URI(rootPathURI)); + return new ExternalFile(this, DocumentFile.fromFile(f), context); + } catch (Exception e) { + //invalid rootPathURI + throw buildRuntimeExceptionForInvalidFileURI(); + } + } + + private ExternalFile android5RootDirectory() { + try { + return new ExternalFile(this, + DocumentFile.fromTreeUri(context, Uri.parse(rootPathURI)), + context); + } catch (Exception e) { + //invalid rootPathURI + throw buildRuntimeExceptionForInvalidFileURI(); + } + } + + private RuntimeException buildRuntimeExceptionForInvalidFileURI() { + return new RuntimeException(context.getString(R.string.ext_document_provider_error)); + } + + @Override + public IFile createFromUri(URI javaURI) { + //TODO: refactor when new DocumentFile API exist + //uri must be of a DocumentFile file, not directory. + Uri androidUri = Uri.parse(javaURI.toString()); + return new ExternalFile(this, + DocumentFile.fromSingleUri(context, androidUri), + context); + } + + @Override + public int getNameResource() { + return R.string.external_sd_file_system; + } + + @Override + public int getId() { + return id; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI)) { + rootPathURI = preferences.getString(key, ""); + } + } + +} diff --git a/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java b/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java new file mode 100644 index 000000000000..34bbcbc4bad6 --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/IExternalDocumentProvider.java @@ -0,0 +1,19 @@ +package org.libreoffice.storage.external; + +import org.libreoffice.storage.IDocumentProvider; + + +/** + * Interface for external document providers. + */ +public interface IExternalDocumentProvider extends IDocumentProvider { + + /** + * Used to obtain the default directory to display when + * browsing using the internal DirectoryBrowser. + * + * @return a guess of the root file's URI. + */ + String guessRootURI(); + +} diff --git a/android/source/src/java/org/libreoffice/storage/external/LegacyExtSDDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/external/LegacyExtSDDocumentsProvider.java new file mode 100644 index 000000000000..ae5ddde40141 --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/LegacyExtSDDocumentsProvider.java @@ -0,0 +1,97 @@ +package org.libreoffice.storage.external; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; + +import org.libreoffice.R; +import org.libreoffice.storage.DocumentProviderSettingsActivity; +import org.libreoffice.storage.IFile; +import org.libreoffice.storage.IOUtils; +import org.libreoffice.storage.local.LocalFile; + +import java.io.File; +import java.net.URI; + +/** + * Legacy document provider for External SD cards, for Android 4.3 and below. + * + * Uses the the LocalFile class. + */ +public class LegacyExtSDDocumentsProvider implements IExternalDocumentProvider, + SharedPreferences.OnSharedPreferenceChangeListener{ + private static final String LOGTAG = LegacyExtSDDocumentsProvider.class.getSimpleName(); + + private int id; + private Context context; + private String rootPathURI; + + public LegacyExtSDDocumentsProvider(int id, Context context) { + this.id = id; + this.context = context; + setupRootPathUri(); + } + + private void setupRootPathUri() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String rootURIGuess = guessRootURI(); + + rootPathURI = preferences.getString( + DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI, rootURIGuess); + } + + public String guessRootURI() { + //hacky method of obtaining extsdcard root + final String value = System.getenv("SECONDARY_STORAGE"); + Log.d(LOGTAG, "guesses: " + value); + if (!TextUtils.isEmpty(value)) { + final String[] paths = value.split(":"); + for (String path : paths) { + File file = new File(path); + if(path.contains("ext") && file.isDirectory()) { + return file.toURI().toString(); + } + } + } + return ""; + } + + @Override + public IFile getRootDirectory() { + if(rootPathURI.equals("")) { + throw new RuntimeException(context.getString(R.string.ext_document_provider_error)); + } + + File f = IOUtils.getFileFromURIString(rootPathURI); + if(IOUtils.isInvalidFile(f)) { + //missing device + throw new RuntimeException(context.getString(R.string.legacy_extsd_missing_error)); + } + return new LocalFile(f); + } + + @Override + public IFile createFromUri(URI uri) { + return new LocalFile(uri); + } + + @Override + public int getNameResource() { + return R.string.external_sd_file_system; + } + + @Override + public int getId() { + return id; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_EXTERNAL_SD_PATH_URI)) { + rootPathURI = preferences.getString(key, ""); + } + } + +} diff --git a/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java b/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java new file mode 100644 index 000000000000..37e9be7d1a32 --- /dev/null +++ b/android/source/src/java/org/libreoffice/storage/external/OTGDocumentsProvider.java @@ -0,0 +1,84 @@ +package org.libreoffice.storage.external; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.libreoffice.R; +import org.libreoffice.storage.DocumentProviderSettingsActivity; +import org.libreoffice.storage.IFile; +import org.libreoffice.storage.IOUtils; +import org.libreoffice.storage.local.LocalFile; + +import java.io.File; +import java.net.URI; + +/** + * TODO: OTG currently uses LocalFile. Change to an IFile that handles abrupt OTG unmounting + */ +public class OTGDocumentsProvider implements IExternalDocumentProvider, + SharedPreferences.OnSharedPreferenceChangeListener { + + private static final String LOGTAG = OTGDocumentsProvider.class.getSimpleName(); + + private Context context; + private String rootPathURI; + private int id; + + public OTGDocumentsProvider(int id, Context context) { + this.context = context; + this.id = id; + setupRootPath(); + } + + private void setupRootPath() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + rootPathURI = preferences.getString( + DocumentProviderSettingsActivity.KEY_PREF_OTG_PATH_URI, ""); + } + + @Override + public IFile createFromUri(URI uri) { + return new LocalFile(uri); + } + + @Override + public int getNameResource() { + return R.string.otg_file_system; + } + + @Override + public int getId() { + return id; + } + + @Override + public IFile getRootDirectory() { + + if(rootPathURI.equals("")) { + throw new RuntimeException(context.getString(R.string.ext_document_provider_error)); + } + + File f = IOUtils.getFileFromURIString(rootPathURI); + if(IOUtils.isInvalidFile(f)) { + //missing device + throw new RuntimeException(context.getString(R.string.otg_missing_error, context)); + } + + return new LocalFile(f); + } + + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(DocumentProviderSettingsActivity.KEY_PREF_OTG_PATH_URI)) { + rootPathURI = sharedPreferences.getString(key, ""); + } + } + + @Override + public String guessRootURI() { + return ""; + } +} |