diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..0244efe91
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: https://kiwix.org/support-us/
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java b/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java
new file mode 100644
index 000000000..9faafd946
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java
@@ -0,0 +1,415 @@
+package org.kiwix.kiwixmobile.main;
+
+import android.Manifest;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import org.kiwix.kiwixmobile.BuildConfig;
+import org.kiwix.kiwixmobile.R;
+import org.kiwix.kiwixmobile.data.ZimContentProvider;
+import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.Unbinder;
+
+import static org.kiwix.kiwixmobile.utils.Constants.NOTES_DIRECTORY;
+
+/**
+ * Created by @author Aditya-Sood (21/05/19) as a part of GSoC 2019
+ *
+ * AddNoteDialog extends DialogFragment and is used to display the note corresponding to a particular
+ * article (of a particular zim file/wiki/book) as a full-screen dialog fragment.
+ *
+ * Notes are saved as text files at location: "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
+ * */
+
+public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDialogFragment.UserClickListener {
+
+ public static String TAG = "AddNoteDialog";
+
+ private SharedPreferenceUtil sharedPreferenceUtil;
+
+ @BindView(R.id.add_note_toolbar)
+ Toolbar toolbar; // Displays options for the note dialog
+ @BindView(R.id.add_note_text_view)
+ TextView addNoteTextView; // Displays article title
+ @BindView(R.id.add_note_edit_text)
+ EditText addNoteEditText; // Displays zim file title (wiki name)
+
+ private Unbinder unbinder;
+
+ private String zimFileTitle;
+ private String articleTitle;
+ private boolean noteFileExists = false;
+ private boolean noteEdited = false; // Keeps track of state of the note (whether edited since last save)
+
+ public AddNoteDialog(SharedPreferenceUtil sharedPreferenceUtil) {
+ this.sharedPreferenceUtil = sharedPreferenceUtil;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setStyle(DialogFragment.STYLE_NORMAL, sharedPreferenceUtil.nightMode() ? R.style.AddNoteDialogStyle_Night : R.style.AddNoteDialogStyle);
+
+ zimFileTitle = ZimContentProvider.getZimFileTitle();
+ articleTitle = ((MainActivity)getActivity()).getCurrentWebView().getTitle();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ View view = inflater.inflate(R.layout.dialog_add_note, container, false);
+ unbinder = ButterKnife.bind(this, view);
+
+ toolbar.setTitle(R.string.note);
+ toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ closeKeyboard();
+ exitAddNoteDialog();
+ }
+ });
+
+ toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+
+ switch (item.getItemId()) {
+ case R.id.share_note: // Opens app-chooser for sharing the note text file
+ shareNote();
+ break;
+
+ case R.id.save_note: // Saves the note as a text file
+ saveNote(addNoteEditText.getText().toString());
+ break;
+ }
+ return true;
+ }
+ });
+
+ toolbar.inflateMenu(R.menu.menu_add_note_dialog);
+ // 'Share' disabled for empty notes, 'Save' disabled for unedited notes
+ disableMenuItems();
+
+ addNoteTextView.setText(articleTitle);
+
+ // Show the previously saved note if it exists
+ displayNote();
+
+ addNoteEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ noteEdited = true;
+ enableSaveNoteMenuItem();
+ enableShareNoteMenuItem();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ return view;
+ }
+
+ // Override onBackPressed() to respond to user pressing 'Back' button on navigation bar
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new Dialog(getActivity(), getTheme()) {
+ @Override
+ public void onBackPressed() {
+ exitAddNoteDialog();
+ }
+ };
+ }
+
+ private void exitAddNoteDialog() {
+ if(noteEdited) {
+ Fragment previousInstance = getActivity().getSupportFragmentManager().findFragmentByTag(ConfirmationAlertDialogFragment.TAG);
+
+ if(previousInstance == null) {
+ // Custom AlertDialog for taking user confirmation before closing note dialog in case of unsaved changes
+ DialogFragment newFragment = new ConfirmationAlertDialogFragment(sharedPreferenceUtil, TAG, R.string.confirmation_alert_dialog_message);
+ newFragment.show(getActivity().getSupportFragmentManager(), ConfirmationAlertDialogFragment.TAG);
+ }
+
+ } else {
+ // Closing unedited note dialog straightaway
+ dismissAddNoteDialog();
+ }
+ }
+
+ private void disableMenuItems() {
+ if(toolbar.getMenu() != null) {
+ MenuItem saveItem = toolbar.getMenu().findItem(R.id.save_note);
+ MenuItem shareItem = toolbar.getMenu().findItem(R.id.share_note);
+ saveItem.setEnabled(false);
+ shareItem.setEnabled(false);
+ saveItem.getIcon().setAlpha(130);
+ shareItem.getIcon().setAlpha(130);
+
+ } else {
+ Log.d(TAG, "Toolbar without inflated menu");
+ }
+ }
+
+ private void enableSaveNoteMenuItem() {
+ if(toolbar.getMenu() != null) {
+ MenuItem saveItem = toolbar.getMenu().findItem(R.id.save_note);
+ saveItem.setEnabled(true);
+ saveItem.getIcon().setAlpha(255);
+
+ } else {
+ Log.d(TAG, "Toolbar without inflated menu");
+ }
+ }
+
+ private void enableShareNoteMenuItem() {
+ if(toolbar.getMenu() != null) {
+ MenuItem shareItem = toolbar.getMenu().findItem(R.id.share_note);
+ shareItem.setEnabled(true);
+ shareItem.getIcon().setAlpha(255);
+
+ } else {
+ Log.d(TAG, "Toolbar without inflated menu");
+ }
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ if(!noteFileExists) {
+ // Prepare for input in case of empty/new note
+ addNoteEditText.requestFocus();
+ showKeyboard();
+ }
+ }
+
+ public void showKeyboard(){
+ InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }
+
+ public void closeKeyboard(){
+ InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0);
+ }
+
+ private void saveNote(String noteText) {
+
+ /* String content of the EditText, given by noteText, is saved into the text file given by:
+ * "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
+ * */
+
+ if(isExternalStorageWritable()) {
+
+ if(ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "WRITE_EXTERNAL_STORAGE permission not granted");
+ showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG);
+ return;
+ }
+
+ File notesFolder = new File(NOTES_DIRECTORY + zimFileTitle);
+ boolean folderExists = true;
+
+ if(!notesFolder.exists()) {
+ // Try creating folder if it doesn't exist
+ folderExists = notesFolder.mkdirs();
+ }
+
+ if(folderExists) {
+ File noteFile = new File(notesFolder.getAbsolutePath(), articleTitle + ".txt");
+
+ // Save note text-file code:
+ try {
+ FileOutputStream fileOutputStream = new FileOutputStream(noteFile);
+ fileOutputStream.write(noteText.getBytes());
+ fileOutputStream.close();
+ showToast(R.string.note_save_successful, Toast.LENGTH_SHORT);
+ noteEdited = false; // As no unsaved changes remain
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG);
+ }
+
+ } else {
+ showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG);
+ Log.d(TAG, "Required folder doesn't exist");
+ }
+ }
+ else {
+ showToast(R.string.note_save_error_storage_not_writable, Toast.LENGTH_LONG);
+ }
+
+ }
+
+ private void displayNote() {
+
+ /* String content of the note text file given at:
+ * "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
+ * is displayed in the EditText field (note content area)
+ * */
+
+ File noteFile = new File(NOTES_DIRECTORY + zimFileTitle + "/" + articleTitle + ".txt");
+
+ if(noteFile.exists()) {
+ noteFileExists = true;
+
+ StringBuilder contents = new StringBuilder();
+ try {
+
+ BufferedReader input = new BufferedReader(new java.io.FileReader(noteFile));
+ try {
+ String line = null;
+
+ while((line = input.readLine()) != null) {
+ contents.append(line);
+ contents.append(System.getProperty("line.separator"));
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.d(TAG, "Error reading line with BufferedReader");
+ } finally {
+ input.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.d(TAG, "Error closing BufferedReader");
+ }
+
+ addNoteEditText.setText(contents.toString()); // Display the note content
+
+ enableShareNoteMenuItem(); // As note content exists which can be shared
+ }
+
+ // No action in case the note file for the currently open article doesn't exist
+ }
+
+ private void shareNote() {
+
+ /* The note text file corresponding to the currently open article, given at:
+ * "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
+ * is shared via an app-chooser intent
+ * */
+
+ if(noteEdited) {
+ saveNote(addNoteEditText.getText().toString()); // Save edited note before sharing the text file
+ }
+
+ File noteFile = new File(NOTES_DIRECTORY + zimFileTitle + "/" + articleTitle + ".txt");
+
+ Uri noteFileUri = null;
+ if(noteFile.exists()) {
+
+ if (Build.VERSION.SDK_INT >= 24) {
+ // From Nougat 7 (API 24) access to files is shared temporarily with other apps
+ // Need to use FileProvider for the same
+ noteFileUri = FileProvider.getUriForFile(getContext(), BuildConfig.APPLICATION_ID+".fileprovider", noteFile);
+
+ } else {
+ noteFileUri = Uri.fromFile(noteFile);
+ }
+
+ } else {
+ showToast(R.string.note_share_error_file_missing, Toast.LENGTH_SHORT);
+ }
+
+ if(noteFileUri != null) {
+ Intent noteFileShareIntent = new Intent(Intent.ACTION_SEND);
+ noteFileShareIntent.setType("application/octet-stream");
+ noteFileShareIntent.putExtra(Intent.EXTRA_STREAM, noteFileUri);
+ noteFileShareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Intent shareChooser = Intent.createChooser(noteFileShareIntent, getString(R.string.note_share_app_chooser_title));
+
+ if(noteFileShareIntent.resolveActivity(getActivity().getPackageManager()) != null) {
+ startActivity(shareChooser);
+ }
+ }
+ }
+
+ public static boolean isExternalStorageWritable() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ private void showToast(int stringResource, int duration) {
+ Toast.makeText(getActivity(), stringResource, duration).show();
+ }
+
+ // Methods from ConfirmationAlertDialogFragment.UserClickListener interface
+ @Override
+ public void onPositiveClick() {
+ dismissAddNoteDialog();
+ }
+
+ @Override
+ public void onNegativeClick() {
+ // Do nothing
+ }
+
+ private void dismissAddNoteDialog() {
+ Dialog dialog = getDialog();
+ dialog.dismiss();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Dialog dialog = getDialog();
+ if(dialog != null) {
+ int width = ViewGroup.LayoutParams.MATCH_PARENT;
+ int height = ViewGroup.LayoutParams.MATCH_PARENT;
+ dialog.getWindow().setLayout(width, height);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (unbinder != null) {
+ unbinder.unbind();
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/ConfirmationAlertDialogFragment.java b/app/src/main/java/org/kiwix/kiwixmobile/main/ConfirmationAlertDialogFragment.java
new file mode 100644
index 000000000..4680e1e68
--- /dev/null
+++ b/app/src/main/java/org/kiwix/kiwixmobile/main/ConfirmationAlertDialogFragment.java
@@ -0,0 +1,83 @@
+package org.kiwix.kiwixmobile.main;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import org.kiwix.kiwixmobile.R;
+import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil;
+
+
+/**
+ * Created by @Aditya-Sood as a part of GSoC 2019
+ *
+ * This is a generic helper class for displaying a 2-button (positive & negative) confirmation dialog fragment on top of an existing dialog fragment
+ * - Only for confirmation dialogs with a Positive & Negative button
+ * - If you also need a Neutral button, add it selectively (if-else) for the required use case (Take care of the callback interface as well)
+ *
+ * Currently used as:
+ * - Helper class to show the alert dialog in case the user tries to exit the {@link AddNoteDialog} with unsaved file changes
+ *
+ **/
+
+public class ConfirmationAlertDialogFragment extends DialogFragment {
+
+ public static String TAG = "ConfirmationAlertDialog";
+
+ private SharedPreferenceUtil sharedPreferenceUtil;
+ private int stringResourceId;
+ private String parentDialogFragmentTAG;
+
+ public ConfirmationAlertDialogFragment(SharedPreferenceUtil sharedPreferenceUtil, String parentDialogFragmentTAG, int stringResourceId) {
+ this.sharedPreferenceUtil = sharedPreferenceUtil;
+ this.parentDialogFragmentTAG = parentDialogFragmentTAG;
+ this.stringResourceId = stringResourceId;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Fragment parentDialogFragment = getFragmentManager().findFragmentByTag(parentDialogFragmentTAG);
+
+ AlertDialog.Builder builder;
+
+ if (sharedPreferenceUtil != null && sharedPreferenceUtil.nightMode()) { // Night Mode support
+ builder = new AlertDialog.Builder(getActivity(), R.style.AppTheme_Dialog_Night);
+ } else {
+ builder = new AlertDialog.Builder(getActivity());
+ }
+
+ builder.setMessage(stringResourceId)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ if(parentDialogFragment != null) {
+ ((UserClickListener) parentDialogFragment).onPositiveClick();
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ if(parentDialogFragment != null) {
+ ((UserClickListener) parentDialogFragment).onNegativeClick();
+ }
+ }
+ });
+
+ return builder.create();
+ }
+
+ /** Callback interface for responding to user clicks to a {@link ConfirmationAlertDialogFragment} dialog */
+ public interface UserClickListener {
+ void onPositiveClick();
+
+ void onNegativeClick();
+ }
+
+}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
index fea4b65b6..a618bd884 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java
@@ -25,6 +25,7 @@ import android.appwidget.AppWidgetManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@@ -36,6 +37,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
+import android.os.Environment;
import android.os.Handler;
import android.provider.Settings;
import android.text.SpannableString;
@@ -70,6 +72,8 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -131,11 +135,15 @@ import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_LIBRARY;
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_SEARCH;
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_ZIM_FILE;
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_ZIM_FILE_2;
+import static org.kiwix.kiwixmobile.utils.Constants.NOTES_DIRECTORY;
import static org.kiwix.kiwixmobile.utils.Constants.PREF_KIWIX_MOBILE;
import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_FILE_SEARCH;
import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_FILE_SELECT;
+import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_HISTORY_ITEM_CHOSEN;
import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_PREFERENCES;
+import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_READ_STORAGE_PERMISSION;
import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_STORAGE_PERMISSION;
+import static org.kiwix.kiwixmobile.utils.Constants.REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE;
import static org.kiwix.kiwixmobile.utils.Constants.RESULT_HISTORY_CLEARED;
import static org.kiwix.kiwixmobile.utils.Constants.RESULT_RESTART;
import static org.kiwix.kiwixmobile.utils.Constants.TAG_CURRENT_ARTICLES;
@@ -151,8 +159,6 @@ import static org.kiwix.kiwixmobile.utils.UpdateUtils.reformatProviderUrl;
public class MainActivity extends BaseActivity implements WebViewCallback,
MainContract.View{
- private static final int REQUEST_READ_STORAGE_PERMISSION = 2;
- private static final int REQUEST_HISTORY_ITEM_CHOSEN = 99;
private static final String NEW_TAB = "NEW_TAB";
private static final String HOME_URL = "file:///android_asset/home.html";
public static boolean isFullscreenOpened;
@@ -445,11 +451,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
hideTabSwitcher();
selectTab(position);
- /* Bug Fix
- * Issue #592 in which the navigational history of the previously open tab (WebView) was
- * carried forward to the newly selected/opened tab; causing erroneous enabling of
- * navigational buttons.
- */
+ /* Bug Fix #592 */
updateBottomToolbarArrowsAlpha();
}
@@ -856,6 +858,19 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
compatCallback.showSoftInput();
break;
+ case R.id.menu_add_note:
+ if(requestExternalStorageWritePermissionForNotes()) {
+ // Check permission since notes are stored in the public-external storage
+ showAddNoteDialog();
+ }
+ break;
+
+ case R.id.menu_clear_notes:
+ if(requestExternalStorageWritePermissionForNotes()) { // Check permission since notes are stored in the public-external storage
+ showClearAllNotesDialog();
+ }
+ break;
+
case R.id.menu_bookmarks_list:
goToBookmarks();
break;
@@ -915,6 +930,117 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
return super.onOptionsItemSelected(item);
}
+ /** Dialog to take user confirmation before deleting all notes */
+ private void showClearAllNotesDialog() {
+
+ AlertDialog.Builder builder;
+ if (sharedPreferenceUtil != null && sharedPreferenceUtil.nightMode()) { // Night Mode support
+ builder = new AlertDialog.Builder(this, R.style.AppTheme_Dialog_Night);
+ } else {
+ builder = new AlertDialog.Builder(this);
+ }
+
+ builder.setMessage(R.string.delete_notes_confirmation_msg)
+ .setNegativeButton(android.R.string.cancel, null) // Do nothing for 'Cancel' button
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ clearAllNotes();
+ }
+ })
+ .show();
+ }
+
+ /** Method to delete all user notes */
+ private void clearAllNotes() {
+
+ boolean result = true; // Result of all delete() calls is &&-ed to this variable
+
+ if(AddNoteDialog.isExternalStorageWritable()) {
+ if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ Log.d("MainActivity", "WRITE_EXTERNAL_STORAGE permission not granted");
+ showToast(R.string.ext_storage_permission_not_granted, Toast.LENGTH_LONG);
+ return;
+ }
+
+ // TODO: Replace below code with Kotlin's deleteRecursively() method
+
+ File notesDirectory = new File(NOTES_DIRECTORY);
+ File[] filesInNotesDirectory = notesDirectory.listFiles();
+
+ if(filesInNotesDirectory == null) { // Notes folder doesn't exist
+ showToast(R.string.notes_deletion_none_found, Toast.LENGTH_LONG);
+ return;
+ }
+
+ for(File wikiFileDirectory : filesInNotesDirectory) {
+ if(wikiFileDirectory.isDirectory()) {
+ File[] filesInWikiDirectory = wikiFileDirectory.listFiles();
+
+ for(File noteFile : filesInWikiDirectory) {
+ if(noteFile.isFile()) {
+ result = result && noteFile.delete();
+ }
+ }
+ }
+
+ result = result && wikiFileDirectory.delete(); // Wiki specific notes directory deleted
+ }
+
+ result = result && notesDirectory.delete(); // "{External Storage}/Kiwix/Notes" directory deleted
+ }
+
+ if(result) {
+ showToast(R.string.notes_deletion_successful, Toast.LENGTH_SHORT);
+ } else {
+ showToast(R.string.notes_deletion_unsuccessful, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /** Creates the full screen AddNoteDialog, which is a DialogFragment */
+ private void showAddNoteDialog() {
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ Fragment previousInstance = getSupportFragmentManager().findFragmentByTag(AddNoteDialog.TAG);
+
+ // To prevent multiple instances of the DialogFragment
+ if(previousInstance == null) {
+ /* Since the DialogFragment is never added to the back-stack, so findFragmentByTag()
+ * returning null means that the AddNoteDialog is currently not on display (as doesn't exist)
+ **/
+ AddNoteDialog dialogFragment = new AddNoteDialog(sharedPreferenceUtil);
+ dialogFragment.show(fragmentTransaction, AddNoteDialog.TAG); // For DialogFragments, show() handles the fragment commit and display
+ }
+ }
+
+ private boolean requestExternalStorageWritePermissionForNotes() {
+ if(Build.VERSION.SDK_INT >= 23) { // For Marshmallow & higher API levels
+
+ if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+
+ } else {
+ if(shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ /* shouldShowRequestPermissionRationale() returns false when:
+ * 1) User has previously checked on "Don't ask me again", and/or
+ * 2) Permission has been disabled on device
+ */
+ showToast(R.string.ext_storage_permission_rationale_add_note, Toast.LENGTH_LONG);
+ }
+
+ requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE);
+ }
+
+ } else { // For Android versions below Marshmallow 6.0 (API 23)
+ return true; // As already requested at install time
+ }
+
+ return false;
+ }
+
+ private void showToast(int stringResource, int duration) {
+ Toast.makeText(this, stringResource, duration).show();
+ }
+
@SuppressWarnings("SameReturnValue")
@OnLongClick(R.id.bottom_toolbar_bookmark)
boolean goToBookmarks() {
@@ -1084,6 +1210,20 @@ public class MainActivity extends BaseActivity implements WebViewCallback,
startActivity(intent);
}).show();
}
+ break;
+ }
+
+ case REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE: {
+
+ if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Successfully granted permission, so opening the note keeper
+ showAddNoteDialog();
+
+ } else {
+ Toast.makeText(getApplicationContext(), getString(R.string.ext_storage_write_permission_denied_add_note), Toast.LENGTH_LONG);
+ }
+
+ break;
}
}
}
diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
index e2dbd7a89..64dd83e99 100644
--- a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
+++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java
@@ -17,6 +17,8 @@
*/
package org.kiwix.kiwixmobile.utils;
+import android.os.Environment;
+
import org.kiwix.kiwixmobile.BuildConfig;
public final class Constants {
@@ -30,6 +32,12 @@ public final class Constants {
public static final int REQUEST_STORAGE_PERMISSION = 1;
+ public static final int REQUEST_READ_STORAGE_PERMISSION = 2;
+
+ public static final int REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE = 3;
+
+ public static final int REQUEST_HISTORY_ITEM_CHOSEN = 99;
+
public static final int REQUEST_FILE_SELECT = 1234;
public static final int REQUEST_PREFERENCES = 1235;
@@ -130,4 +138,7 @@ public final class Constants {
public static final String OLD_PROVIDER_DOMAIN = "org.kiwix.zim.base";
public static final String NEW_PROVIDER_DOMAIN = BuildConfig.APPLICATION_ID + ".zim.base";
+
+ // Path Constants
+ public static final String NOTES_DIRECTORY = Environment.getExternalStorageDirectory() + "/Kiwix/Notes/";
}
diff --git a/app/src/main/res/drawable/ic_add_note.xml b/app/src/main/res/drawable/ic_add_note.xml
new file mode 100644
index 000000000..d6e151f22
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_note.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_delete_sweep_24px.xml b/app/src/main/res/drawable/ic_baseline_delete_sweep_24px.xml
new file mode 100644
index 000000000..c5495c74c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_delete_sweep_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close_white_24dp.xml b/app/src/main/res/drawable/ic_close_white_24dp.xml
index 4a1ffc298..6af45f517 100644
--- a/app/src/main/res/drawable/ic_close_white_24dp.xml
+++ b/app/src/main/res/drawable/ic_close_white_24dp.xml
@@ -1,6 +1,6 @@
diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml
new file mode 100644
index 000000000..3a06e8a60
--- /dev/null
+++ b/app/src/main/res/drawable/ic_save.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/dialog_add_note.xml b/app/src/main/res/layout/dialog_add_note.xml
new file mode 100644
index 000000000..4694d289e
--- /dev/null
+++ b/app/src/main/res/layout/dialog_add_note.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_add_note_dialog.xml b/app/src/main/res/menu/menu_add_note_dialog.xml
new file mode 100644
index 000000000..37ba47112
--- /dev/null
+++ b/app/src/main/res/menu/menu_add_note_dialog.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 243fa7fca..cad504d35 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -14,6 +14,18 @@
app:actionLayout="@layout/ic_tab_switcher"
app:showAsAction="always"/>
+
+
+
+
- Unknown HTTP code received
Unknown
HTTP code %s
-
+ Add Note
+ Save
+ Note
+ Wiki Article Title
+ Storage access is required for Notes
+ Notes can\'t be used without access to storage
+ Note save unsuccessful
+ Note saved
+ Error saving note: Storage not writable
+ Note file doesn\'t exist
+ Share note file with:
+ Discard unsaved changes?
+ Clear All Notes
+ Delete all notes?
+ Error: Storage permissions not granted
+ No notes found for deletion
+ Entire notes folder deleted
+ Some files not deleted
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7cdad26be..3a2425c87 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -129,5 +129,29 @@
-
+
+
+
+
+