Skip to content

Commit 2ef00dc

Browse files
committed
feat: allow user to choose custom local storage
1 parent 69961b7 commit 2ef00dc

File tree

7 files changed

+183
-52
lines changed

7 files changed

+183
-52
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ can be found within the above link.
149149
```
150150
/storage/emulated/0/Android/data/com.kamwithk.ankiconnectandroid/files/
151151
```
152+
153+
* Alternatively locate a different folder. This will require later customisation.
152154
153155
* After locating the two folders, copy `android.db` from the desktop's add-on folder
154156
into Ankiconnect Android's data folder.
@@ -161,6 +163,14 @@ can be found within the above link.
161163
```
162164
/storage/emulated/0/Android/data/com.kamwithk.ankiconnectandroid/files/android.db
163165
```
166+
* If you chose a different folder than the default, additional customisation is necessary on the settings page.
167+
* Locate the `Local Audio Settings` section
168+
* If you selected a different storage device such as an external SD card, you may need to update the `Choose Local Audio Storage Device` option.
169+
* The first option will always be the internal storage. If you think you have more than one storage device, it may be that android treats it as one, in which case there will only be one option.
170+
* If you selected a different directory such as `Documents`, `Audio` etc, you may need to update the `Choose Local Audio Directory` setting.
171+
* If you selected a user-owned directory like the aforementioned `Documents`, `Audio` and similar, you may also need to allow `Manage All Files` permission for Ankiconnect Android.
172+
* The `Manage All Files` permission is only necessary for folders outside of the app-owned `/Android/data/com.kamwithk.ankiconnectandroid/` directory.
173+
* If you make a mistake you can always reset the settings using `Reset Local Audio settings`. This will reset all the local audio settings and revert it to the default.
164174
165175
4. Setup local audio on Firefox Browser's Yomitan. (Warning: this URL is different than the one on desktop!)
166176
* Click on `Configure audio playback sources` and under the `Audio` section

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
55
<uses-permission android:name="android.permission.INTERNET" />
66
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
7+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
78
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
89
<!--
910
This permission is required to open another activity from an app in the background

app/src/main/java/com/kamwithk/ankiconnectandroid/SettingsActivity.java

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import android.content.Intent;
55
import android.os.Bundle;
66
import android.provider.Settings;
7+
import android.view.LayoutInflater;
8+
import android.view.View;
9+
import android.view.ViewGroup;
10+
import android.widget.ListView;
711
import android.widget.Toast;
812

913
import androidx.appcompat.app.ActionBar;
@@ -14,7 +18,14 @@
1418
import androidx.preference.Preference;
1519
import androidx.preference.PreferenceFragmentCompat;
1620

21+
import com.google.android.material.snackbar.Snackbar;
22+
23+
import org.jsoup.internal.StringUtil;
24+
1725
import java.io.File;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.Paths;
1829
import java.util.Arrays;
1930

2031

@@ -42,10 +53,13 @@ protected void onCreate(Bundle savedInstanceState) {
4253
}
4354

4455
public static class SettingsFragment extends PreferenceFragmentCompat {
56+
57+
private static final String DEFAULT_DIRECTORY_PATH = "/Android/data/com.kamwithk.ankiconnectandroid/files";
4558
@Override
4659
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
4760
setPreferencesFromResource(R.xml.root_preferences, rootKey);
4861

62+
4963
Preference preference = findPreference("access_overlay_perms");
5064
if (preference != null) {
5165
// custom handler of preference: open permissions screen
@@ -57,6 +71,85 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
5771

5872
}
5973

74+
preference = findPreference("access_manage_all_files_perms");
75+
if (preference != null) {
76+
// custom handler of preference: open permissions screen
77+
preference.setOnPreferenceClickListener(p -> {
78+
Intent permIntent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
79+
startActivity(permIntent);
80+
return true;
81+
});
82+
83+
}
84+
85+
86+
EditTextPreference corsHostPreference = findPreference("cors_hostname");
87+
if (corsHostPreference != null) {
88+
corsHostPreference.setOnBindEditTextListener(editText -> editText.setHint("e.g. http://example.com")); }
89+
90+
91+
92+
preference = findPreference("storage_location");
93+
if (preference != null) {
94+
Context context = getContext();
95+
if (context == null) {
96+
Toast.makeText(getContext(), "Cannot get local audio folder, as context is null.", Toast.LENGTH_LONG).show();
97+
} else {
98+
String[] dirs = Arrays.stream(context.getExternalFilesDirs(null)).map(File::getAbsolutePath).map(s -> s.replace(DEFAULT_DIRECTORY_PATH, "")).toArray(String[]::new);
99+
((ListPreference) preference).setEntries(dirs);
100+
((ListPreference) preference).setEntryValues(dirs);
101+
102+
if((StringUtil.isBlank(((ListPreference) preference).getValue()))){
103+
preference.setDefaultValue(dirs[0]); // The first value is equivalent to context.getExternalFilesDir(null)
104+
((ListPreference) preference).setValueIndex(0);
105+
}
106+
}
107+
}
108+
preference = findPreference("storage_dir_path");
109+
if (preference != null) {
110+
Context context = getContext();
111+
if (context == null) {
112+
Toast.makeText(getContext(), "Cannot get local audio folder, as context is null.", Toast.LENGTH_LONG).show();
113+
} else {
114+
preference.setDefaultValue(DEFAULT_DIRECTORY_PATH);
115+
preference.setOnPreferenceChangeListener((p, i) -> {
116+
ListPreference storagePreference = findPreference("storage_location");
117+
Path fullPath = Paths.get(storagePreference.getValue(), i.toString());
118+
119+
if (!Files.exists(fullPath)){
120+
Snackbar.make(context, getView(), "Not a valid directory\n"+fullPath.toString(), Snackbar.LENGTH_LONG)
121+
.show();
122+
return false;
123+
}
124+
return true;
125+
});
126+
127+
}
128+
}
129+
130+
preference = findPreference("reset_storage_settings");
131+
if (preference != null) {
132+
// custom handler of preference: open permissions screen
133+
preference.setOnPreferenceClickListener(p -> {
134+
135+
Context context = getContext();
136+
if (context == null) {
137+
Toast.makeText(getContext(), "Cannot get local audio folder, as context is null.", Toast.LENGTH_LONG).show();
138+
} else {
139+
ListPreference storageDevicePreference = findPreference("storage_location");
140+
EditTextPreference storageDirPreference = findPreference("storage_dir_path");
141+
142+
String[] dirs = Arrays.stream(context.getExternalFilesDirs(null)).map(File::getAbsolutePath).map(s -> s.replace(DEFAULT_DIRECTORY_PATH, "")).toArray(String[]::new);
143+
144+
storageDevicePreference.setValue(dirs[0]);
145+
storageDevicePreference.setValueIndex(0);
146+
storageDirPreference.setText(DEFAULT_DIRECTORY_PATH);
147+
148+
}
149+
return true;
150+
});
151+
}
152+
60153
preference = findPreference("get_dir_path");
61154
if (preference != null) {
62155
// custom handler of preference: open permissions screen
@@ -66,10 +159,12 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
66159
if (context == null) {
67160
Toast.makeText(getContext(), "Cannot get local audio folder, as context is null.", Toast.LENGTH_LONG).show();
68161
} else {
69-
ListPreference folderPreference = findPreference("storage_location");
70-
String folder = folderPreference != null && folderPreference.getValue() != null ?
71-
folderPreference.getValue() : context.getExternalFilesDir(null).getAbsolutePath();
72-
Toast.makeText(getContext(), "Local audio folder: " + folder, Toast.LENGTH_LONG).show();
162+
ListPreference storageDevicePreference = findPreference("storage_location");
163+
EditTextPreference storageDirPreference = findPreference("storage_dir_path");
164+
165+
Path fullPath = Paths.get(storageDevicePreference.getValue(), storageDirPreference.getText());
166+
167+
Toast.makeText(getContext(), "Local audio folder: " + fullPath.toString(), Toast.LENGTH_LONG).show();
73168

74169
// TODO snackbar?
75170
// getView() seems to be null...
@@ -81,23 +176,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
81176
return true;
82177
});
83178
}
84-
85-
preference = findPreference("storage_location");
86-
if (preference != null) {
87-
Context context = getContext();
88-
if (context == null) {
89-
Toast.makeText(getContext(), "Cannot get local audio folder, as context is null.", Toast.LENGTH_LONG).show();
90-
} else {
91-
String[] dirs = Arrays.stream(context.getExternalFilesDirs(null)).map(File::getAbsolutePath).toArray(String[]::new);
92-
preference.setDefaultValue(dirs[0]); // The first value is equivalent to context.getExternalFilesDir(null)
93-
((ListPreference) preference).setEntries(dirs);
94-
((ListPreference) preference).setEntryValues(dirs);
95-
}
96-
}
97-
98-
EditTextPreference corsHostPreference = findPreference("cors_hostname");
99-
if (corsHostPreference != null) {
100-
corsHostPreference.setOnBindEditTextListener(editText -> editText.setHint("e.g. http://example.com")); }
101179
}
102180
}
103181

app/src/main/java/com/kamwithk/ankiconnectandroid/routing/LocalAudioAPIRouting.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.lang.reflect.Type;
3131
import java.net.URLDecoder;
3232
import java.nio.file.Files;
33+
import java.nio.file.Path;
3334
import java.nio.file.Paths;
3435
import java.util.ArrayList;
3536
import java.util.Collections;
@@ -81,18 +82,20 @@ public LocalAudioAPIRouting(Context context) {
8182
private EntriesDatabase getDB() {
8283
// TODO global instance?
8384
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
84-
File externalFilesDir = context.getExternalFilesDir(null);
85-
String preferredFilesDirPath = sharedPreferences.getString("storage_location", externalFilesDir.getAbsolutePath());
85+
final File externalFilesDir = context.getExternalFilesDir(null);
86+
final String preferredStorageDevicePath = sharedPreferences.getString("storage_location", "");
87+
final String preferredDirectoryPath = sharedPreferences.getString("storage_dir_path", "");
88+
89+
Path preferredPath = Paths.get(preferredStorageDevicePath, preferredDirectoryPath, "android.db");
8690

8791
// If the preferences point to a file store that no longer is available
8892
// Attempt the default externalFilesDir set by the OS.
89-
if (!Files.isReadable(Paths.get(preferredFilesDirPath))){
90-
preferredFilesDirPath = externalFilesDir.getAbsolutePath();
93+
if (!Files.isReadable(preferredPath)){
94+
preferredPath = Paths.get(externalFilesDir.getAbsolutePath(), "android.db");
9195
}
9296

93-
File databasePath = new File(preferredFilesDirPath, "android.db");
9497
EntriesDatabase db = Room.databaseBuilder(context,
95-
EntriesDatabase.class, databasePath.toString()).build();
98+
EntriesDatabase.class, preferredPath.toString()).build();
9699
return db;
97100
}
98101

app/src/main/res/layout/settings_activity.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
22
xmlns:app="http://schemas.android.com/apk/res-auto"
33
android:layout_width="match_parent"
4-
android:layout_height="match_parent">
4+
android:layout_height="wrap_content">
55

66
<com.google.android.material.appbar.MaterialToolbar
77
android:id="@+id/settingsToolbar"
@@ -20,5 +20,6 @@
2020
android:id="@+id/settings"
2121
android:layout_width="match_parent"
2222
android:layout_height="wrap_content"
23+
android:paddingBottom="100dp"
2324
app:layout_constraintTop_toBottomOf="@+id/settingsToolbar" />
2425
</androidx.constraintlayout.widget.ConstraintLayout>

app/src/main/res/values/strings.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33

44
<string name="action_settings">Settings</string>
55
<string name="settings_overlay_permissions_title">Access Overlay Permissions</string>
6+
<string name="settings_manage_all_files_permissions_title">Access Manage All Files Permissions</string>
7+
<string name="settings_manage_all_files_permissions_summary">If a custom local audio directory is chosen, this permission may be required.</string>
68
<string name="settings_permissions_header">Permissions</string>
79
<string name="settings_overlay_permissions_summary">Allows Ankiconnect Android to open AnkiDroid\'s card browser.</string>
10+
<string name="settings_preferred_storage_header">Local Audio Settings</string>
811
<string name="settings_other_header">Other</string>
912
<string name="settings_forvo_language_title">Forvo language</string>
1013
<string name="settings_forvo_language_summary">Sets the language to use for Forvo audio.</string>
1114
<string name="settings_forvo_language_dialog_title">Select Language</string>
12-
<string name="settings_preferred_storage_location_title">Choose Local Audio Directory Location</string>
13-
<string name="settings_preferred_storage_location_summary">Allows you to choose where to fetch local audio data from.</string>
15+
<string name="settings_preferred_storage_location_title">Choose Local Audio Storage Device</string>
16+
<string name="settings_preferred_storage_location_summary">Allows you to choose which storage device to use, such as an external SD card. May only have one option if the external storage device is treated as internal or none is found.</string>
17+
<string name="settings_preferred_storage_dir_path_title">Choose Local Audio Directory</string>
18+
<string name="settings_preferred_storage_dir_path_summary">Allows you to choose which directory the local audio file is in. If a user-owned folder like Documents or Audio is chosen, the Manage All Files permission may also need to be granted to ensure read access to the file.</string>
19+
<string name="reset_preferred_storage_settings_title">Reset Local Audio settings</string>
20+
<string name="reset_preferred_storage_settings_summary">Resets the local audio settings back to default.</string>
1421
<string name="get_dir_path_title">Print Local Audio Directory</string>
1522
<string name="get_dir_path_title_summary">Prints the expected directory path where the local audio is searched in.</string>
1623
<string name="dialog_notif_perm_info">This app uses a persistent notification to inform you that the server is running. Please enable notifications to see this.</string>
Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,71 @@
11
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
2-
xmlns:app="http://schemas.android.com/apk/res-auto">
2+
xmlns:app="http://schemas.android.com/apk/res-auto"
3+
app:iconSpaceReserved="false">
34

4-
<PreferenceCategory app:title="@string/settings_permissions_header">
5+
<PreferenceCategory app:iconSpaceReserved="false" app:title="@string/settings_permissions_header">
56

67
<Preference
7-
app:key="access_overlay_perms"
8-
app:title="@string/settings_overlay_permissions_title"
9-
app:summary="@string/settings_overlay_permissions_summary" />
8+
app:iconSpaceReserved="false"
9+
app:key="access_overlay_perms"
10+
app:title="@string/settings_overlay_permissions_title"
11+
app:summary="@string/settings_overlay_permissions_summary" />
12+
<Preference
13+
app:iconSpaceReserved="false"
14+
app:key="access_manage_all_files_perms"
15+
app:title="@string/settings_manage_all_files_permissions_title"
16+
app:summary="@string/settings_manage_all_files_permissions_summary" />
17+
18+
</PreferenceCategory>
19+
20+
<PreferenceCategory
21+
android:title="Access from other sites"
22+
app:summary="Allow access from an external site"
23+
app:iconSpaceReserved="false">
24+
25+
<EditTextPreference
26+
app:iconSpaceReserved="false"
27+
app:dialogMessage="Enter the hostname you want to allow access from."
28+
app:dialogTitle="Enter CORS Host"
29+
app:key="cors_host"
30+
app:title="CORS Host"
31+
app:useSimpleSummaryProvider="true" />
1032

1133
</PreferenceCategory>
1234

1335
<PreferenceCategory
14-
android:title="Access from other sites"
15-
app:summary="Allow access from an external site">
36+
app:iconSpaceReserved="false"
37+
app:title="@string/settings_preferred_storage_header">
1638

39+
<ListPreference
40+
app:iconSpaceReserved="false"
41+
app:key="storage_location"
42+
app:title="@string/settings_preferred_storage_location_title"
43+
app:summary="@string/settings_preferred_storage_location_summary"/>
1744
<EditTextPreference
18-
app:dialogMessage="Enter the hostname you want to allow access from."
19-
app:dialogTitle="Enter CORS Host"
20-
app:key="cors_host"
21-
app:title="CORS Host"
22-
app:useSimpleSummaryProvider="true" />
45+
app:iconSpaceReserved="false"
46+
app:key="storage_dir_path"
47+
app:title="@string/settings_preferred_storage_dir_path_title"
48+
app:summary="@string/settings_preferred_storage_dir_path_summary"
49+
app:defaultValue="/Android/data/com.kamwithk.ankiconnectandroid/files"/>
50+
<Preference
51+
app:iconSpaceReserved="false"
52+
app:key="reset_storage_settings"
53+
app:title="@string/reset_preferred_storage_settings_title"
54+
app:summary="@string/reset_preferred_storage_settings_summary"/>
55+
<Preference
56+
app:iconSpaceReserved="false"
57+
app:key="get_dir_path"
58+
app:title="@string/get_dir_path_title"
59+
app:summary="@string/get_dir_path_title_summary" />
2360

2461
</PreferenceCategory>
2562

26-
<PreferenceCategory app:title="@string/settings_other_header">
63+
<PreferenceCategory
64+
app:iconSpaceReserved="false"
65+
app:title="@string/settings_other_header">
2766

2867
<ListPreference
68+
app:iconSpaceReserved="false"
2969
app:key="forvo_language"
3070
app:title="@string/settings_forvo_language_title"
3171
app:summary="@string/settings_forvo_language_summary"
@@ -34,14 +74,5 @@
3474
android:entries="@array/forvo_language_entries"
3575
android:entryValues="@array/forvo_language_values" />
3676

37-
<ListPreference
38-
app:key="storage_location"
39-
app:title="@string/settings_preferred_storage_location_title"
40-
app:summary="@string/settings_preferred_storage_location_summary"/>
41-
<Preference
42-
app:key="get_dir_path"
43-
app:title="@string/get_dir_path_title"
44-
app:summary="@string/get_dir_path_title_summary" />
45-
4677
</PreferenceCategory>
4778
</PreferenceScreen>

0 commit comments

Comments
 (0)