Add a download location preference. https://redmine.stoutner.com/issues/32
authorSoren Stoutner <soren@stoutner.com>
Wed, 25 Mar 2020 20:29:17 +0000 (13:29 -0700)
committerSoren Stoutner <soren@stoutner.com>
Wed, 25 Mar 2020 20:29:17 +0000 (13:29 -0700)
36 files changed:
app/build.gradle
app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetSource.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/GetUrlSize.java
app/src/main/java/com/stoutner/privacybrowser/asynctasks/SaveUrl.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/OpenDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.java
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveLogcatDialog.java
app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.java
app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java [new file with mode: 0644]
app/src/main/java/com/stoutner/privacybrowser/helpers/ImportExportDatabaseHelper.java
app/src/main/res/drawable/downloads.xml [deleted file]
app/src/main/res/drawable/downloads_enabled_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/downloads_enabled_light.xml [new file with mode: 0644]
app/src/main/res/drawable/downloads_ghosted_dark.xml [new file with mode: 0644]
app/src/main/res/drawable/downloads_ghosted_light.xml [new file with mode: 0644]
app/src/main/res/drawable/full_screen_disabled_dark.xml
app/src/main/res/drawable/full_screen_disabled_light.xml
app/src/main/res/drawable/full_screen_enabled_dark.xml
app/src/main/res/drawable/full_screen_enabled_light.xml
app/src/main/res/drawable/search_custom_url_enabled_dark.xml
app/src/main/res/drawable/search_custom_url_enabled_light.xml
app/src/main/res/drawable/search_custom_url_ghosted_dark.xml
app/src/main/res/drawable/search_custom_url_ghosted_light.xml
app/src/main/res/menu/webview_navigation_menu.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values/attrs.xml
app/src/main/res/values/strings.xml
app/src/main/res/values/styles.xml
app/src/main/res/xml/preferences.xml

index 5c9bb272e8fe8c3ffbbfb5ec5fa05c21424c10a5..769e71e4237386d8fb2e05b4b1f84fcec575e2fd 100644 (file)
@@ -96,5 +96,5 @@ dependencies {
     implementation 'com.google.android.material:material:1.1.0'
 
     // Only compile Firebase ads for the free flavor.
-    freeImplementation 'com.google.firebase:firebase-ads:19.0.0'
+    freeImplementation 'com.google.firebase:firebase-ads:19.0.1'
 }
\ No newline at end of file
index df41cf31f6502f5aa86f506e3be11ab3dbac9285..3481f312dd555cd3b98054e9b7ab51c55bee1b5a 100644 (file)
@@ -60,6 +60,7 @@ import com.google.android.material.textfield.TextInputLayout;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
+import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 import com.stoutner.privacybrowser.helpers.FileNameHelper;
 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
 
@@ -176,30 +177,24 @@ public class ImportExportActivity extends AppCompatActivity implements StoragePe
         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
         importExportButton.setVisibility(View.GONE);
 
-        // Create strings for the default file paths.
-        String defaultFilePath;
-        String defaultPasswordEncryptionFilePath;
-        String defaultPgpFilePath;
+        // Instantiate the download location helper.
+        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
 
-        // Set the default file paths according to the storage permission status.
-        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Set the default file paths to use the external public directory.
-            defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.settings_pbs);
-            defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
-            defaultPgpFilePath = defaultFilePath + ".pgp";
+        // Get the default file path.
+        String defaultFilePath = downloadLocationHelper.getDownloadLocation(this) + "/" + getString(R.string.settings_pbs);
 
-            // Hide the storage permission text view.
-            storagePermissionTextView.setVisibility(View.GONE);
-        } else {  // The storage permission has not been granted.
-            // Set the default file paths to use the external private directory.
-            defaultFilePath = getApplicationContext().getExternalFilesDir(null) + "/" + getString(R.string.settings_pbs);
-            defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
-            defaultPgpFilePath = defaultFilePath + ".pgp";
-        }
+        // Set the other default file paths.
+        String defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
+        String defaultPgpFilePath = defaultFilePath + ".pgp";
 
         // Set the default file path.
         fileNameEditText.setText(defaultFilePath);
 
+        // Hide the storage permission text view if the permission has already been granted.
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+            storagePermissionTextView.setVisibility(View.GONE);
+        }
+
         // Update the UI when the spinner changes.
         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
             @Override
index 63c606cdbb3303ea89dc8c4f3190042f24bb67dd..81a80cf28686d981fe97f7fb1c545a061dbf208c 100644 (file)
@@ -1895,6 +1895,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
+                // Make it so.
                 startActivity(downloadManagerIntent);
                 break;
 
index 415f35bddf6eff65ba299cdd9d2da02852ca691e..f5b4e54b67f1be6870a9c6b50e528bbca787e7e7 100644 (file)
@@ -491,8 +491,8 @@ public class GetSource extends AsyncTask<String, Void, SpannableStringBuilder[]>
                 // Disconnect HTTP URL connection.
                 httpUrlConnection.disconnect();
             }
-        } catch (IOException exception) {
-            exception.printStackTrace();
+        } catch (Exception exception) {
+            // Do nothing.
         }
 
         // Return the response body string as the result.
index b34f62e885937bf33ab2d81c2669ce0e29ed9a8f..74968df9508599744504dec070bf568297ef992c 100644 (file)
@@ -28,7 +28,6 @@ import android.widget.TextView;
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 
-import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
@@ -81,10 +80,10 @@ public class GetUrlSize extends AsyncTask<String, Void, String> {
             Proxy proxy = proxyHelper.getCurrentProxy(context);
 
             // Open a connection to the URL.  No data is actually sent at this point.
-            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(proxy);
+            HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
 
             // Add the user agent to the header property.
-            httpURLConnection.setRequestProperty("User-Agent", userAgent);
+            httpUrlConnection.setRequestProperty("User-Agent", userAgent);
 
             // Add the cookies if they are enabled.
             if (cookiesEnabled) {
@@ -94,7 +93,7 @@ public class GetUrlSize extends AsyncTask<String, Void, String> {
                 // Only add the cookies if they are not null.
                 if (cookiesString != null) {
                     // Add the cookies to the header property.
-                    httpURLConnection.setRequestProperty("Cookie", cookiesString);
+                    httpUrlConnection.setRequestProperty("Cookie", cookiesString);
                 }
             }
 
@@ -103,19 +102,19 @@ public class GetUrlSize extends AsyncTask<String, Void, String> {
                 // Exit if the task has been cancelled.
                 if (isCancelled()) {
                     // Disconnect the HTTP URL connection.
-                    httpURLConnection.disconnect();
+                    httpUrlConnection.disconnect();
 
                     // Return the formatted file size string.
                     return formattedFileSize;
                 }
 
                 // Get the status code.
-                int responseCode = httpURLConnection.getResponseCode();
+                int responseCode = httpUrlConnection.getResponseCode();
 
                 // Exit if the task has been cancelled.
                 if (isCancelled()) {
                     // Disconnect the HTTP URL connection.
-                    httpURLConnection.disconnect();
+                    httpUrlConnection.disconnect();
 
                     // Return the formatted file size string.
                     return formattedFileSize;
@@ -127,7 +126,7 @@ public class GetUrlSize extends AsyncTask<String, Void, String> {
                     formattedFileSize = context.getString(R.string.invalid_url);
                 } else {  // The response code is not an error message.
                     // Get the content length header.
-                    String contentLengthString = httpURLConnection.getHeaderField("Content-Length");
+                    String contentLengthString = httpUrlConnection.getHeaderField("Content-Length");
 
                     // Define the file size long.
                     long fileSize;
@@ -143,9 +142,9 @@ public class GetUrlSize extends AsyncTask<String, Void, String> {
                 }
             } finally {
                 // Disconnect the HTTP URL connection.
-                httpURLConnection.disconnect();
+                httpUrlConnection.disconnect();
             }
-        } catch (IOException exception) {
+        } catch (Exception exception) {
             // Set the formatted file size to indicate a bad URL.
             formattedFileSize = context.getString(R.string.invalid_url);
         }
index e6b811500726162066086320c13053c9869950b5..6b383f4e40de02e435a289cbfcda5fb52973d9fc 100644 (file)
@@ -41,7 +41,6 @@ import com.stoutner.privacybrowser.views.NoSwipeViewPager;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.lang.ref.WeakReference;
@@ -214,7 +213,7 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
                 // Close the output stream.
                 outputStream.close();
 
-                // Define a media scanner intent, which adds items like pictures to Android's recent file list.
+                // Create a media scanner intent, which adds items like pictures to Android's recent file list.
                 Intent mediaScannerIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
 
                 // Add the URI to the media scanner intent.
@@ -226,7 +225,7 @@ public class SaveUrl extends AsyncTask<String, Long, String> {
                 // Disconnect the HTTP URL connection.
                 httpUrlConnection.disconnect();
             }
-        } catch (IOException exception) {
+        } catch (Exception exception) {
             // Store the error in the save disposition string.
             saveDisposition = exception.toString();
         }
index 2d6a776153b3dc746e37a02a9703175d6530c4f5..59aa177ba945c1fa58288357987a22c69570bfa9 100644 (file)
@@ -48,6 +48,7 @@ import androidx.preference.PreferenceManager;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
+import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 
 import java.io.File;
 
@@ -143,9 +144,6 @@ public class OpenDialog extends DialogFragment {
         TextView storagePermissionTextView = alertDialog.findViewById(R.id.storage_permission_textview);
         Button openButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
 
-        // Create a string for the default file path.
-        String defaultFilePath;
-
         // Update the status of the open button when the file name changes.
         fileNameEditText.addTextChangedListener(new TextWatcher() {
             @Override
@@ -183,17 +181,11 @@ public class OpenDialog extends DialogFragment {
             }
         });
 
-        // Set the default file path according to the storage permission state.
-        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Set the default file path to use the external public directory.
-            defaultFilePath = Environment.getExternalStorageDirectory() + "/";
+        // Instantiate the download location helper.
+        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
 
-            // Hide the storage permission text view.
-            storagePermissionTextView.setVisibility(View.GONE);
-        } else {  // The storage permission has not been granted.
-            // Set the default file path to use the external private directory.
-            defaultFilePath = context.getExternalFilesDir(null) + "/";
-        }
+        // Get the default file path.
+        String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/";
 
         // Display the default file path.
         fileNameEditText.setText(defaultFilePath);
@@ -201,6 +193,11 @@ public class OpenDialog extends DialogFragment {
         // Move the cursor to the end of the default file path.
         fileNameEditText.setSelection(defaultFilePath.length());
 
+        // Hide the storage permission text view if the permission has already been granted.
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+            storagePermissionTextView.setVisibility(View.GONE);
+        }
+
         // Handle clicks on the browse button.
         browseButton.setOnClickListener((View view) -> {
             // Create the file picker intent.
index d271b876ea374dda7aff28f63989da202ebaf54c..66282c8681f902020df937e1e7f221303b58ec8a 100644 (file)
@@ -51,6 +51,7 @@ import androidx.preference.PreferenceManager;
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
 import com.stoutner.privacybrowser.asynctasks.GetUrlSize;
+import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 
 import java.io.File;
 
@@ -319,19 +320,15 @@ public class SaveDialog extends DialogFragment {
         // Save the file name as the default file name.  This must be final to be used in the lambda below.
         final String defaultFileName = fileName;
 
-        // Create a string for the default file path.
-        String defaultFilePath;
+        // Instantiate the download location helper.
+        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
 
-        // Set the default file path according to the storage permission state.
-        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Set the default file path to use the external public directory.
-            defaultFilePath = Environment.getExternalStorageDirectory() + "/" + defaultFileName;
+        // Get the default file path.
+        String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + defaultFileName;
 
-            // Hide the storage permission text view.
+        // Hide the storage permission text view if the permission has already been granted.
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
             storagePermissionTextView.setVisibility(View.GONE);
-        } else {  // The storage permission has not been granted.
-            // Set the default file path to use the external private directory.
-            defaultFilePath = context.getExternalFilesDir(null) + "/" + defaultFileName;
         }
 
         // Populate the edit texts.
index dcd10f9408cf2f022482ac21d0a3b5e84611f9d7..624e45532e98e81c6239c46ec745832ada65fe25 100644 (file)
@@ -47,6 +47,7 @@ import androidx.core.content.ContextCompat;
 import androidx.fragment.app.DialogFragment;  // The AndroidX dialog fragment is required or an error is produced on API <=22.  It is also required for the browse button to work correctly.
 
 import com.stoutner.privacybrowser.R;
+import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 
 import java.io.File;
 
@@ -83,11 +84,13 @@ public class SaveLogcatDialog extends DialogFragment {
         // Use an alert dialog builder to create the alert dialog.
         AlertDialog.Builder dialogBuilder;
 
-        // Get a handle for the activity.
+        // Get a handle for the activity and the context.
         Activity activity = getActivity();
+        Context context = getContext();
 
-        // Remove the incorrect lint warning below that the activity might be null.
+        // Remove the incorrect lint warnings.
         assert activity != null;
+        assert context != null;
 
         // Set the style according to the theme.
         if (darkTheme) {
@@ -175,30 +178,20 @@ public class SaveLogcatDialog extends DialogFragment {
             }
         });
 
-        // Create a string for the default file path.
-        String defaultFilePath;
+        // Instantiate the download location helper.
+        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
 
-        // Get a handle for the context.
-        Context context = getContext();
-
-        // Remove the incorrect lint warning below that context might be null.
-        assert context != null;
+        // Get the default file path.
+        String defaultFilePath = downloadLocationHelper.getDownloadLocation(context) + "/" + getString(R.string.privacy_browser_logcat_txt);
 
-        // Set the default file path according to the storage permission state.
-        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
-            // Set the default file path to use the external public directory.
-            defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.privacy_browser_logcat_txt);
+        // Display the default file path.
+        fileNameEditText.setText(defaultFilePath);
 
-            // Hide the storage permission text view.
+        // Hide the storage permission text view if the permission has already been granted.
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
             storagePermissionTextView.setVisibility(View.GONE);
-        } else {  // The storage permission has not been granted.
-            // Set the default file path to use the external private directory.
-            defaultFilePath = context.getExternalFilesDir(null) + "/" + getString(R.string.privacy_browser_logcat_txt);
         }
 
-        // Display the default file path.
-        fileNameEditText.setText(defaultFilePath);
-
         // Handle clicks on the browse button.
         browseButton.setOnClickListener((View view) -> {
             // Create the file picker intent.
index 1d4dd164e4c3fa62810434bc6b23c2eb587332f8..643051abb970995d2dc5d3e4d5ab93e46ecf8afc 100644 (file)
@@ -24,6 +24,7 @@ import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.res.Resources;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -38,6 +39,7 @@ import androidx.preference.PreferenceFragmentCompat;
 
 import com.stoutner.privacybrowser.R;
 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
+import com.stoutner.privacybrowser.helpers.DownloadLocationHelper;
 import com.stoutner.privacybrowser.helpers.ProxyHelper;
 
 public class SettingsFragment extends PreferenceFragmentCompat {
@@ -95,6 +97,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         Preference clearFormDataPreference = findPreference("clear_form_data");  // The clear form data preference can be removed once the minimum API >= 26.
         Preference clearCachePreference = findPreference("clear_cache");
         Preference homepagePreference = findPreference("homepage");
+        Preference downloadLocationPreference = findPreference("download_location");
+        Preference downloadCustomLocationPreference = findPreference("download_custom_location");
         Preference fontSizePreference = findPreference("font_size");
         Preference openIntentsInNewTabPreference = findPreference("open_intents_in_new_tab");
         Preference swipeToRefreshPreference = findPreference("swipe_to_refresh");
@@ -138,6 +142,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         assert clearFormDataPreference != null;
         assert clearCachePreference != null;
         assert homepagePreference != null;
+        assert downloadLocationPreference != null;
+        assert downloadCustomLocationPreference != null;
         assert fontSizePreference != null;
         assert openIntentsInNewTabPreference != null;
         assert swipeToRefreshPreference != null;
@@ -152,8 +158,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         hideAppBarPreference.setDependency("full_screen_browsing_mode");
 
         // Get strings from the preferences.
+        String userAgentName = savedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
         String searchString = savedPreferences.getString("search", getString(R.string.search_default_value));
         String proxyString = savedPreferences.getString("proxy", getString(R.string.proxy_default_value));
+        String downloadLocationString = savedPreferences.getString("download_location", getString(R.string.download_location_default_value));
 
         // Get booleans that are used in multiple places from the preferences.
         boolean javaScriptEnabled = savedPreferences.getBoolean("javascript", false);
@@ -190,10 +198,14 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         // Only enable Fanboy's social blocking list preference if Fanboy's annoyance list is disabled.
         fanboySocialBlockingListPreference.setEnabled(!fanboyAnnoyanceListEnabled);
 
+
         // Inflate a WebView to get the default user agent.
         LayoutInflater inflater = getActivity().getLayoutInflater();
+
         // `@SuppressLint("InflateParams")` removes the warning about using `null` as the `ViewGroup`, which in this case makes sense because the `bare_webview` will not be displayed.
         @SuppressLint("InflateParams") View bareWebViewLayout = inflater.inflate(R.layout.bare_webview, null, false);
+
+        // Get a handle for a bare WebView.
         WebView bareWebView = bareWebViewLayout.findViewById(R.id.bare_webview);
 
         // Get the user agent arrays.
@@ -201,9 +213,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         String[] translatedUserAgentNamesArray = getResources().getStringArray(R.array.translated_user_agent_names);
         String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
 
-        // Get the current user agent name from the preference.
-        String userAgentName = savedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
-
         // Get the array position of the user agent name.
         int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
 
@@ -229,10 +238,13 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                 userAgentPreference.setSummary(translatedUserAgentNamesArray[userAgentArrayPosition] + ":\n" + userAgentDataArray[userAgentArrayPosition]);
         }
 
-        // Set the summary text for the custom user agent preference and enable it if user agent preference is set to custom.
+        // Set the summary text for the custom user agent preference.
         customUserAgentPreference.setSummary(savedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
+
+        // Only enable the custom user agent preference if the user agent is set to `Custom`.
         customUserAgentPreference.setEnabled(userAgentPreference.getSummary().equals(getString(R.string.custom_user_agent)));
 
+
         // Set the search URL as the summary text for the search preference when the preference screen is loaded.
         if (searchString.equals("Custom URL")) {
             // Use R.string.custom_url, which will be translated, instead of the array value, which will not.
@@ -242,10 +254,13 @@ public class SettingsFragment extends PreferenceFragmentCompat {
             searchPreference.setSummary(searchString);
         }
 
-        // Set the summary text for `search_custom_url` (the default is `""`) and enable it if `search` is set to `Custom URL`.
+        // Set the summary text for the search custom URL (the default is `""`).
         searchCustomURLPreference.setSummary(savedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)));
+
+        // Only enable the search custom URL preference if the search is set to `Custom URL`.
         searchCustomURLPreference.setEnabled(searchString.equals("Custom URL"));
 
+
         // Set the summary text for the proxy preference when the preference screen is loaded.
         switch (proxyString) {
             case ProxyHelper.NONE:
@@ -269,11 +284,12 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                 break;
         }
 
+        // Set the summary text for the custom proxy URL.
+        proxyCustomUrlPreference.setSummary(savedPreferences.getString("proxy_custom_url", getString(R.string.proxy_custom_url_default_value)));
+
         // Only enable the custom proxy URL if a custom proxy is selected.
         proxyCustomUrlPreference.setEnabled(proxyString.equals("Custom"));
 
-        // Set the summary text for the custom proxy URL.
-        proxyCustomUrlPreference.setSummary(savedPreferences.getString("proxy_custom_url", getString(R.string.proxy_custom_url_default_value)));
 
         // Set the status of the Clear and Exit preferences.
         clearCookiesPreference.setEnabled(!clearEverything);
@@ -281,12 +297,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
         clearFormDataPreference.setEnabled(!clearEverything);  // The form data line can be removed once the minimum API is >= 26.
         clearCachePreference.setEnabled(!clearEverything);
 
+
         // Set the homepage URL as the summary text for the homepage preference.
         homepagePreference.setSummary(savedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
 
+
+        // Instantiate the download location helper.
+        DownloadLocationHelper downloadLocationHelper = new DownloadLocationHelper();
+
+        // Set the download location summary text.
+        downloadLocationPreference.setSummary(downloadLocationHelper.getDownloadLocation(context));
+
+        // Set the summary text for the download custom location (the default is `"`).
+        downloadCustomLocationPreference.setSummary(savedPreferences.getString("download_custom_location", getString(R.string.download_custom_location_default_value)));
+
+        // Get the download location entry values string array.
+        String[] downloadLocationEntryValuesStringArray = context.getResources().getStringArray(R.array.download_location_entry_values);
+
+        // Only enable the download custom location preference if the download location is set to `Custom`.
+        downloadCustomLocationPreference.setEnabled(downloadLocationString.equals(downloadLocationEntryValuesStringArray[3]));
+
+
         // Set the font size as the summary text for the preference.
         fontSizePreference.setSummary(savedPreferences.getString("font_size", getString(R.string.font_size_default_value)) + "%");
 
+
         // Disable the JavaScript preference if Night Mode is enabled.  JavaScript will be enabled for all web pages.
         javaScriptPreference.setEnabled(!nightMode);
 
@@ -725,6 +760,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
             clearCachePreference.setIcon(R.drawable.cache_warning);
         }
 
+        // Set the download custom location icon.
+        if (downloadCustomLocationPreference.isEnabled()) {
+            if (darkTheme) {
+                downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_dark);
+            } else {
+                downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_light);
+            }
+        } else {
+            if (darkTheme) {
+                downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_dark);
+            } else {
+                downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_light);
+            }
+        }
+
         // Set the open intents in new tab preference icon.
         if (savedPreferences.getBoolean("open_intents_in_new_tab", true)) {
             if (darkTheme) {
@@ -1303,7 +1353,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                     // Store the new search string.
                     String newSearchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
 
-                    // Update `searchPreference` and `searchCustomURLPreference`.
+                    // Update the search and search custom URL preferences.
                     if (newSearchString.equals("Custom URL")) {  // `Custom URL` is selected.
                         // Set the summary text to `R.string.custom_url`, which is translated.
                         searchPreference.setSummary(R.string.custom_url);
@@ -1334,7 +1384,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                     break;
 
                 case "search_custom_url":
-                    // Set the new custom search URL as the summary text for `search_custom_url`.  The default is `""`.
+                    // Set the new search custom URL as the summary text for the preference.
                     searchCustomURLPreference.setSummary(sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value)));
                     break;
 
@@ -1595,6 +1645,37 @@ public class SettingsFragment extends PreferenceFragmentCompat {
                     homepagePreference.setSummary(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
                     break;
 
+                case "download_location":
+                    // Get the new download location.
+                    String newDownloadLocationString = sharedPreferences.getString("download_location", getString(R.string.download_location_default_value));
+
+                    // Update the download location summary text.
+                    downloadLocationPreference.setSummary(downloadLocationHelper.getDownloadLocation(context));
+
+                    // Update the status of the download custom location preference.
+                    downloadCustomLocationPreference.setEnabled(newDownloadLocationString.equals(downloadLocationEntryValuesStringArray[3]));
+
+                    // Update the download custom location icon.
+                    if (downloadCustomLocationPreference.isEnabled()) {
+                        if (darkTheme) {
+                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_dark);
+                        } else {
+                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_enabled_light);
+                        }
+                    } else {
+                        if (darkTheme) {
+                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_dark);
+                        } else {
+                            downloadCustomLocationPreference.setIcon(R.drawable.downloads_ghosted_light);
+                        }
+                    }
+                    break;
+
+                case "download_custom_location":
+                    // Set the new download custom location as the summary text for the preference.
+                    downloadCustomLocationPreference.setSummary(sharedPreferences.getString("download_custom_location", getString(R.string.download_custom_location_default_value)));
+                    break;
+
                 case "font_size":
                     // Update the font size summary text.
                     fontSizePreference.setSummary(sharedPreferences.getString("font_size", getString(R.string.font_size_default_value)) + "%");
diff --git a/app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java b/app/src/main/java/com/stoutner/privacybrowser/helpers/DownloadLocationHelper.java
new file mode 100644 (file)
index 0000000..f23aaea
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2020 Soren Stoutner <soren@stoutner.com>.
+ *
+ * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
+ *
+ * Privacy Browser is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Privacy Browser is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.stoutner.privacybrowser.helpers;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+
+import com.stoutner.privacybrowser.R;
+
+import java.io.File;
+
+public class DownloadLocationHelper {
+    public String getDownloadLocation(Context context) {
+        // Get the download location entry values string array.
+        String[] downloadLocationEntryValuesStringArray = context.getResources().getStringArray(R.array.download_location_entry_values);
+
+        // Get the two standard download directories.
+        File publicDownloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+        File publicAppFilesDirectory = context.getExternalFilesDir(null);
+
+        // Remove the incorrect lint warning below that the public app files directory might be null.
+        assert publicAppFilesDirectory != null;
+
+        // Convert the download directories to strings.
+        String publicDownloadDirectoryString = publicDownloadDirectory.toString();
+        String publicAppFilesDirectoryString = publicAppFilesDirectory.toString();
+
+        // Get a handle for the shared preferences.
+        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+        // Get the download location strings from the preferences.
+        String downloadLocationString = sharedPreferences.getString("download_location", context.getString(R.string.download_location_default_value));
+        String downloadCustomLocationString = sharedPreferences.getString("download_custom_location", context.getString(R.string.download_custom_location_default_value));
+
+        // Define a string for the default file path.
+        String defaultFilePath;
+
+        // Set the default file path according to the download location.
+        if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[0])) {  // the download location is set to auto.
+            // Set the download location summary text according to the storage permission status.
+            if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
+                // Use the public download directory.
+                defaultFilePath = publicDownloadDirectoryString;
+            } else {  // The storage permission has not been granted.
+                // Use the public app files directory.
+                defaultFilePath = publicAppFilesDirectoryString;
+            }
+        } else if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[1])) {  // The download location is set to the app directory.
+            // Use the public app files directory.
+            defaultFilePath = publicAppFilesDirectoryString;
+        } else if (downloadLocationString.equals(downloadLocationEntryValuesStringArray[2])) {  // The download location is set to the public directory.
+            // Use the public download directory.
+            defaultFilePath = publicDownloadDirectoryString;
+        } else {  // The download location is set to custom.
+            // Use the download custom location.
+            defaultFilePath = downloadCustomLocationString;
+        }
+
+        // Return the default file path.
+        return defaultFilePath;
+    }
+}
\ No newline at end of file
index 50a5ad2341f31653f13e3abfac7d68d37851aef0..74b4e510ccb961430cdc1e07edb0f1117e504f22 100644 (file)
@@ -76,6 +76,8 @@ public class ImportExportDatabaseHelper {
     private static final String CLEAR_FORM_DATA = "clear_form_data";
     private static final String CLEAR_CACHE = "clear_cache";
     private static final String HOMEPAGE = "homepage";
+    private static final String DOWNLOAD_LOCATION = "download_location";
+    private static final String DOWNLOAD_CUSTOM_LOCATION = "download_custom_location";
     private static final String FONT_SIZE = "font_size";
     private static final String OPEN_INTENTS_IN_NEW_TAB = "open_intents_in_new_tab";
     private static final String SWIPE_TO_REFRESH = "swipe_to_refresh";
@@ -229,6 +231,8 @@ public class ImportExportDatabaseHelper {
                     CLEAR_FORM_DATA + " BOOLEAN, " +
                     CLEAR_CACHE + " BOOLEAN, " +
                     HOMEPAGE + " TEXT, " +
+                    DOWNLOAD_LOCATION + " TEXT, " +
+                    DOWNLOAD_CUSTOM_LOCATION + " TEXT, " +
                     FONT_SIZE + " TEXT, " +
                     OPEN_INTENTS_IN_NEW_TAB + " BOOLEAN, " +
                     SWIPE_TO_REFRESH + " BOOLEAN, " +
@@ -279,6 +283,8 @@ public class ImportExportDatabaseHelper {
             preferencesContentValues.put(CLEAR_FORM_DATA, sharedPreferences.getBoolean(CLEAR_FORM_DATA, true));  // Clear form data can be removed once the minimum API >= 26.
             preferencesContentValues.put(CLEAR_CACHE, sharedPreferences.getBoolean(CLEAR_CACHE, true));
             preferencesContentValues.put(HOMEPAGE, sharedPreferences.getString(HOMEPAGE, context.getString(R.string.homepage_default_value)));
+            preferencesContentValues.put(DOWNLOAD_LOCATION, sharedPreferences.getString(DOWNLOAD_LOCATION, context.getString(R.string.download_location_default_value)));
+            preferencesContentValues.put(DOWNLOAD_CUSTOM_LOCATION, sharedPreferences.getString(DOWNLOAD_CUSTOM_LOCATION, context.getString(R.string.download_custom_location_default_value)));
             preferencesContentValues.put(FONT_SIZE, sharedPreferences.getString(FONT_SIZE, context.getString(R.string.font_size_default_value)));
             preferencesContentValues.put(OPEN_INTENTS_IN_NEW_TAB, sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true));
             preferencesContentValues.put(SWIPE_TO_REFRESH, sharedPreferences.getBoolean(SWIPE_TO_REFRESH, true));
@@ -392,7 +398,7 @@ public class ImportExportDatabaseHelper {
                         // Create the new font size column.
                         importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + FONT_SIZE + " TEXT");
 
-                        // Place the font size string in the new column.
+                        // Populate the preferences table with the current font size value.
                         importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + FONT_SIZE + " = '" + fontSize + "'");
 
                     // Upgrade from schema version 3.
@@ -411,7 +417,7 @@ public class ImportExportDatabaseHelper {
                         boolean hideAppBar = sharedPreferences.getBoolean(HIDE_APP_BAR, true);
                         boolean scrollAppBar = sharedPreferences.getBoolean(SCROLL_APP_BAR, true);
 
-                        // Populate the database with the current values.
+                        // Populate the preferences table with the current app bar values.
                         if (hideAppBar) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + HIDE_APP_BAR + " = " + 1);
                         } else {
@@ -432,7 +438,7 @@ public class ImportExportDatabaseHelper {
                         // Get the current open intents in new tab preference.
                         boolean openIntentsInNewTab = sharedPreferences.getBoolean(OPEN_INTENTS_IN_NEW_TAB, true);
 
-                        // Populate the database with the current value.
+                        // Populate the preferences table with the current open intents value.
                         if (openIntentsInNewTab) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + OPEN_INTENTS_IN_NEW_TAB + " = " + 1);
                         } else {
@@ -456,28 +462,28 @@ public class ImportExportDatabaseHelper {
                         boolean twitterAmpRedirects = sharedPreferences.getBoolean(TWITTER_AMP_REDIRECTS, true);
                         boolean wideViewport = sharedPreferences.getBoolean(WIDE_VIEWPORT, true);
 
-                        // Populate the database with the current Google Analytics value.
+                        // Populate the preferences with the current Google Analytics value.
                         if (googleAnalytics) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + GOOGLE_ANALYTICS + " = " + 1);
                         } else {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + GOOGLE_ANALYTICS + " = " + 0);
                         }
 
-                        // Populate the database with the current Facebook Click IDs value.
+                        // Populate the preferences with the current Facebook Click IDs value.
                         if (facebookClickIds) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + FACEBOOK_CLICK_IDS + " = " + 1);
                         } else {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + FACEBOOK_CLICK_IDS + " = " + 0);
                         }
 
-                        // Populate the database with the current Twitter AMP redirects value.
+                        // Populate the preferences table with the current Twitter AMP redirects value.
                         if (twitterAmpRedirects) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + TWITTER_AMP_REDIRECTS + " = " + 1);
                         } else {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + TWITTER_AMP_REDIRECTS + " = " + 0);
                         }
 
-                        // Populate the database with the current wide viewport value.
+                        // Populate the preferences table with the current wide viewport value.
                         if (wideViewport) {
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + WIDE_VIEWPORT + " = " + 1);
                         } else {
@@ -495,7 +501,7 @@ public class ImportExportDatabaseHelper {
                         // Get the current preference values.
                         boolean ultraList = sharedPreferences.getBoolean(ULTRALIST, true);
 
-                        // Populate the tables with the current UltraList value.
+                        // Populate the preferences tables with the current UltraList value.
                         if (ultraList) {
                             importDatabase.execSQL("UPDATE " + DomainsDatabaseHelper.DOMAINS_TABLE + " SET " + DomainsDatabaseHelper.ULTRALIST + " = " + 1);
                             importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + ULTRALIST + " = " + 1);
@@ -517,13 +523,23 @@ public class ImportExportDatabaseHelper {
                         // SQL escape the proxy custom URL string.
                         proxyCustomUrl = DatabaseUtils.sqlEscapeString(proxyCustomUrl);
 
-                        // Populate the table with the current proxy values.
+                        // Populate the preferences table with the current proxy values.
                         importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + PROXY + " = '" + proxy + "'");
                         importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + PROXY_CUSTOM_URL + " = '" + proxyCustomUrl + "'");
 
                     // Upgrade from schema version 9.
                     case 9:
-                        // `download_with_external_app` was removed the the Preferences table in schema version 10, but there is no need to make any modifications to the database.
+                        // Add the download location columns to the preferences table.
+                        importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + DOWNLOAD_LOCATION + " TEXT");
+                        importDatabase.execSQL("ALTER TABLE " + PREFERENCES_TABLE + " ADD COLUMN " + DOWNLOAD_CUSTOM_LOCATION + " TEXT");
+
+                        // Get the current download location values.
+                        String downloadLocation = sharedPreferences.getString(DOWNLOAD_LOCATION, context.getString(R.string.download_location_default_value));
+                        String downloadCustomLocation = sharedPreferences.getString(DOWNLOAD_CUSTOM_LOCATION, context.getString(R.string.download_custom_location_default_value));
+
+                        // Populate the preferences table with the current download location values.
+                        importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_LOCATION + " = '" + downloadLocation + "'");
+                        importDatabase.execSQL("UPDATE " + PREFERENCES_TABLE + " SET " + DOWNLOAD_CUSTOM_LOCATION + " = '" + downloadCustomLocation + "'");
                 }
             }
 
@@ -672,6 +688,8 @@ public class ImportExportDatabaseHelper {
                     .putBoolean(CLEAR_FORM_DATA, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_FORM_DATA)) == 1)
                     .putBoolean(CLEAR_CACHE, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(CLEAR_CACHE)) == 1)
                     .putString(HOMEPAGE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(HOMEPAGE)))
+                    .putString(DOWNLOAD_LOCATION, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(DOWNLOAD_LOCATION)))
+                    .putString(DOWNLOAD_CUSTOM_LOCATION, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(DOWNLOAD_CUSTOM_LOCATION)))
                     .putString(FONT_SIZE, importPreferencesCursor.getString(importPreferencesCursor.getColumnIndex(FONT_SIZE)))
                     .putBoolean(OPEN_INTENTS_IN_NEW_TAB, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(OPEN_INTENTS_IN_NEW_TAB)) == 1)
                     .putBoolean(SWIPE_TO_REFRESH, importPreferencesCursor.getInt(importPreferencesCursor.getColumnIndex(SWIPE_TO_REFRESH)) == 1)
diff --git a/app/src/main/res/drawable/downloads.xml b/app/src/main/res/drawable/downloads.xml
deleted file mode 100644 (file)
index 4bbd96e..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
-<vector
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:height="24dp"
-    android:width="24dp"
-    android:viewportHeight="24.0"
-    android:viewportWidth="24.0" >
-
-    <!-- The hard coded color must be used until API >= 21.  Then `@color` may be used. -->
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
-</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/downloads_enabled_dark.xml b/app/src/main/res/drawable/downloads_enabled_dark.xml
new file mode 100644 (file)
index 0000000..f362252
--- /dev/null
@@ -0,0 +1,13 @@
+<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0" >
+
+    <!-- The hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF1E88E5"
+        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/downloads_enabled_light.xml b/app/src/main/res/drawable/downloads_enabled_light.xml
new file mode 100644 (file)
index 0000000..34e0fc7
--- /dev/null
@@ -0,0 +1,13 @@
+<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0" >
+
+    <!-- The hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF1565C0"
+        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/downloads_ghosted_dark.xml b/app/src/main/res/drawable/downloads_ghosted_dark.xml
new file mode 100644 (file)
index 0000000..c857a82
--- /dev/null
@@ -0,0 +1,13 @@
+<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0" >
+
+    <!-- The hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FF616161"
+        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/downloads_ghosted_light.xml b/app/src/main/res/drawable/downloads_ghosted_light.xml
new file mode 100644 (file)
index 0000000..bfcd415
--- /dev/null
@@ -0,0 +1,13 @@
+<!-- This file comes from the Android Material icon set, where it is called `file_download`.  It is released under the Apache License 2.0. -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0" >
+
+    <!-- The hard coded color must be used until API >= 21.  Then `@color` may be used. -->
+    <path
+        android:fillColor="#FFB7B7B7"
+        android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
\ No newline at end of file
index 7237961917e51d88a7d19779c2403e1fd013e00c..f01cb25633bf06805c7efa4b3ea6f8cf9db52afb 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `full_screen_disabled_dark.xml` comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:height="24dp"
index a29123f87bbcab51d2d3621f571ba65f6c716828..e4f440d7651460070a257b874a585e4f60a13996 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `full_screen_disabled_light.xml` comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:height="24dp"
index 10998caa6456a91cf88b354cb3409642c8ca3b1f..b1085adb6047b2aee5d283ead89c9f276db62245 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `full_screen_enabled_dark.xml` comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:height="24dp"
index 533294279c21df3ad4041faf984f412b3e8c7dcc..9e81e0a69827ff306f70c4ea9b210fb49c63c3a2 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `full_screen_enabled_light.xml` comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `smartphone`.  It is released under the Apache License 2.0. -->
 <vector
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:height="24dp"
index bffd12d94b67dafbf9d17ac698278659384514e8..fdf809fceeeb9bf2733b64fb7fbf40f41684a1d5 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `search_custom_url_enabled_dark.xml` comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
 
 <!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
 <vector
index e89d4edc0791642ab42600270986c94527991aef..5ad69243f2f6e169740a92d6820b9cc7800947d1 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `search_custom_url_enabled_light.xml` comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
 
 <!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
 <vector
index 2f2f08230bddb1b225c5ad95839ff2cff4392d89..2521845ba824b6758a7636ce40309ca1fae53e7f 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `search_custom_url_ghosted_dark.xml` comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
 
 <!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
 <vector
index f1de88edf4bdb414e92d67f0868fce5bfd5195f6..f064d2b8b6b810d2190d2436eb268497427c64eb 100644 (file)
@@ -1,4 +1,4 @@
-<!-- `search_custom_url_ghosted_light.xml` comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
+<!-- This file comes from the Android Material icon set, where it is called `find_in_page`.  It is released under the Apache License 2.0. -->
 
 <!-- `tools:ignore="VectorRaster"` removes the lint warning about `android:autoMirrored="true"` not applying to API < 21. -->
 <vector
index 75d4a91dc2597b9ec524d2200d266325d2506fe2..04cae079bdb5ba559cb2ce8cca9528d080d3ce8e 100644 (file)
@@ -75,7 +75,7 @@
         <item
             android:id="@+id/downloads"
             android:title="@string/downloads"
-            android:icon="@drawable/downloads"
+            android:icon="@drawable/downloads_enabled_light"
             android:orderInCategory="80" />
     </group>
 
index 1cb4588bf6a6b09588cbeb9bf65478a098dac9bc..6c247979b5a6f7878eadba2b3de979092124e185 100644 (file)
     <string name="previous">Vorheriges</string>
     <string name="next">Nächstes</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Dateiname</string>
     <string name="save_archive">Archiv speichern</string>
     <string name="save_image">Grafik speichern</string>
             <item>Yahoo - JavaScript aktiviert</item>
             <item>Eigene</item>
         </string-array>
-        <string name="search_custom_url">Suchmaschinen-URL</string>
         <string name="custom_url">Eigene URL</string>
+        <string name="search_custom_url">Suchmaschinen-URL</string>
     <string name="proxy">Proxy</string>
         <string name="proxy_none">Keiner</string>
         <string name="proxy_tor">Tor</string>
index 088dd6173bd8a39178bf43a8f7c849cf32af9fda..e75f0343444db2eac8ef444c1daf6f06d69fdb5e 100644 (file)
     <string name="previous">Anterior</string>
     <string name="next">Siguiente</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Nombre de archivo</string>
     <string name="save_archive">Guardar archivo</string>
     <string name="save_image">Guardar imagen</string>
             <item>Yahoo - Javascript habilitado</item>
             <item>Personalizado</item>
         </string-array>
-        <string name="no_proxy_enabled">Ninguno - conectar directamente a Internet.</string>
-        <string name="tor_enabled">Tor - conectar a través de socks://localhost:9050.</string>
-        <string name="tor_enabled_kitkat">Tor - conectar a través de http://localhost:8118.</string>
-        <string name="i2p_enabled">I2P - conectar a través de http://localhost:4444.</string>
-        <string name="search_custom_url">URL personalizado de búsqueda</string>
         <string name="custom_url">URL personalizado</string>
-        <string name="proxy_custom_url">URL personalizada del proxy</string>
+        <string name="search_custom_url">URL personalizado de búsqueda</string>
     <string name="proxy">Proxy</string>
         <string name="proxy_none">Ninguno</string>
         <string name="proxy_tor">Tor</string>
             <item>I2P</item>
             <item>Personalizado</item>
         </string-array>
+        <string name="no_proxy_enabled">Ninguno - conectar directamente a Internet.</string>
+        <string name="tor_enabled">Tor - conectar a través de socks://localhost:9050.</string>
+        <string name="tor_enabled_kitkat">Tor - conectar a través de http://localhost:8118.</string>
+        <string name="i2p_enabled">I2P - conectar a través de http://localhost:4444.</string>
         <string name="custom_proxy">Proxy personalizado</string>
+    <string name="proxy_custom_url">URL personalizada del proxy</string>
     <string name="full_screen">Pantalla completa</string>
         <string name="full_screen_browsing_mode">Navegación de pantalla completa</string>
         <string name="full_screen_browsing_mode_summary">Doble toque para alternar a modo de navegación de pantalla completa.</string>
index 4a227f107e1bc8e8b794a19efe505c0bf2e33cc9..3af69bdeb8670264b3f4d8bcd9249fb44e3441f4 100644 (file)
     <string name="previous">Précédent</string>
     <string name="next">Suivant</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Nom du fichier</string>
     <string name="save_archive">Enregistrer l\'archive</string>
     <string name="save_image">Sauvegarder en tant qu\'image</string>
             <item>Yahoo - JavaScript activé</item>
             <item>Personnalisée</item>
         </string-array>
-        <string name="search_custom_url">URL de recherche personnalisée</string>
         <string name="custom_url">URL personnalisée</string>
+        <string name="search_custom_url">URL de recherche personnalisée</string>
     <string name="proxy">Proxy</string>
         <string name="proxy_none">Aucun</string>
         <string name="proxy_tor">Tor</string>
index 45e6df92dd10cecb6fc53962e0e24071bc50fc2c..a567b71e07b1f502ac674191ed8aba7c3acbf457 100644 (file)
     <string name="previous">Precedente</string>
     <string name="next">Successivo</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Nome File</string>
     <string name="save_archive">Salva Archivio</string>
     <string name="save_image">Salva Immagine</string>
             <item>Yahoo - JavaScript abilitato</item>
             <item>Personalizzata</item>
         </string-array>
-        <string name="search_custom_url">Ricerca personalizzata</string>
         <string name="custom_url">URL Personalizzata</string>
+        <string name="search_custom_url">Ricerca personalizzata</string>
     <string name="proxy">Proxy</string>
         <string name="proxy_none">Nessuno</string>
         <string name="proxy_tor">Tor</string>
index b3aeea430ab5093c66e2416c9118c5ae72b91c1d..4c5e3485aa89a3103e24f79bc1c489ed58ffce59 100644 (file)
     <string name="previous">Предыдущий</string>
     <string name="next">Следующий</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Имя файла</string>
     <string name="save_archive">Сохранить архив</string>
     <string name="save_image">Сохранить изображение</string>
             <item>Yahoo - JavaScript включен</item>
             <item>Настраиваемый</item>
         </string-array>
-        <string name="search_custom_url">Настраиваемый URL поиска</string>
         <string name="custom_url">Настраиваемый URL</string>
+        <string name="search_custom_url">Настраиваемый URL поиска</string>
     <string name="proxy">Прокси</string>
         <string name="proxy_none">Нет</string>
         <string name="proxy_tor">Tor</string>
index c1ac660a0664cc866966d590d54c0dd762e5715b..689cc1efaf846330aef762c2998f68dc685fac59 100644 (file)
     <string name="previous">Önceki</string>
     <string name="next">Sonraki</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="file_name">Dosya adı</string>
     <string name="save_image">Resmi kaydet</string>
     <string name="webpage_png">Websayfası.png</string>
             <item>Yahoo - JavaScript etkin</item>
             <item>Özel sayfa</item>
         </string-array>
-        <string name="search_custom_url">Özel sayfa ara</string>
         <string name="custom_url">Özel sayfa</string>
+        <string name="search_custom_url">Özel sayfa ara</string>
     <string name="full_screen">Tam Ekran</string>
         <string name="full_screen_browsing_mode">Tam ekran modu</string>
         <string name="full_screen_browsing_mode_summary">Tam ekran moduna geçmek için çift dokun.</string>
index 3632b7ccae5fda5dc82d049f21e41d0c5d3ae255..30cc9844b1a8be80a0adb3d3a994143a6d89f7df 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2017-2019 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2017-2020 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
@@ -60,6 +60,7 @@
     <attr name="clearIcon" format="reference" />
     <attr name="copyIcon" format="reference" />
     <attr name="deleteIcon" format="reference" />
+    <attr name="downloadIcon" format="reference" />
     <attr name="editIcon" format="reference" />
     <attr name="fontSizeIcon" format="reference" />
     <attr name="homepageIcon" format="reference" />
index a6b43d71e67ff238932054cdb750a29023253d65..3dffed5415d82feaff3261bbc53e244e3d8e99bb 100644 (file)
@@ -65,8 +65,6 @@
     <string name="loading_ultralist">Loading UltraList</string>
     <string name="loading_ultraprivacy">Loading UltraPrivacy</string>
 
-    <!-- Save As. -->
-
     <!-- Custom App Bar. -->
     <string name="favorite_icon">Favorite Icon</string>
     <string name="url_or_search_terms">URL or Search Terms</string>
     <string name="previous">Previous</string>
     <string name="next">Next</string>
 
-    <!-- Save Webpage. -->
+    <!-- Save. -->
     <string name="save_dialog" translatable="false">Save Dialog</string>  <!-- This string is used to tag the save dialog.  It is never displayed to the user. -->
     <string name="file_name">File name</string>
     <string name="save_archive">Save Archive</string>
             <item>https://search.yahoo.com/mobile/s?p=</item>
             <item>Custom URL</item>  <!-- This item must not be translated into other languages because it is referenced in code.  It is never displayed on the screen. -->
         </string-array>
-        <string name="search_custom_url">Search custom URL</string>
         <string name="custom_url">Custom URL</string>
+        <string name="search_custom_url">Search custom URL</string>
     <string name="proxy">Proxy</string>
         <string name="proxy_none">None</string>
         <string name="proxy_tor">Tor</string>
         <string name="clear_cache_summary">Clears WebView’s cache.</string>
     <string name="general">General</string>
         <string name="homepage">Homepage</string>
+        <string name="download_location">Download location</string>
+        <string-array name="download_location_entries">
+            <item>Auto</item>
+            <item>App directory</item>
+            <item>Public directory</item>
+            <item>Custom</item>
+        </string-array>
+        <string-array name="download_location_entry_values" translatable="false">  <!-- None of the items in this string array should be translated. -->
+            <item>Auto</item>
+            <item>App</item>
+            <item>Public</item>
+            <item>Custom</item>
+        </string-array>
+        <string name="download_custom_location">Download custom location</string>
         <string name="font_size_preference">Font size</string>
         <string name="open_intents_in_new_tab">Open intents in new tab</string>
         <string name="open_intents_in_new_tab_summary">Intents are links sent from other apps.</string>
     <string name="proxy_default_value" translatable="false">None</string>
     <string name="proxy_custom_url_default_value" translatable="false">http://localhost:8118</string>
     <string name="homepage_default_value" translatable="false">https://www.startpage.com/</string>
+    <string name="download_location_default_value" translatable="false">Auto</string>
+    <string name="download_custom_location_default_value" translatable="false" />
     <string name="font_size_default_value" translatable="false">100</string>
 
     <!-- Ad Control. There are no ads in the standard flavor, but these strings must exist because they are referenced in the code. -->
index cd849e2b287c577312be2f13991346d3c35908f2..2cdc1eec7d1d689ece39071b9066033e5ae0a8fe 100644 (file)
         <item name="colorPrimary">@color/blue_700</item>
         <item name="colorPrimaryDark">@color/blue_900</item>
         <item name="colorAccent">@color/blue_700</item>
-        <item name="userAgentIcon">@drawable/user_agent_light</item>
-        <item name="searchIcon">@drawable/search_enabled_light</item>
-        <item name="homepageIcon">@drawable/home_enabled_light</item>
+        <item name="downloadIcon">@drawable/downloads_enabled_light</item>
         <item name="fontSizeIcon">@drawable/font_size_light</item>
+        <item name="homepageIcon">@drawable/home_enabled_light</item>
+        <item name="searchIcon">@drawable/search_enabled_light</item>
+        <item name="userAgentIcon">@drawable/user_agent_light</item>
     </style>
 
     <!-- `ThemeOverlay.AppCompat.ActionBar` makes the hamburger icons dark. -->
     </style>
 
     <style name="PrivacyBrowserSettingsDark" parent="Theme.AppCompat" >
+        <item name="android:textColorPrimary">@color/primary_text_color_selector_dark</item>
         <item name="colorPrimary">@color/blue_800</item>
         <item name="colorPrimaryDark">@color/blue_900</item>
-        <item name="android:textColorPrimary">@color/primary_text_color_selector_dark</item>
         <item name="colorAccent">@color/blue_600</item>
-        <item name="userAgentIcon">@drawable/user_agent_dark</item>
-        <item name="searchIcon">@drawable/search_enabled_dark</item>
-        <item name="homepageIcon">@drawable/home_enabled_dark</item>
+        <item name="downloadIcon">@drawable/downloads_enabled_dark</item>
         <item name="fontSizeIcon">@drawable/font_size_dark</item>
+        <item name="homepageIcon">@drawable/home_enabled_dark</item>
+        <item name="searchIcon">@drawable/search_enabled_dark</item>
+        <item name="userAgentIcon">@drawable/user_agent_dark</item>
     </style>
 
     <!-- `ThemeOverlay.AppCompat.Dark.ActionBar` makes the text and the icons in the AppBar white. -->
index dafe62e6d232805e2d9739b6c0bf98823a91346e..3234462a9c1e5e18d2c717be614c5557f24bf827 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-  Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
+  Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
 
   This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
 
             android:inputType="textUri"
             android:icon="?attr/homepageIcon" />
 
+        <ListPreference
+            android:key="download_location"
+            android:title="@string/download_location"
+            android:entries="@array/download_location_entries"
+            android:entryValues="@array/download_location_entry_values"
+            android:defaultValue="@string/download_location_default_value"
+            android:inputType="textUri"
+            android:icon="?attr/downloadIcon" />
+
+        <EditTextPreference
+            android:key="download_custom_location"
+            android:title="@string/download_custom_location"
+            android:defaultValue="@string/download_custom_location_default_value"
+            android:inputType="textUri" />
+
         <!-- `android:inputType="number"` currently doesn't work with AndroidX. -->
         <EditTextPreference
             android:key="font_size"