Merge pull request #1198 from kiwix/feature/NoteKeeper

GSoC 2019 Deliverable: Note Keeper
This commit is contained in:
Isaac Hutt 2019-06-26 21:23:51 +01:00 committed by GitHub
commit 0539d56849
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 844 additions and 10 deletions

12
.github/FUNDING.yml vendored Normal file
View File

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

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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/";
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,18h12v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h18v-2L3,11v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z"/>
</vector>

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:tint="#ffffff"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp">

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_add_note_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/add_note_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/app_bar_add_note_dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/add_note_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:background="@android:color/transparent"
android:hint="@string/wiki_article_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:paddingTop="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_marginRight="15dp"
android:background="#000000"/>
<EditText
android:id="@+id/add_note_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:minLines="6"
android:gravity="top|left"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:scrollbars="vertical"
android:background="@android:color/transparent"
android:hint="@string/note"/>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/share_note"
android:icon="@drawable/baseline_share_24"
android:title="@string/share"
app:showAsAction="always"/>
<item android:id="@+id/save_note"
android:icon="@drawable/ic_save"
android:title="@string/save"
app:showAsAction="always"/>
</menu>

View File

@ -14,6 +14,18 @@
app:actionLayout="@layout/ic_tab_switcher"
app:showAsAction="always"/>
<item
android:id="@+id/menu_add_note"
android:title="@string/add_note"
android:icon="@drawable/ic_add_note"
app:showAsAction="always"/>
<item
android:id="@+id/menu_clear_notes"
android:title="@string/clear_all_notes"
android:icon="@drawable/ic_baseline_delete_sweep_24px"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_new_tab"
android:title="@string/new_tab"

View File

@ -264,4 +264,22 @@
<string name="failed_unhandled_http_code">Unknown HTTP code received</string>
<string name="failed_unknown">Unknown</string>
<string name="failed_http_code">HTTP code %s</string>
</resources>
<string name="add_note">Add Note</string>
<string name="save">Save</string>
<string name="note">Note</string>
<string name="wiki_article_title">Wiki Article Title</string>
<string name="ext_storage_permission_rationale_add_note">Storage access is required for Notes</string>
<string name="ext_storage_write_permission_denied_add_note">Notes can\'t be used without access to storage</string>
<string name="note_save_unsuccessful">Note save unsuccessful</string>
<string name="note_save_successful">Note saved</string>
<string name="note_save_error_storage_not_writable">Error saving note: Storage not writable</string>
<string name="note_share_error_file_missing">Note file doesn\'t exist</string>
<string name="note_share_app_chooser_title">Share note file with:</string>
<string name="confirmation_alert_dialog_message">Discard unsaved changes?</string>
<string name="clear_all_notes">Clear All Notes</string>
<string name="delete_notes_confirmation_msg">Delete all notes?</string>
<string name="ext_storage_permission_not_granted">Error: Storage permissions not granted</string>
<string name="notes_deletion_none_found">No notes found for deletion</string>
<string name="notes_deletion_successful">Entire notes folder deleted</string>
<string name="notes_deletion_unsuccessful">Some files not deleted</string>
</resources>

View File

@ -129,5 +129,29 @@
<style name="NeutralButtonStyle" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
<item name="android:textColor">@color/accent</item>
</style>
</resources>
<style name="AddNoteDialogStyle" parent="Theme.AppCompat.Dialog">
<item name="android:windowNoTitle">true</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorPrimary">@color/primary</item>
<item name="android:textColor">#000000</item>
<item name="android:editTextColor">#000000</item>
<item name="android:textColorHint">@color/textDarkTertiary</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@color/white</item>
</style>
<style name="AddNoteDialogStyle.Night" parent="Theme.AppCompat.Dialog">
<item name="android:windowNoTitle">true</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorPrimary">@color/primary</item>
<item name="android:textColor">#ffffff</item>
<item name="android:editTextColor">#ffffff</item>
<item name="android:textColorHint">@color/grey</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@color/cardview_dark_background</item>
</style>
</resources>