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 @@ - + + + + +