Repo created
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
86
app/build.gradle
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "kotlin-android"
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply plugin: "kotlin-kapt"
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion '33.0.0'
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 32
|
||||
versionCode 19
|
||||
versionName "1.19"
|
||||
applicationId "org.bisw.nxbookmarks"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
||||
flavorDimensions "default"
|
||||
buildTypes {
|
||||
release {
|
||||
// Do not minify: https://github.com/nextcloud/Android-SingleSignOn/issues/488
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
owncloud {
|
||||
// Do not minify: https://github.com/nextcloud/Android-SingleSignOn/issues/488
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
applicationIdSuffix ".owncloud"
|
||||
}
|
||||
}
|
||||
productFlavors {
|
||||
fdroid {
|
||||
applicationId "org.schabi.nxbookmarks"
|
||||
}
|
||||
amazon{
|
||||
applicationId "org.bisw.nxbookmarks"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
useIR = true
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding true
|
||||
}
|
||||
lint {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//implementation 'com.github.desperateCoder:Android-SingleSignOn:273-multiple-params-for-key-SNAPSHOT'
|
||||
implementation "com.github.nextcloud:Android-SingleSignOn:0.6.0"
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.5.0', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
implementation 'org.eclipse.birt.runtime.3_7_1:org.apache.commons.codec:1.3.0'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
}
|
||||
28
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/the-scrabi/bin/Android/Sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
-keep class org.jsoup.**
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumentation test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("org.schabi.ocbookmarks", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
54
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.schabi.ocbookmarks">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name_short"
|
||||
android:exported="true"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".LoginAcitivty"
|
||||
android:exported="true"
|
||||
android:theme="@style/LoginStyle">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".AddBookmarkActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/share_target_normal"
|
||||
android:theme="@style/Transparent">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".QuickaddBookmarkActivity"
|
||||
android:label="@string/share_target_quick"
|
||||
android:exported="true"
|
||||
android:theme="@style/Transparent">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-web.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -0,0 +1,103 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.nextcloud.android.sso.BuildConfig;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.OCBookmarksRestConnector;
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.api.LoginData;
|
||||
import org.schabi.ocbookmarks.api.SSOUtil;
|
||||
import org.schabi.ocbookmarks.listener.BookmarkListener;
|
||||
|
||||
public class AddBookmarkActivity extends AppCompatActivity implements BookmarkListener {
|
||||
|
||||
LoginData loginData = new LoginData();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.add_bookmark_activity);
|
||||
setTitle("");
|
||||
|
||||
Intent intent = getIntent();
|
||||
String title = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
String url = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
EditBookmarkDialog bookmarkDialog = new EditBookmarkDialog();
|
||||
bookmarkDialog.newBookmark(title, url);
|
||||
AlertDialog dialog = bookmarkDialog.getDialog(this, null, this);
|
||||
dialog.show();
|
||||
|
||||
dialog.setOnDismissListener(dialog1 -> AddBookmarkActivity.this.finish());
|
||||
}
|
||||
|
||||
|
||||
private void update(Bookmark bookmark) {
|
||||
AsyncTask<Void, Void, String> updateTask = new AsyncTask<Void, Void, String>() {
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
NextcloudAPI nextcloudAPI = null;
|
||||
try {
|
||||
SingleSignOnAccount ssoa = SingleAccountHelper.getCurrentSingleSignOnAccount(AddBookmarkActivity.this.getApplicationContext());
|
||||
nextcloudAPI = SSOUtil.getNextcloudAPI(AddBookmarkActivity.this, ssoa);
|
||||
} catch (NextcloudFilesAppAccountNotFoundException e) {
|
||||
Toast.makeText(AddBookmarkActivity.this,
|
||||
R.string.nextcloud_files_app_account_not_found_message,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} catch ( NoCurrentAccountSelectedException e) {
|
||||
Toast.makeText(AddBookmarkActivity.this,
|
||||
R.string.no_current_account_selected_exception_message,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(nextcloudAPI);
|
||||
try {
|
||||
connector.addBookmark(bookmark);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
return getString(R.string.could_not_add_bookmark);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if(result != null) {
|
||||
Toast.makeText(AddBookmarkActivity.this,
|
||||
result,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(AddBookmarkActivity.this,
|
||||
R.string.bookmark_saved,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bookmarkChanged(Bookmark bookmark) {
|
||||
update(bookmark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBookmark(Bookmark bookmark) {}
|
||||
}
|
||||
208
app/src/main/java/org/schabi/ocbookmarks/BookmarkFragment.java
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.REST.model.BookmarkListElement;
|
||||
import org.schabi.ocbookmarks.REST.model.Folder;
|
||||
import org.schabi.ocbookmarks.listener.BookmarkListener;
|
||||
import org.schabi.ocbookmarks.listener.FolderListener;
|
||||
import org.schabi.ocbookmarks.listener.OnRequestReloadListener;
|
||||
import org.schabi.ocbookmarks.ui.BookmarksRecyclerViewAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 15.05.17.
|
||||
*/
|
||||
|
||||
public class BookmarkFragment extends Fragment implements FolderListener {
|
||||
|
||||
private ArrayList<Bookmark> mBookmarkList = new ArrayList<>();
|
||||
private ArrayList<BookmarkListElement> mFilteredBookmarks = new ArrayList<>();
|
||||
private BookmarksRecyclerViewAdapter mAdapter;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
|
||||
private Folder mRootFolder;
|
||||
private Folder mCurrentFolder;
|
||||
|
||||
private String mTagFilter = "";
|
||||
private String mSearchTerm = "";
|
||||
|
||||
|
||||
private OnRequestReloadListener onRequestReloadListener = null;
|
||||
private BookmarkListener bookmarkListener = null;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void changeFolderCallback(@NonNull Folder f) {
|
||||
if(f.getId() == Folder.UP_ID) {
|
||||
f = getFolderFromID(mCurrentFolder.getParentFolderId());
|
||||
}
|
||||
buildCurrentView(f);
|
||||
}
|
||||
|
||||
|
||||
public void setBookmarkListener(BookmarkListener listener) {
|
||||
bookmarkListener = listener;
|
||||
}
|
||||
|
||||
public void setOnRequestReloadListener(OnRequestReloadListener listener) {
|
||||
onRequestReloadListener = listener;
|
||||
}
|
||||
|
||||
public boolean onBackHandled() {
|
||||
if(mCurrentFolder == null) {
|
||||
return false;
|
||||
}
|
||||
if(mRootFolder.getId() == mCurrentFolder.getId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Folder f = getFolderFromID(mCurrentFolder.getParentFolderId());
|
||||
buildCurrentView(f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Folder getFolderFromID(int id) {
|
||||
if(id == mRootFolder.getId()) {
|
||||
return mRootFolder;
|
||||
}
|
||||
return getFolderFromID(id, mRootFolder);
|
||||
}
|
||||
|
||||
private Folder getFolderFromID(int id, Folder level) {
|
||||
for(Folder f: level.getChildren()){
|
||||
if(f.getId() == id) {
|
||||
return f;
|
||||
}
|
||||
Folder m = getFolderFromID(id, f);
|
||||
if(m != null) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fagment_bookmarks, container, false);
|
||||
|
||||
refreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swiperefresh_bookmarks);
|
||||
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.rv);
|
||||
mAdapter = new BookmarksRecyclerViewAdapter(mFilteredBookmarks, getContext());
|
||||
mAdapter.setBookmarkListener(bookmarkListener);
|
||||
mAdapter.setBookmarkFolderListener(this);
|
||||
recyclerView.setAdapter(mAdapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
|
||||
refreshLayout.setOnRefreshListener(() -> {
|
||||
if(onRequestReloadListener != null) {
|
||||
onRequestReloadListener.requestReload();
|
||||
}
|
||||
});
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public void buildCurrentView(Folder currentFolder) {
|
||||
mCurrentFolder = currentFolder;
|
||||
mFilteredBookmarks = new ArrayList<>();
|
||||
|
||||
if(!isCurrentFolderRoot()) {
|
||||
Folder up = new Folder();
|
||||
up.setTitle("..");
|
||||
up.setId(Folder.UP_ID);
|
||||
mFilteredBookmarks.add(new BookmarkListElement(up));
|
||||
}
|
||||
|
||||
for (Folder f: currentFolder.getChildren()) {
|
||||
mFilteredBookmarks.add(new BookmarkListElement(f));
|
||||
}
|
||||
|
||||
for (Bookmark b : mBookmarkList) {
|
||||
boolean shouldAdd = true;
|
||||
|
||||
if(!mSearchTerm.equals("") &&
|
||||
!b.getTitle().contains(mSearchTerm) &&
|
||||
!b.getDescription().contains(mSearchTerm) &&
|
||||
!b.getUrl().contains(mSearchTerm)
|
||||
) {
|
||||
shouldAdd = false;
|
||||
}
|
||||
|
||||
if(!mTagFilter.equals("") && !b.getTags().contains(mTagFilter)) {
|
||||
shouldAdd = false;
|
||||
}
|
||||
|
||||
if (b.getFolders().contains(currentFolder.getId()) && shouldAdd){
|
||||
mFilteredBookmarks.add(new BookmarkListElement(b));
|
||||
}
|
||||
}
|
||||
|
||||
mAdapter.updateBookmarklist(mFilteredBookmarks);
|
||||
}
|
||||
|
||||
public void showByTag(String tag) {
|
||||
mTagFilter = tag;
|
||||
buildCurrentView(mCurrentFolder);
|
||||
}
|
||||
|
||||
public void releaseTag() {
|
||||
mTagFilter = "";
|
||||
buildCurrentView(mCurrentFolder);
|
||||
}
|
||||
|
||||
public void search(String term) {
|
||||
mSearchTerm = term;
|
||||
buildCurrentView(mCurrentFolder);
|
||||
}
|
||||
|
||||
public void clearSearch() {
|
||||
mSearchTerm = "";
|
||||
buildCurrentView(mCurrentFolder);
|
||||
}
|
||||
|
||||
public void updateData(Folder hierarchy, Bookmark[] bookmarks) {
|
||||
mRootFolder = hierarchy;
|
||||
mBookmarkList.clear();
|
||||
mFilteredBookmarks.clear();
|
||||
mSearchTerm = "";
|
||||
mTagFilter = "";
|
||||
mBookmarkList.addAll(Arrays.asList(bookmarks));
|
||||
buildCurrentView(mRootFolder);
|
||||
}
|
||||
|
||||
public boolean isCurrentFolderRoot() {
|
||||
return mRootFolder.equals(mCurrentFolder);
|
||||
}
|
||||
|
||||
public void setRefreshing(boolean refresh) {
|
||||
refreshLayout.setRefreshing(refresh);
|
||||
}
|
||||
|
||||
public Folder getCurrentFolder() {
|
||||
if(mCurrentFolder == null) {
|
||||
Log.e("TAG", "gcf" + mRootFolder.getTitle());
|
||||
return mRootFolder;
|
||||
}
|
||||
Log.e("TAG", "gcf" + mCurrentFolder.getTitle());
|
||||
return mCurrentFolder;
|
||||
}
|
||||
}
|
||||
162
app/src/main/java/org/schabi/ocbookmarks/EditBookmarkDialog.java
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.listener.BookmarkListener;
|
||||
import org.schabi.ocbookmarks.ui.TagsRecyclerViewAdapter;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public class EditBookmarkDialog {
|
||||
ArrayList<String> tagList = new ArrayList<>();
|
||||
private static final String logTAG = "ocbookmarks";
|
||||
Bookmark bookmark;
|
||||
String title = "";
|
||||
String url = "";
|
||||
|
||||
private BookmarkListener onBookmarkChangedListener;
|
||||
|
||||
public void newBookmark(final String title, final String url) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public AlertDialog getDialog(final Activity context, Bookmark b, BookmarkListener listener) {
|
||||
onBookmarkChangedListener = listener;
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View view = inflater.inflate(R.layout.edit_bookmark_dialog, null);
|
||||
final Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar);
|
||||
final EditText urlInput = (EditText) view.findViewById(R.id.urlInput);
|
||||
final EditText titleInput = (EditText) view.findViewById(R.id.titleInput);
|
||||
final EditText descriptionInput = (EditText) view.findViewById(R.id.descriptionInput);
|
||||
|
||||
String dialogTitle = null;
|
||||
|
||||
if(b == null) {
|
||||
dialogTitle = "Add bookmark";
|
||||
bookmark = Bookmark.emptyInstance();
|
||||
bookmark.setTitle(title);
|
||||
bookmark.setUrl(url);
|
||||
} else {
|
||||
dialogTitle = "Edit bookmark";
|
||||
bookmark = b;
|
||||
}
|
||||
urlInput.setText(bookmark.getUrl());
|
||||
titleInput.setText(bookmark.getTitle());
|
||||
descriptionInput.setText(bookmark.getDescription());
|
||||
|
||||
for(String tag : bookmark.getTags()) {
|
||||
tagList.add(tag);
|
||||
}
|
||||
|
||||
toolbar.setTitle(dialogTitle);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.edit_bookmark_menu);
|
||||
|
||||
final AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setCancelable(true)
|
||||
.setView(view)
|
||||
.create();
|
||||
|
||||
|
||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
//Here Actually we are saving Data to NextCloud
|
||||
if(item.getItemId() == R.id.save_menu) {
|
||||
bookmark.setUrl(urlInput.getText().toString());
|
||||
bookmark.setTitle(titleInput.getText().toString());
|
||||
bookmark.setDescription(descriptionInput.getText().toString());
|
||||
|
||||
if(bookmark.getUrl().isEmpty()){
|
||||
Toast.makeText(context, R.string.no_url_entered, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
else {
|
||||
bookmark.setTags(tagList);
|
||||
if (onBookmarkChangedListener != null) {
|
||||
onBookmarkChangedListener.bookmarkChanged(bookmark);
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// setup recycler view
|
||||
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.tag_recycler_view);
|
||||
final TagsRecyclerViewAdapter adapter = new TagsRecyclerViewAdapter(context, true, tagList);
|
||||
adapter.setOnTagDeletedListener(new TagsRecyclerViewAdapter.OnTagDeletedListener() {
|
||||
@Override
|
||||
public void onTagDeleted(String tag) {
|
||||
tagList.remove(tag);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
adapter.setOnTagEditedListener(new TagsRecyclerViewAdapter.OnTagEditedListener() {
|
||||
@Override
|
||||
public void onTagEdited(String oldTag, String newTag) {
|
||||
if(newTag.isEmpty()) {
|
||||
tagList.remove(oldTag);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
if (newTag != oldTag) {
|
||||
int oldTagPos = tagList.indexOf(oldTag);
|
||||
if (oldTagPos >= 0) {
|
||||
tagList.set(tagList.indexOf(oldTag), newTag);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(context, 2));
|
||||
|
||||
fixTitlebarColor(toolbar, context);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void fixTitlebarColor(Toolbar toolbar, Context context) {
|
||||
int textColor = 0;
|
||||
if(SDK_INT <= 23) {
|
||||
textColor = Color.parseColor("#ffffffff");
|
||||
} else {
|
||||
textColor = context.getColor(R.color.editTitlebarTextColor);
|
||||
}
|
||||
toolbar.setTitleTextColor(textColor);
|
||||
TextView saveItem = (TextView) toolbar.findViewById(R.id.save_menu);
|
||||
saveItem.setTextColor(textColor);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
193
app/src/main/java/org/schabi/ocbookmarks/LoginAcitivty.java
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.nextcloud.android.sso.AccountImporter;
|
||||
import com.nextcloud.android.sso.BuildConfig;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
|
||||
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
|
||||
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
||||
import com.nextcloud.android.sso.ui.UiExceptionManager;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.OCBookmarksRestConnector;
|
||||
import org.schabi.ocbookmarks.REST.RequestException;
|
||||
import org.schabi.ocbookmarks.api.LoginData;
|
||||
import org.schabi.ocbookmarks.api.SSOUtil;
|
||||
|
||||
|
||||
public class LoginAcitivty extends AppCompatActivity {
|
||||
|
||||
// reply info
|
||||
private static final int OK = 0;
|
||||
private static final int CONNECTION_FAIL = 1;
|
||||
private static final int HOST_NOT_FOUND = 2;
|
||||
private static final int FILE_NOT_FOUND = 3;
|
||||
private static final int TIME_OUT = 4;
|
||||
private static final int SSO_FAILED = 5;
|
||||
private static final int BOOKMARK_NOT_INSTALLED = 6;
|
||||
|
||||
LoginData loginData = new LoginData();
|
||||
|
||||
Button ssoButton;
|
||||
TextView errorView;
|
||||
View errorContainer;
|
||||
|
||||
SharedPreferences sharedPrefs;
|
||||
String TAG = this.getClass().toString();
|
||||
Context mContext;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_login_acitivty);
|
||||
|
||||
mContext = this;
|
||||
ssoButton = findViewById(R.id.ssoButton);
|
||||
errorView = findViewById(R.id.loginErrorView);
|
||||
errorContainer = findViewById(R.id.errorContainer);
|
||||
|
||||
sharedPrefs = getSharedPreferences(getPackageName(), Context.MODE_PRIVATE);
|
||||
|
||||
ssoButton.setOnClickListener(v -> {
|
||||
try {
|
||||
AccountImporter.pickNewAccount(LoginAcitivty.this);
|
||||
} catch (NextcloudFilesAppNotInstalledException | AndroidGetAccountsPermissionNotGranted e) {
|
||||
UiExceptionManager.showDialogForException(LoginAcitivty.this, e);
|
||||
}
|
||||
});
|
||||
|
||||
checkIfSSOIsDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
checkIfSSOIsDone();
|
||||
}
|
||||
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
try {
|
||||
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> {
|
||||
|
||||
Log.d(TAG, "Login Attempt: "+account.name);
|
||||
SingleAccountHelper.setCurrentAccount(this.getApplicationContext(), account.name);
|
||||
loginData.ssologin = true;
|
||||
checkIfSSOIsDone();
|
||||
});
|
||||
} catch (AccountImportCancelledException e) {
|
||||
Log.i("log", "Account import has been canceled.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void checkIfSSOIsDone() {
|
||||
try {
|
||||
SingleAccountHelper.getCurrentSingleSignOnAccount(this.getApplicationContext());
|
||||
// If we pass here, we do have an account set and can continue.
|
||||
|
||||
TestLoginTask testLoginTask = new TestLoginTask();
|
||||
testLoginTask.execute(loginData);
|
||||
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
|
||||
Log.i(TAG, "No Account available. Please log in.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
private void storeLogin(LoginData loginData) {
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
editor.putBoolean(getString(R.string.ssologin), loginData.ssologin);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private class TestLoginTask extends AsyncTask<LoginData, Void, Integer> {
|
||||
protected Integer doInBackground(LoginData... loginDatas) {
|
||||
NextcloudAPI nextcloudAPI = null;
|
||||
if (loginData.ssologin) {
|
||||
try {
|
||||
nextcloudAPI = SSOUtil.getNextcloudAPI(LoginAcitivty.this, SingleAccountHelper.getCurrentSingleSignOnAccount(LoginAcitivty.this));
|
||||
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
|
||||
e.printStackTrace();
|
||||
return SSO_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(nextcloudAPI);
|
||||
try {
|
||||
connector.testAPI();
|
||||
return OK;
|
||||
} catch (RequestException re) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
re.printStackTrace();
|
||||
}
|
||||
if(re.getError() == RequestException.ERROR.BOOKMARK_NOT_INSTALLED) {
|
||||
return BOOKMARK_NOT_INSTALLED;
|
||||
}
|
||||
if (re.getMessage().contains("FileNotFound")) {
|
||||
return FILE_NOT_FOUND;
|
||||
}
|
||||
if (re.getMessage().contains("UnknownHost")) {
|
||||
return HOST_NOT_FOUND;
|
||||
}
|
||||
if (re.getMessage().contains("SocketTimeout")) {
|
||||
return TIME_OUT;
|
||||
}
|
||||
return CONNECTION_FAIL;
|
||||
} catch (Exception e) {
|
||||
return CONNECTION_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPostExecute(Integer result) {
|
||||
if (result == OK) {
|
||||
storeLogin(loginData);
|
||||
Intent intent = new Intent(mContext, MainActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
errorContainer.setVisibility(View.VISIBLE);
|
||||
SSOUtil.invalidateAPICache();
|
||||
SingleAccountHelper.setCurrentAccount(LoginAcitivty.this, null);
|
||||
switch (result) {
|
||||
case CONNECTION_FAIL:
|
||||
errorView.setText(getString(R.string.connection_failed_login));
|
||||
break;
|
||||
case HOST_NOT_FOUND:
|
||||
errorView.setText(getString(R.string.login_host_not_found));
|
||||
break;
|
||||
case FILE_NOT_FOUND:
|
||||
errorView.setText(getString(R.string.login_failed));
|
||||
break;
|
||||
case TIME_OUT:
|
||||
errorView.setText(getString(R.string.login_timeout));
|
||||
break;
|
||||
case SSO_FAILED:
|
||||
errorView.setText(getString(R.string.sso_failed));
|
||||
break;
|
||||
case BOOKMARK_NOT_INSTALLED:
|
||||
errorView.setText(R.string.login_error_no_bookmarks_api);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
513
app/src/main/java/org/schabi/ocbookmarks/MainActivity.java
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
|
||||
import com.nextcloud.android.sso.AccountImporter;
|
||||
import com.nextcloud.android.sso.BuildConfig;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
|
||||
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
|
||||
import com.nextcloud.android.sso.exceptions.SSOException;
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
import com.nextcloud.android.sso.ui.UiExceptionManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.REST.OCBookmarksRestConnector;
|
||||
import org.schabi.ocbookmarks.REST.model.Folder;
|
||||
import org.schabi.ocbookmarks.api.LoginData;
|
||||
import org.schabi.ocbookmarks.api.SSOUtil;
|
||||
import org.schabi.ocbookmarks.listener.BookmarkListener;
|
||||
import org.schabi.ocbookmarks.listener.OnRequestReloadListener;
|
||||
import org.schabi.ocbookmarks.ui.IconHandler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String DATA_FILE_NAME = "data.json";
|
||||
private static final String DATA_BACKUP_FILE_NAME = "data-backup.json";
|
||||
private static final int TAGLIST_MIN_ID = 10;
|
||||
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
private NextcloudAPI mNextcloudAPI = null;
|
||||
|
||||
private static final String BOOKMARK_FRAGMENT = "bookmark_fragment";
|
||||
private BookmarkFragment mBookmarkFragment = null;
|
||||
private ProgressBar mainProgressBar;
|
||||
|
||||
private NavigationView navigationview;
|
||||
private DrawerLayout drawerLayout;
|
||||
|
||||
private static final String TAG = MainActivity.class.toString();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
//Get Navigationview and do the action
|
||||
drawerLayout = findViewById(R.id.drawer_layout);
|
||||
navigationview = findViewById(R.id.nvView);
|
||||
navigationview.setNavigationItemSelectedListener(item -> {
|
||||
int id = item.getItemId();
|
||||
|
||||
if(id >= TAGLIST_MIN_ID) {
|
||||
String tag = item.getTitle().toString();
|
||||
mBookmarkFragment.showByTag(tag);
|
||||
} else {
|
||||
mBookmarkFragment.releaseTag();
|
||||
}
|
||||
drawerLayout.closeDrawer(this.navigationview);
|
||||
return true;
|
||||
});
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(true);
|
||||
|
||||
ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.nav_open, R.string.nav_close);
|
||||
drawerLayout.addDrawerListener(drawerToggle);
|
||||
drawerToggle.syncState();
|
||||
|
||||
|
||||
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(view -> {
|
||||
EditBookmarkDialog bookmarkDialog = new EditBookmarkDialog();
|
||||
AlertDialog dialog = bookmarkDialog.getDialog(MainActivity.this, null, new BookmarkListener() {
|
||||
@Override
|
||||
public void bookmarkChanged(Bookmark bookmark) {
|
||||
bookmark.setFolders(Arrays.asList(mBookmarkFragment.getCurrentFolder().getId()));
|
||||
addEditBookmark(bookmark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBookmark(Bookmark bookmark) {}
|
||||
});
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
mainProgressBar = findViewById(R.id.mainProgressBar);
|
||||
|
||||
|
||||
if(savedInstanceState == null) {
|
||||
mBookmarkFragment = new BookmarkFragment();
|
||||
setupBookmarkFragmentListener();
|
||||
}
|
||||
|
||||
prepareSSO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle inState) {
|
||||
super.onRestoreInstanceState(inState);
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
mBookmarkFragment = (BookmarkFragment) fm.getFragment(inState, BOOKMARK_FRAGMENT);
|
||||
setupBookmarkFragmentListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
fm.putFragment(outState, BOOKMARK_FRAGMENT, mBookmarkFragment);
|
||||
}
|
||||
|
||||
private void setupBookmarkFragmentListener() {
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
fm.beginTransaction()
|
||||
.add(R.id.container, mBookmarkFragment)
|
||||
.commit();
|
||||
|
||||
mBookmarkFragment.setOnRequestReloadListener(new OnRequestReloadListener() {
|
||||
@Override
|
||||
public void requestReload() {
|
||||
reloadData();
|
||||
}
|
||||
});
|
||||
|
||||
mBookmarkFragment.setBookmarkListener(new BookmarkListener() {
|
||||
@Override
|
||||
public void bookmarkChanged(Bookmark bookmark) {
|
||||
addEditBookmark(bookmark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBookmark(final Bookmark bookmark) {
|
||||
setRefreshing(true);
|
||||
new AsyncTask<Void, Void, String>() {
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(mNextcloudAPI);
|
||||
try {
|
||||
connector.deleteBookmark(bookmark);
|
||||
} catch (Exception e) {
|
||||
return getString(R.string.could_not_delete_bookmark);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if(result != null) {
|
||||
Toast.makeText(MainActivity.this, result, Toast.LENGTH_LONG);
|
||||
}
|
||||
reloadData();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addEditBookmark(final Bookmark bookmark) {
|
||||
setRefreshing(true);
|
||||
AsyncTask<Void, Void, String> updateTask = new AsyncTask<Void, Void, String>() {
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(mNextcloudAPI);
|
||||
// loginData.token,
|
||||
// loginData.ssologin);
|
||||
if(bookmark.getId() < 0) {
|
||||
// add new bookmark
|
||||
try {
|
||||
connector.addBookmark(bookmark);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
return getString(R.string.could_not_add_bookmark);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
connector.editBookmark(bookmark);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
return getString(R.string.could_not_change_bookmark);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
if(result != null) {
|
||||
Toast.makeText(MainActivity.this, result, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
reloadData();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
prepareSSO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
mBookmarkFragment.onBackHandled();
|
||||
}
|
||||
|
||||
|
||||
private void prepareSSO() {
|
||||
if(mNextcloudAPI != null) {
|
||||
Log.e(TAG, "API is already set up, we can continue...");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.e(TAG, "Prepare the API");
|
||||
SingleSignOnAccount ssoa = SingleAccountHelper.getCurrentSingleSignOnAccount(this.getApplicationContext());
|
||||
Log.e(TAG, "Found user: "+ssoa.name);
|
||||
mNextcloudAPI = SSOUtil.getNextcloudAPI(this, ssoa);
|
||||
Log.e(TAG, "Done!");
|
||||
|
||||
View headerView = navigationview.getHeaderView(0);
|
||||
TextView userTextView= (TextView)headerView.findViewById(R.id.userTextView);
|
||||
TextView urlTextView= (TextView)headerView.findViewById(R.id.urlTextView);
|
||||
urlTextView.setText(ssoa.url);
|
||||
userTextView.setText(ssoa.name);
|
||||
reloadData();
|
||||
|
||||
} catch (NextcloudFilesAppAccountNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
SSOUtil.invalidateAPICache();
|
||||
} catch (NoCurrentAccountSelectedException e) {
|
||||
Log.e(TAG, "Exception: No Account set up, log in again!");
|
||||
Log.e(TAG, e.toString());
|
||||
Intent intent = new Intent(this, LoginAcitivty.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem backupDataItem = menu.findItem(R.id.action_backup_data);
|
||||
if (backupDataItem != null) {
|
||||
backupDataItem.setVisible(getDataFileIfExists() != null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
switch (id) {
|
||||
case R.id.action_change_login:
|
||||
try {
|
||||
SSOUtil.invalidateAPICache();
|
||||
SingleAccountHelper.setCurrentAccount(this, null);
|
||||
SingleAccountHelper.reauthenticateCurrentAccount(this);
|
||||
} catch (SSOException e) {
|
||||
UiExceptionManager.showDialogForException(this, e);
|
||||
}
|
||||
Intent intent = new Intent(this, LoginAcitivty.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.action_reload_icons:
|
||||
IconHandler iconHandler = new IconHandler(MainActivity.this);
|
||||
iconHandler.deleteAll();
|
||||
reloadData();
|
||||
return true;
|
||||
case R.id.action_backup_data:
|
||||
new BackupDataTask(this).execute();
|
||||
return true;
|
||||
case android.R.id.home:
|
||||
if (drawerLayout.isDrawerOpen(this.navigationview)) {
|
||||
drawerLayout.closeDrawer(this.navigationview);
|
||||
} else {
|
||||
drawerLayout.openDrawer(this.navigationview);
|
||||
}
|
||||
|
||||
this.onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void reloadData() {
|
||||
RelodDataTask relodDataTask = new RelodDataTask();
|
||||
relodDataTask.execute();
|
||||
}
|
||||
|
||||
private void setRefreshing(boolean refresh) {
|
||||
mBookmarkFragment.setRefreshing(refresh);
|
||||
}
|
||||
|
||||
private class RelodDataTask extends AsyncTask<Void, Void, Bookmark[]> {
|
||||
Folder root = null;
|
||||
protected Bookmark[] doInBackground(Void... bla) {
|
||||
try {
|
||||
prepareSSO();
|
||||
OCBookmarksRestConnector connector =
|
||||
new OCBookmarksRestConnector(mNextcloudAPI);
|
||||
//new OCBookmarksRestConnector(loginData.url, loginData.user, loginData.password,loginData.token,loginData.ssologin);
|
||||
root = connector.getFolders();
|
||||
|
||||
JSONArray data = connector.getRawBookmarks();
|
||||
storeToFile(data);
|
||||
return connector.getFromRawJson(data);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPostExecute(Bookmark[] bookmarks) {
|
||||
if(bookmarks == null) {
|
||||
Toast.makeText(MainActivity.this, R.string.connectino_failed, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} else {
|
||||
mainProgressBar.setVisibility(View.GONE);
|
||||
mBookmarkFragment.updateData(root, bookmarks);
|
||||
|
||||
|
||||
Menu menu = navigationview.getMenu();
|
||||
menu.removeGroup(R.id.tag_group);
|
||||
SubMenu subMenu = menu.addSubMenu(R.id.tag_group, 1, Menu.NONE, R.string.nav_drawer_tags_header);
|
||||
|
||||
int i = TAGLIST_MIN_ID;
|
||||
for (String tag: Bookmark.getTagsFromBookmarks(bookmarks)) {
|
||||
MenuItem menuItem = subMenu.add(i, i++, Menu.NONE, tag);
|
||||
menuItem.setIcon(R.drawable.ic_tag);
|
||||
}
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class BackupDataTask extends AsyncTask<Void, Void, String> {
|
||||
private WeakReference<MainActivity> activityReference;
|
||||
|
||||
BackupDataTask(MainActivity mainActivity) {
|
||||
this.activityReference = new WeakReference<>(mainActivity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... voids) {
|
||||
final MainActivity mainActivity = activityReference.get();
|
||||
if (mainActivity == null || mainActivity.isFinishing()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final File dataFile = mainActivity.getDataFileIfExists();
|
||||
if (dataFile == null) {
|
||||
Log.e(this.getClass().getName(), DATA_FILE_NAME + " does not exist");
|
||||
return null;
|
||||
}
|
||||
|
||||
final File backupDir = mainActivity.getExternalFilesDir(null);
|
||||
if (backupDir == null) {
|
||||
Log.e(this.getClass().getName(), "External storage not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
final File backupFile = new File(backupDir, DATA_BACKUP_FILE_NAME);
|
||||
if (backupFile.exists() && !backupFile.delete()) {
|
||||
Log.e(this.getClass().getName(), "Existing backup file could not be deleted");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
doCopy(dataFile, backupFile);
|
||||
return backupFile.getAbsolutePath();
|
||||
} catch (Exception e) {
|
||||
Log.e(this.getClass().getName(), "Error creating backup of " + dataFile, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String backupFilePath) {
|
||||
final MainActivity mainActivity = activityReference.get();
|
||||
if (mainActivity == null || mainActivity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
if (backupFilePath != null) {
|
||||
mainActivity.mainProgressBar.setVisibility(View.GONE);
|
||||
mainActivity.setRefreshing(false);
|
||||
Toast.makeText(
|
||||
mainActivity,
|
||||
mainActivity.getApplicationContext().getString(
|
||||
R.string.backup_successful,
|
||||
backupFilePath),
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
mainActivity,
|
||||
R.string.backup_failed,
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void doCopy(final File dataFile, final File backupFile) throws Exception {
|
||||
try (final InputStream fis = new FileInputStream(dataFile);
|
||||
final OutputStream fos = new FileOutputStream(backupFile)) {
|
||||
final byte[] buffer = new byte[1024];
|
||||
|
||||
int length;
|
||||
while ((length = fis.read(buffer)) > 0) {
|
||||
fos.write(buffer, 0, length);
|
||||
}
|
||||
fos.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getDataFileIfExists() {
|
||||
final File dataFile = new File(getFilesDir() + File.pathSeparator + DATA_FILE_NAME);
|
||||
return dataFile.exists() ? dataFile : null;
|
||||
}
|
||||
|
||||
private void loadFromFile() {
|
||||
File jsonFile = getDataFileIfExists();
|
||||
if (jsonFile != null) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
mainProgressBar.setVisibility(View.GONE);
|
||||
try {
|
||||
BufferedReader br = new BufferedReader(new FileReader(jsonFile));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
text.append(line);
|
||||
text.append("\n");
|
||||
}
|
||||
br.close();
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(mNextcloudAPI);
|
||||
Bookmark[] bookmarks = connector.getFromRawJson(new JSONArray(text.toString()));
|
||||
mBookmarkFragment.updateData(connector.getFolders(), bookmarks);
|
||||
} catch (JSONException je) {
|
||||
if (BuildConfig.DEBUG) je.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
if (BuildConfig.DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void storeToFile(JSONArray data) {
|
||||
try {
|
||||
FileOutputStream jsonFile =
|
||||
new FileOutputStream(getFilesDir() + File.pathSeparator + DATA_FILE_NAME);
|
||||
jsonFile.write(data.toString().getBytes());
|
||||
jsonFile.flush();
|
||||
jsonFile.close();
|
||||
} catch (Exception e) {
|
||||
if (BuildConfig.DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package org.schabi.ocbookmarks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.nextcloud.android.sso.BuildConfig;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.REST.OCBookmarksRestConnector;
|
||||
import org.schabi.ocbookmarks.api.SSOUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class QuickaddBookmarkActivity extends AppCompatActivity {
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.add_bookmark_activity);
|
||||
setTitle("");
|
||||
|
||||
|
||||
TextView stateView = findViewById(R.id.adding_text_view);
|
||||
ProgressBar progressView = findViewById(R.id.progressView);
|
||||
ImageView successView = findViewById(R.id.successView);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String title = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
String url = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
Bookmark bookmark = Bookmark.emptyInstance();
|
||||
bookmark.setTitle(title);
|
||||
bookmark.setUrl(url);
|
||||
// bookmark.setTags(new String[] {this.getString(R.string.share_target_quick)});
|
||||
ArrayList<String> list;
|
||||
list = new ArrayList<String>();
|
||||
list.add(String.valueOf(R.string.share_target_quick));
|
||||
bookmark.setTags(list);
|
||||
|
||||
|
||||
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
executor.execute(() -> {
|
||||
|
||||
String result = getString(R.string.bookmark_saved);
|
||||
NextcloudAPI nextcloudAPI = null;
|
||||
try {
|
||||
SingleSignOnAccount ssoa = SingleAccountHelper.getCurrentSingleSignOnAccount(QuickaddBookmarkActivity.this.getApplicationContext());
|
||||
nextcloudAPI = SSOUtil.getNextcloudAPI(QuickaddBookmarkActivity.this, ssoa);
|
||||
} catch (NextcloudFilesAppAccountNotFoundException e) {
|
||||
Toast.makeText(QuickaddBookmarkActivity.this,
|
||||
R.string.nextcloud_files_app_account_not_found_message,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
} catch ( NoCurrentAccountSelectedException e) {
|
||||
Toast.makeText(QuickaddBookmarkActivity.this,
|
||||
R.string.no_current_account_selected_exception_message,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
OCBookmarksRestConnector connector = new OCBookmarksRestConnector(nextcloudAPI);
|
||||
try {
|
||||
connector.addBookmark(bookmark);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
result = getString(R.string.could_not_add_bookmark);
|
||||
}
|
||||
String finalResult = result;
|
||||
handler.post(() -> {
|
||||
progressView.setVisibility(View.GONE);
|
||||
successView.setVisibility(View.VISIBLE);
|
||||
stateView.setText(finalResult);
|
||||
|
||||
Toast.makeText(QuickaddBookmarkActivity.this,
|
||||
finalResult,
|
||||
Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
package org.schabi.ocbookmarks.REST;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nextcloud.android.sso.QueryParam;
|
||||
import com.nextcloud.android.sso.aidl.NextcloudRequest;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.api.Response;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.REST.model.Folder;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 14.05.17.
|
||||
* Modified by @dasbiswajit on 14.04.2019
|
||||
*/
|
||||
|
||||
|
||||
public class OCBookmarksRestConnector {
|
||||
private String apiRootUrl = "";
|
||||
private final NextcloudAPI nextcloudAPI;
|
||||
String TAG = this.getClass().toString();
|
||||
|
||||
|
||||
/**
|
||||
* @param nextcloudAPI will be used if not null instead of traditional user / password authentication
|
||||
*/
|
||||
public OCBookmarksRestConnector(NextcloudAPI nextcloudAPI) {
|
||||
this.nextcloudAPI = nextcloudAPI;
|
||||
// host is defined by SingleSignOnAccount
|
||||
|
||||
apiRootUrl = "/index.php/apps/bookmarks/public/rest/v2";
|
||||
Log.e(TAG,"API Root-Url: "+apiRootUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending SSO fancy way
|
||||
*/
|
||||
public JSONObject sendWithSSO(@NonNull String methode, @NonNull String relativeUrl, @NonNull Collection<QueryParam> parameter) throws RequestException {
|
||||
if (this.nextcloudAPI == null) {
|
||||
Log.e(TAG,"API not set up.");
|
||||
throw new RequestException("Trying to send request via SSO, but API is null.", RequestException.ERROR.API_NOT_SET_UP);
|
||||
}
|
||||
|
||||
Log.i(TAG,"API is already set up");
|
||||
NextcloudRequest request = new NextcloudRequest
|
||||
.Builder()
|
||||
.setMethod(methode)
|
||||
.setUrl(apiRootUrl + relativeUrl)
|
||||
.setParameter(parameter)
|
||||
.build();
|
||||
try {
|
||||
Response response = nextcloudAPI.performNetworkRequestV2(request);
|
||||
|
||||
Log.e(TAG, response.getPlainHeaders().toString());
|
||||
|
||||
final StringBuilder result = new StringBuilder();
|
||||
final BufferedReader rd = new BufferedReader(new InputStreamReader(response.getBody()));
|
||||
String line;
|
||||
while ((line = rd.readLine()) != null) {
|
||||
result.append(line);
|
||||
}
|
||||
response.getBody().close();
|
||||
|
||||
return parseJson(methode, apiRootUrl + relativeUrl, result.toString());
|
||||
} catch (Exception e) {
|
||||
if(e.getMessage().contains("status-code: 302")) {
|
||||
throw new RequestException(RequestException.ERROR.BOOKMARK_NOT_INSTALLED);
|
||||
}
|
||||
e.printStackTrace();
|
||||
throw new RequestException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject parseJson(String methode, String url, String response) throws RequestException {
|
||||
|
||||
JSONObject data = null;
|
||||
if("GET".equals(methode) && url.endsWith("/folder")) {
|
||||
JSONObject array = null;
|
||||
try {
|
||||
data = new JSONObject(response);
|
||||
// data = new JSONObject();
|
||||
// data.put("data", array);
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Parsing error, maybe owncloud does not support bookmark api", je);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
if ("GET".equals(methode) && url.endsWith("/tag")) {
|
||||
// we have to handle GET /tag different:
|
||||
// https://github.com/nextcloud/bookmarks#list-all-tags
|
||||
JSONArray array = null;
|
||||
try {
|
||||
array = new JSONArray(response);
|
||||
data = new JSONObject();
|
||||
data.put("data", array);
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Parsing error, maybe owncloud does not support bookmark api", je);
|
||||
}
|
||||
return data;
|
||||
} else if ("PUT".equals(methode)) {
|
||||
try {
|
||||
data = new JSONObject(response);
|
||||
return data.getJSONObject("item");
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Parsing error, maybe owncloud does not support bookmark api", je);
|
||||
}
|
||||
} else {
|
||||
|
||||
try {
|
||||
data = new JSONObject(response);
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Parsing error, maybe owncloud does not support bookmark api", je);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!data.getString("status").equals("success")) {
|
||||
throw new RequestException("Error bad request: " + url);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new RequestException("Error bad request: " + url, e);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// +++++++++++++++++
|
||||
// + bookmarks +
|
||||
// +++++++++++++++++
|
||||
|
||||
|
||||
/**
|
||||
* This is a function to get the first bookmarks.
|
||||
* This assures us that the bookmark app is avaliable on the server
|
||||
* while not gathering every bookmark. Only one query is made.
|
||||
* @return
|
||||
* @throws RequestException
|
||||
*/
|
||||
public JSONArray testAPI() throws RequestException {
|
||||
try {
|
||||
JSONArray bookmarks = new JSONArray();
|
||||
|
||||
Collection<QueryParam> parameter = new ArrayList<>();
|
||||
parameter.add(new QueryParam("page", "1"));
|
||||
parameter.add(new QueryParam("limit", "10"));
|
||||
JSONObject now = sendWithSSO("GET", "/bookmark", parameter);
|
||||
JSONArray data = now.getJSONArray("data");
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
JSONObject bm = (JSONObject) data.get(i);
|
||||
bookmarks.put(bm);
|
||||
}
|
||||
return bookmarks;
|
||||
} catch (JSONException e) {
|
||||
throw new RequestException("Could not parse data", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public JSONArray getRawBookmarks() throws RequestException {
|
||||
try {
|
||||
JSONArray bookmarks = new JSONArray();
|
||||
int pageSize = 300;
|
||||
int resultLength = pageSize;
|
||||
int page = 0;
|
||||
|
||||
while (resultLength == pageSize) {
|
||||
Collection<QueryParam> parameter = new ArrayList<>();
|
||||
parameter.add(new QueryParam("page", String.valueOf(page++)));
|
||||
parameter.add(new QueryParam("limit", "300"));
|
||||
JSONObject now = sendWithSSO("GET", "/bookmark", parameter);
|
||||
JSONArray data = now.getJSONArray("data");
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
JSONObject bm = (JSONObject) data.get(i);
|
||||
bookmarks.put(bm);
|
||||
}
|
||||
resultLength = bookmarks.length();
|
||||
}
|
||||
return bookmarks;
|
||||
} catch (JSONException e) {
|
||||
throw new RequestException("Could not parse data", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Bookmark[] getFromRawJson(JSONArray data) throws RequestException {
|
||||
try {
|
||||
Bookmark[] bookmarks = new Bookmark[data.length()];
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
JSONObject bookmark = data.getJSONObject(i);
|
||||
bookmarks[i] = getBookmarkFromJsonO(bookmark);
|
||||
}
|
||||
return bookmarks;
|
||||
} catch (JSONException e) {
|
||||
throw new RequestException("Could not parse data", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Bookmark[] getBookmarks() throws RequestException {
|
||||
JSONArray data = getRawBookmarks();
|
||||
return getFromRawJson(data);
|
||||
}
|
||||
|
||||
|
||||
private Bookmark getBookmarkFromJsonO(JSONObject jBookmark) throws RequestException {
|
||||
|
||||
ArrayList<String> tags;
|
||||
try {
|
||||
JSONArray jTags = jBookmark.getJSONArray("tags");
|
||||
tags = new ArrayList<>();
|
||||
for (int j = 0; j < jTags.length(); j++) {
|
||||
tags.add(jTags.getString(j));
|
||||
}
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not parse array", je);
|
||||
}
|
||||
|
||||
List<Integer> folders;
|
||||
try {
|
||||
JSONArray jfolders = jBookmark.getJSONArray("folders");
|
||||
folders = new ArrayList<>(jfolders.length());
|
||||
for (int j = 0; j < jfolders.length(); j++) {
|
||||
folders.add(jfolders.getInt(j));
|
||||
}
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not parse folder array", je);
|
||||
}
|
||||
|
||||
|
||||
// Todo: another api error we need to fix
|
||||
if (tags.size() == 1 && tags.get(0).isEmpty()) {
|
||||
tags = new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
return Bookmark.emptyInstance()
|
||||
.setId(jBookmark.getInt("id"))
|
||||
.setUrl(jBookmark.getString("url"))
|
||||
.setTitle(jBookmark.getString("title"))
|
||||
.setUserId(jBookmark.getString("userId"))
|
||||
.setDescription(jBookmark.getString("description"))
|
||||
//.setPublic(false) //dummy to false for version 2 to 3 upgrade.
|
||||
// .setAdded(new Date(jBookmark.getLong("added") * 1000))
|
||||
.setLastModified(new Date(jBookmark.getLong("lastmodified") * 1000))
|
||||
.setClickcount(jBookmark.getInt("clickcount"))
|
||||
.setTags(tags)
|
||||
.setFolders(folders);
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not gather all data", je);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private String createBookmarkParameterString(Bookmark bookmark) {
|
||||
if (!bookmark.getTitle().isEmpty() && !bookmark.getUrl().startsWith("http")) {
|
||||
//tittle can only be set if the sheme is given
|
||||
//this is a bug we need to fix
|
||||
bookmark.setUrl("http://" + bookmark.getUrl());
|
||||
}
|
||||
|
||||
String url = "?url=" + URLEncoder.encode(bookmark.getUrl());
|
||||
|
||||
if (!bookmark.getTitle().isEmpty()) {
|
||||
url += "&title=" + URLEncoder.encode(bookmark.getTitle());
|
||||
}
|
||||
if (!bookmark.getDescription().isEmpty()) {
|
||||
url += "&description=" + URLEncoder.encode(bookmark.getDescription());
|
||||
}
|
||||
// if(bookmark.isPublic()) {
|
||||
// url += "&is_public=1";
|
||||
// }
|
||||
|
||||
for (String tag : bookmark.getTags()) {
|
||||
url += "&" + URLEncoder.encode("tags[]") + "=" + URLEncoder.encode(tag);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private Collection<QueryParam> createBookmarkParameter(Bookmark bookmark) {
|
||||
final Collection<QueryParam> parameter = new ArrayList<>(3 + bookmark.getTags().size());
|
||||
if (!bookmark.getTitle().isEmpty() && !bookmark.getUrl().startsWith("http")) {
|
||||
// Title can only be set if the sheme is given
|
||||
// This is a bug we need to fix
|
||||
bookmark.setUrl("http://" + bookmark.getUrl());
|
||||
}
|
||||
|
||||
parameter.add(new QueryParam("url", bookmark.getUrl()));
|
||||
parameter.add(new QueryParam("title", bookmark.getTitle()));
|
||||
parameter.add(new QueryParam("description", bookmark.getDescription()));
|
||||
|
||||
for (String tag : bookmark.getTags()) {
|
||||
parameter.add(new QueryParam("tags[]", tag));
|
||||
}
|
||||
|
||||
for (Integer folder : bookmark.getFolders()) {
|
||||
parameter.add(new QueryParam("folders[]", folder.toString()));
|
||||
}
|
||||
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
public Bookmark addBookmark(Bookmark bookmark) throws RequestException {
|
||||
try {
|
||||
if (bookmark.getId() == -1) {
|
||||
JSONObject reply;
|
||||
reply = sendWithSSO("POST", "/bookmark", createBookmarkParameter(bookmark));
|
||||
|
||||
Log.e(TAG, "Bookmark Creation Reply: "+reply);
|
||||
return getBookmarkFromJsonO(reply.getJSONObject("item"));
|
||||
} else {
|
||||
throw new RequestException("Bookmark id is set. Maybe this bookmark already exist: id=" + bookmark.getId());
|
||||
}
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not parse reply", je);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteBookmark(Bookmark bookmark) throws RequestException {
|
||||
if (bookmark.getId() < 0) {
|
||||
return;
|
||||
}
|
||||
sendWithSSO("DELETE", "/bookmark/" + bookmark.getId(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Bookmark editBookmark(Bookmark bookmark) throws RequestException {
|
||||
return editBookmark(bookmark, bookmark.getId());
|
||||
}
|
||||
|
||||
public Bookmark editBookmark(Bookmark bookmark, int newRecordId) throws RequestException {
|
||||
if (bookmark.getId() < 0) {
|
||||
throw new RequestException("Bookmark has no valid id. Maybe you want to add a bookmark? id="
|
||||
+ bookmark.getId());
|
||||
}
|
||||
if (bookmark.getUrl().isEmpty()) {
|
||||
throw new RequestException("Bookmark has no url. Maybe you want to add a bookmark?");
|
||||
}
|
||||
|
||||
Collection<QueryParam> parameter = createBookmarkParameter(bookmark);
|
||||
parameter.add(new QueryParam("record_id", Integer.toString(newRecordId)));
|
||||
return getBookmarkFromJsonO(sendWithSSO("PUT", "/bookmark/" + bookmark.getId(), parameter));
|
||||
}
|
||||
|
||||
// ++++++++++++++++++
|
||||
// + folders +
|
||||
// ++++++++++++++++++
|
||||
|
||||
public Folder getFolders() throws RequestException {
|
||||
try {
|
||||
JSONArray data = sendWithSSO("GET", "/folder", Collections.emptyList()).getJSONArray("data");
|
||||
Folder root = Folder.createEmptyRootFolder();
|
||||
fillChildren(root, data);
|
||||
return root;
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not get all folders", je);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillChildren(Folder rootFolder, JSONArray children) {
|
||||
if (children == null || children.length() < 1) {
|
||||
return;
|
||||
}
|
||||
List<Folder> childFolderList = new ArrayList<>();
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
try {
|
||||
JSONObject folderJson = children.getJSONObject(i);
|
||||
Folder folder = new Folder();
|
||||
folder.setId(folderJson.getInt("id"));
|
||||
folder.setParentFolderId(folderJson.getInt("parent_folder"));
|
||||
folder.setTitle(folderJson.getString("title"));
|
||||
if (folderJson.has("children")) {
|
||||
fillChildren(folder, folderJson.getJSONArray("children"));
|
||||
}
|
||||
childFolderList.add(folder);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
rootFolder.setChildren(childFolderList);
|
||||
}
|
||||
|
||||
|
||||
// ++++++++++++++++++
|
||||
// + tags +
|
||||
// ++++++++++++++++++
|
||||
|
||||
public String[] getTags() throws RequestException {
|
||||
try {
|
||||
JSONArray data;
|
||||
data = sendWithSSO("GET", "/tag", Collections.emptyList()).getJSONArray("data");
|
||||
|
||||
String[] tags = new String[data.length()];
|
||||
for (int i = 0; i < tags.length; i++) {
|
||||
tags[i] = data.getString(i);
|
||||
}
|
||||
|
||||
return tags;
|
||||
} catch (JSONException je) {
|
||||
throw new RequestException("Could not get all tags", je);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteTag(String tag) throws RequestException {
|
||||
sendWithSSO("DELETE", "/tag", Collections.singletonList(new QueryParam("old_name", tag)));
|
||||
}
|
||||
|
||||
public void renameTag(String oldName, String newName) throws RequestException {
|
||||
final Collection<QueryParam> parameter = new ArrayList<>(2);
|
||||
parameter.add(new QueryParam("old_name", oldName));
|
||||
parameter.add(new QueryParam("new_name", newName));
|
||||
sendWithSSO("POST", "/tag", parameter);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.schabi.ocbookmarks.REST;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 14.05.17.
|
||||
*/
|
||||
public class PermissionException extends RequestException {
|
||||
PermissionException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package org.schabi.ocbookmarks.REST;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* Todo: Phase out the String message and try to use enum.
|
||||
* Reason: I think it makes handling errors more easy because we dont rely on string parsing.
|
||||
* Created by the-scrabi on 14.05.17.
|
||||
*/
|
||||
public class RequestException extends IOException {
|
||||
|
||||
|
||||
public enum ERROR {
|
||||
UNKNOWN,
|
||||
API_NOT_SET_UP,
|
||||
FILE_NOT_FOUND,
|
||||
HOST_NOT_FOUND,
|
||||
TIME_OUT,
|
||||
BOOKMARK_NOT_INSTALLED
|
||||
}
|
||||
|
||||
private ERROR mError = ERROR.UNKNOWN;
|
||||
|
||||
|
||||
@Deprecated
|
||||
RequestException(String message, Exception e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
RequestException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
RequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
RequestException(String message, ERROR error) {
|
||||
super(message);
|
||||
mError = error;
|
||||
}
|
||||
|
||||
RequestException(ERROR error) {
|
||||
super(error.name());
|
||||
mError = error;
|
||||
}
|
||||
|
||||
public ERROR getError() {
|
||||
return mError;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
package org.schabi.ocbookmarks.REST.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 14.05.17.
|
||||
* modified by bisasda
|
||||
*/
|
||||
public class Bookmark {
|
||||
private int id = -1;
|
||||
private String url = "";
|
||||
private String title = "";
|
||||
private String userId = "";
|
||||
private String description = "";
|
||||
// private Date added = null;
|
||||
private Date lastModified = null;
|
||||
private int clickcount = -1;
|
||||
// private boolean isPublic = false;
|
||||
private ArrayList<String> tags = new ArrayList<>();
|
||||
private List<Integer> folders = new ArrayList<>();
|
||||
|
||||
private boolean isFolder = false;
|
||||
|
||||
public static Bookmark emptyInstance() {
|
||||
return new Bookmark();
|
||||
}
|
||||
|
||||
private Bookmark() {
|
||||
|
||||
}
|
||||
|
||||
// ++++++++++++++++++++
|
||||
// + factory setter +
|
||||
// ++++++++++++++++++++
|
||||
|
||||
public Bookmark setId(int id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setTitle(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
// public Bookmark setAdded(Date added) {
|
||||
// this.added = added;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
public Bookmark setLastModified(Date lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setClickcount(int clickcount) {
|
||||
this.clickcount = clickcount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setTags(ArrayList<String> tags) {
|
||||
this.tags = tags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Bookmark setFolders(List<Integer> folders) {
|
||||
this.folders = folders;
|
||||
return this;
|
||||
}
|
||||
// public Bookmark setPublic(boolean aPublic) {
|
||||
// isPublic = aPublic;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
// +++++++++++++++++++++++++
|
||||
// + getter functions +
|
||||
// +++++++++++++++++++++++++
|
||||
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
// public Date getAdded() {
|
||||
// return added;
|
||||
// }
|
||||
public Date getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
public int getClickcount() {
|
||||
return clickcount;
|
||||
}
|
||||
public ArrayList<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
public List<Integer> getFolders(){
|
||||
return folders;
|
||||
}
|
||||
// public boolean isPublic() {
|
||||
// return isPublic;
|
||||
// }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String tagsString = "[";
|
||||
for(String tag : tags) {
|
||||
tagsString += tag + ",";
|
||||
}
|
||||
tagsString += "]";
|
||||
|
||||
String foldersString = "[";
|
||||
for(int folder : folders) {
|
||||
foldersString += folder + ",";
|
||||
}
|
||||
foldersString += "]";
|
||||
|
||||
return "id:" + Integer.toString(id) + "\n" +
|
||||
"url:" + url + "\n" +
|
||||
"title:" + title + "\n" +
|
||||
"userId:" + userId + "\n" +
|
||||
"description:" + description + "\n" +
|
||||
// "added:" + added.toString() + "\n" +
|
||||
"lastModified:" + lastModified.toString() + "\n" +
|
||||
"clickount:" + clickcount + "\n" +
|
||||
"tags:" + tagsString+ "\n" +
|
||||
"folders:"+foldersString;
|
||||
// "isPublic:" + Boolean.toString(isPublic);
|
||||
}
|
||||
|
||||
|
||||
public static String[] getTagsFromBookmarks(Bookmark[] bookmarks) {
|
||||
Vector<String> tagList = new Vector<>();
|
||||
for(Bookmark b : bookmarks) {
|
||||
for(String tag : b.getTags()) {
|
||||
if(!tagList.contains(tag)) {
|
||||
tagList.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] returnTagList = new String[tagList.size()];
|
||||
for(int i = 0; i < returnTagList.length; i++) {
|
||||
returnTagList[i] = tagList.get(i);
|
||||
}
|
||||
return returnTagList;
|
||||
}
|
||||
|
||||
public static int[] getFoldersFromBookmarks(Bookmark[] bookmarks) {
|
||||
Vector<Integer> folderList = new Vector<>();
|
||||
for(Bookmark b : bookmarks) {
|
||||
for(int folder : b.getFolders()) {
|
||||
if(!folderList.contains(folder)) {
|
||||
folderList.add(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[] returnFolderList = new int[folderList.size()];
|
||||
for(int i = 0; i < returnFolderList.length; i++) {
|
||||
returnFolderList[i] = folderList.get(i);
|
||||
}
|
||||
return returnFolderList;
|
||||
}
|
||||
|
||||
public boolean isFolder() {
|
||||
return isFolder;
|
||||
}
|
||||
|
||||
public void setFolder(boolean folder) {
|
||||
isFolder = folder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.schabi.ocbookmarks.REST.model;
|
||||
|
||||
public class BookmarkListElement {
|
||||
|
||||
|
||||
private Folder mFolder;
|
||||
private Bookmark mBookmark;
|
||||
|
||||
private boolean isFolder = false;
|
||||
|
||||
|
||||
public BookmarkListElement(Folder folder) {
|
||||
isFolder = true;
|
||||
mFolder = folder;
|
||||
}
|
||||
|
||||
public BookmarkListElement(Bookmark bookmark) {
|
||||
isFolder = false;
|
||||
mBookmark = bookmark;
|
||||
}
|
||||
|
||||
|
||||
public boolean isFolder() {
|
||||
return isFolder;
|
||||
}
|
||||
|
||||
public Folder getFolder() {
|
||||
return mFolder;
|
||||
}
|
||||
|
||||
public Bookmark getBookmark() {
|
||||
return mBookmark;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package org.schabi.ocbookmarks.REST.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Folder implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// https://nextcloud-bookmarks.readthedocs.io/en/latest/folder.html#folder-model
|
||||
// This is how it looks like in JSON:
|
||||
/*
|
||||
{
|
||||
"status": "success", "data": [
|
||||
{"id": 1, "title": "work", "parent_folder": -1},
|
||||
{"id": 2, "title": "personal", "parent_folder": -1, "children": [
|
||||
{"id": 3, "title": "garden", "parent_folder": 2},
|
||||
{"id": 4, "title": "music", "parent_folder": 2}
|
||||
]},
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public static final int ROOT_ID = -1;
|
||||
public static final int UP_ID = -2;
|
||||
|
||||
|
||||
private int id;
|
||||
private String title;
|
||||
private int parentFolderId;
|
||||
private List<Folder> children = new ArrayList<>();
|
||||
|
||||
|
||||
public static Folder createEmptyRootFolder() {
|
||||
Folder root = new Folder();
|
||||
root.setId(ROOT_ID);
|
||||
root.setTitle("All Bookmarks");
|
||||
root.setParentFolderId(ROOT_ID);
|
||||
return root;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public int getParentFolderId() {
|
||||
return parentFolderId;
|
||||
}
|
||||
|
||||
public void setParentFolderId(int parentFolderId) {
|
||||
this.parentFolderId = parentFolderId;
|
||||
}
|
||||
|
||||
public List<Folder> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<Folder> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Folder folder = (Folder) o;
|
||||
|
||||
if (id != folder.id) return false;
|
||||
if (parentFolderId != folder.parentFolderId) return false;
|
||||
if (title != null ? !title.equals(folder.title) : folder.title != null) return false;
|
||||
return children != null ? children.equals(folder.children) : folder.children == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = id;
|
||||
result = 31 * result + (title != null ? title.hashCode() : 0);
|
||||
result = 31 * result + parentFolderId;
|
||||
result = 31 * result + (children != null ? children.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.schabi.ocbookmarks.api;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 04.06.17.
|
||||
*/
|
||||
|
||||
public class LoginData {
|
||||
public boolean ssologin;
|
||||
}
|
||||
55
app/src/main/java/org/schabi/ocbookmarks/api/SSOUtil.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package org.schabi.ocbookmarks.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.nextcloud.android.sso.api.NextcloudAPI;
|
||||
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
/**
|
||||
* This class keeps {@link NextcloudAPI} instance
|
||||
*/
|
||||
@WorkerThread
|
||||
public class SSOUtil {
|
||||
|
||||
private static final String TAG = SSOUtil.class.getSimpleName();
|
||||
|
||||
private static NextcloudAPI mNextcloudAPI;
|
||||
|
||||
public static NextcloudAPI getNextcloudAPI(@NonNull Context appContext, @NonNull SingleSignOnAccount ssoAccount) {
|
||||
if (mNextcloudAPI != null) {
|
||||
return mNextcloudAPI;
|
||||
}
|
||||
Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
|
||||
final NextcloudAPI nextcloudAPI = new NextcloudAPI(appContext, ssoAccount, new GsonBuilder().create(), new NextcloudAPI.ApiConnectedListener() {
|
||||
@Override
|
||||
public void onConnected() {
|
||||
Log.i(TAG, "SSO API connected for " + ssoAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
});
|
||||
mNextcloudAPI = nextcloudAPI;
|
||||
return nextcloudAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cached {@link NextcloudAPI}
|
||||
* Should be called in case a {@link TokenMismatchException} occurs.
|
||||
*/
|
||||
public static void invalidateAPICache() {
|
||||
Log.v(TAG, "Invalidating API cache");
|
||||
if (mNextcloudAPI != null) {
|
||||
mNextcloudAPI.stop();
|
||||
}
|
||||
mNextcloudAPI = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.schabi.ocbookmarks.listener;
|
||||
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
|
||||
public interface BookmarkListener {
|
||||
void bookmarkChanged(Bookmark bookmark);
|
||||
void deleteBookmark(Bookmark bookmark);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.schabi.ocbookmarks.listener
|
||||
|
||||
import org.schabi.ocbookmarks.REST.model.Folder
|
||||
|
||||
interface FolderListener {
|
||||
fun changeFolderCallback(f: Folder)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.schabi.ocbookmarks.listener;
|
||||
|
||||
public interface OnRequestReloadListener {
|
||||
void requestReload();
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package org.schabi.ocbookmarks.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.ocbookmarks.EditBookmarkDialog;
|
||||
import org.schabi.ocbookmarks.R;
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
import org.schabi.ocbookmarks.REST.model.BookmarkListElement;
|
||||
import org.schabi.ocbookmarks.REST.model.Folder;
|
||||
import org.schabi.ocbookmarks.listener.BookmarkListener;
|
||||
import org.schabi.ocbookmarks.listener.FolderListener;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class BookmarksRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private final ArrayList<BookmarkListElement> mListElements;
|
||||
Context mContext;
|
||||
LayoutInflater mInflater;
|
||||
FolderListener mFolderCallback;
|
||||
BookmarkListener mBookmarkCallback;
|
||||
|
||||
private static final int FOLDER_TYPE = 0;
|
||||
private static final int BOOKMARK_TYPE = 1;
|
||||
|
||||
public BookmarksRecyclerViewAdapter(ArrayList<BookmarkListElement> listElements, Context context) {
|
||||
this.mListElements = listElements;
|
||||
this.mContext = context;
|
||||
this.mInflater = LayoutInflater.from(mContext);
|
||||
}
|
||||
|
||||
|
||||
public void updateBookmarklist(ArrayList<BookmarkListElement> listElements) {
|
||||
this.mListElements.clear();
|
||||
this.mListElements.addAll(listElements);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setBookmarkListener(BookmarkListener listener) {
|
||||
this.mBookmarkCallback = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case BOOKMARK_TYPE:
|
||||
return new BookmarkHolder(mInflater.inflate(R.layout.bookmark_list_item, parent, false));
|
||||
case FOLDER_TYPE:
|
||||
return new FolderViewHolder(mInflater.inflate(R.layout.bookmark_list_item_folder, parent, false));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
|
||||
if (holder instanceof BookmarkHolder) {
|
||||
BookmarkHolder bookmarkView = (BookmarkHolder) holder;
|
||||
bookmarkView.relatedBookmarkId = holder.getAdapterPosition();
|
||||
Bookmark b = mListElements.get(holder.getAdapterPosition()).getBookmark();
|
||||
bookmarkView.titleView.setText(b.getTitle());
|
||||
if (!b.getDescription().isEmpty()) {
|
||||
bookmarkView.urlDescriptionView.setText(b.getDescription());
|
||||
} else {
|
||||
bookmarkView.urlDescriptionView.setText(b.getUrl());
|
||||
}
|
||||
IconHandler ih = new IconHandler(mContext);
|
||||
ih.loadIcon(bookmarkView.iconView, b);
|
||||
|
||||
|
||||
} else if (holder instanceof FolderViewHolder) {
|
||||
FolderViewHolder folderView = (FolderViewHolder) holder;
|
||||
folderView.relatedBookmarkId = holder.getAdapterPosition();
|
||||
Folder f = mListElements.get(holder.getAdapterPosition()).getFolder();
|
||||
folderView.folderTitle.setText(f.getTitle());
|
||||
folderView.setUpFolder(f.getId() == Folder.UP_ID);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void setBookmarkFolderListener(FolderListener fl){
|
||||
mFolderCallback = fl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (mListElements.get(position).isFolder()) {
|
||||
return FOLDER_TYPE;
|
||||
} else {
|
||||
return BOOKMARK_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mListElements.size();
|
||||
}
|
||||
|
||||
public class BookmarkHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
final PopupMenu popup;
|
||||
final TextView titleView;
|
||||
final TextView urlDescriptionView;
|
||||
final ImageView iconView;
|
||||
int relatedBookmarkId;
|
||||
|
||||
public BookmarkHolder(View view) {
|
||||
super(view);
|
||||
view.setOnClickListener(this);
|
||||
view.setOnLongClickListener(this);
|
||||
titleView = (TextView) view.findViewById(R.id.bookmark_title);
|
||||
urlDescriptionView = (TextView) view.findViewById(R.id.bookmark_url_description);
|
||||
iconView = (ImageView) view.findViewById(R.id.site_icon);
|
||||
|
||||
popup = new PopupMenu(mContext, view);
|
||||
MenuInflater inflater = popup.getMenuInflater();
|
||||
inflater.inflate(R.menu.edit_bookmark_item_menu, popup.getMenu());
|
||||
|
||||
|
||||
// try setting force show icons via reflections
|
||||
Object menuHelper;
|
||||
Class[] argTypes;
|
||||
try {
|
||||
Field fMenuHelper = PopupMenu.class.getDeclaredField("mPopup");
|
||||
fMenuHelper.setAccessible(true);
|
||||
menuHelper = fMenuHelper.get(popup);
|
||||
argTypes = new Class[]{boolean.class};
|
||||
menuHelper.getClass().getDeclaredMethod("setForceShowIcon", argTypes).invoke(menuHelper, true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
int id = item.getItemId();
|
||||
Bookmark bookmark = mListElements.get(relatedBookmarkId).getBookmark();
|
||||
|
||||
switch (id) {
|
||||
case R.id.share:
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, bookmark.getTitle());
|
||||
intent.putExtra(Intent.EXTRA_TEXT, bookmark.getUrl());
|
||||
mContext.startActivity(intent);
|
||||
return true;
|
||||
case R.id.edit_menu:
|
||||
EditBookmarkDialog bookmarkDialog = new EditBookmarkDialog();
|
||||
bookmarkDialog.getDialog((Activity) mContext,
|
||||
bookmark,
|
||||
mBookmarkCallback
|
||||
).show();
|
||||
return true;
|
||||
case R.id.delete_menu:
|
||||
showDeleteDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void showDeleteDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(mContext)
|
||||
.setTitle(R.string.sure_to_delete_bookmark)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if(mBookmarkCallback != null) {
|
||||
mBookmarkCallback.deleteBookmark(mListElements.get(relatedBookmarkId).getBookmark());
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
Bookmark b = mListElements.get(relatedBookmarkId).getBookmark();
|
||||
intent.setData(Uri.parse(b.getUrl()));
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
popup.show();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class FolderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final TextView folderTitle;
|
||||
int relatedBookmarkId;
|
||||
|
||||
private final ImageView upImage;
|
||||
private final ImageView folderImage;
|
||||
|
||||
FolderViewHolder(View view) {
|
||||
super(view);
|
||||
folderTitle = view.findViewById(R.id.folder_title);
|
||||
upImage = view.findViewById(R.id.icon_back);
|
||||
folderImage = view.findViewById(R.id.icon);
|
||||
|
||||
((RelativeLayout) view.findViewById(R.id.layout)).setOnClickListener(view1 -> {
|
||||
Folder f = mListElements.get(relatedBookmarkId).getFolder();
|
||||
mFolderCallback.changeFolderCallback(f);
|
||||
});
|
||||
}
|
||||
|
||||
public void setUpFolder(boolean isUp) {
|
||||
if(isUp) {
|
||||
upImage.setVisibility(View.VISIBLE);
|
||||
folderImage.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
upImage.setVisibility(View.INVISIBLE);
|
||||
folderImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
180
app/src/main/java/org/schabi/ocbookmarks/ui/IconHandler.java
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package org.schabi.ocbookmarks.ui;
|
||||
|
||||
import static org.schabi.ocbookmarks.R.drawable.*;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.ocbookmarks.BuildConfig;
|
||||
import org.schabi.ocbookmarks.R;
|
||||
import org.schabi.ocbookmarks.REST.model.Bookmark;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 16.06.17.
|
||||
*/
|
||||
|
||||
public class IconHandler {
|
||||
|
||||
private Context context;
|
||||
public IconHandler(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
public void loadIcon(final ImageView imageView, final Bookmark bookmark) {
|
||||
|
||||
if(siteHasNoIcon(bookmark)) {
|
||||
Bitmap icon = BitmapFactory.decodeResource(context.getResources(),
|
||||
ic_globe);
|
||||
imageView.setImageBitmap(icon);
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap icon = loadIcon(bookmark);
|
||||
if(icon != null) {
|
||||
|
||||
imageView.setImageBitmap(icon);
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncTask<Void, Void, Bitmap> loadTask = new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
HttpURLConnection connection = null;
|
||||
BufferedReader in = null;
|
||||
try {
|
||||
|
||||
// get icon url
|
||||
URL url = new URL(bookmark.getUrl());
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
in = new BufferedReader(
|
||||
new InputStreamReader(connection.getInputStream()));
|
||||
|
||||
StringBuilder res = new StringBuilder();
|
||||
String inputLine;
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
res.append(inputLine);
|
||||
}
|
||||
in.close();
|
||||
|
||||
Document doc = Jsoup.parse(res.toString(), bookmark.getUrl());
|
||||
Element link = null;
|
||||
// try to get highres firs
|
||||
link = doc.select("link[rel*=\"apple-touch-icon\"]").first();
|
||||
if (link == null) {
|
||||
link = doc.select("link[rel*=\"icon\"]").first();
|
||||
}
|
||||
|
||||
if (link != null) {
|
||||
|
||||
// get icon
|
||||
String iconUrl = link.attr("abs:href");
|
||||
|
||||
// fix icon url for certain sites
|
||||
// ---------------------------------
|
||||
iconUrl = iconUrl.replace("google.com", "www.google.com");
|
||||
// ---------------------------------
|
||||
|
||||
URL iUrl = new URL(iconUrl);
|
||||
connection = (HttpURLConnection) iUrl.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
Bitmap icon = BitmapFactory.decodeStream(connection.getInputStream());
|
||||
return icon;
|
||||
} else {
|
||||
Log.d("IconHandler", "Nothing found for: " + bookmark.getUrl());
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap result) {
|
||||
if(result == null) {
|
||||
setSiteHasNoIcon(bookmark);
|
||||
} else {
|
||||
storeIcon(bookmark, result);
|
||||
}
|
||||
imageView.setImageBitmap(result);
|
||||
}
|
||||
}.execute();
|
||||
|
||||
}
|
||||
|
||||
private Bitmap loadIcon(Bookmark bookmark) {
|
||||
int id = bookmark.getId();
|
||||
File homeDir = context.getFilesDir();
|
||||
File iconFile = new File(homeDir.toString() + "/" + id + ".png");
|
||||
if(iconFile.exists()) {
|
||||
return BitmapFactory.decodeFile(iconFile.toString());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean siteHasNoIcon(Bookmark bookmark) {
|
||||
int id = bookmark.getId();
|
||||
File homeDir = context.getFilesDir();
|
||||
File iconFile = new File(homeDir.toString() + "/" + id + ".noicon");
|
||||
return iconFile.exists();
|
||||
}
|
||||
|
||||
private void setSiteHasNoIcon(Bookmark bookmark) {
|
||||
int id = bookmark.getId();
|
||||
File homeDir = context.getFilesDir();
|
||||
File iconFile = new File(homeDir.toString() + "/" + id + ".noicon");
|
||||
try {
|
||||
iconFile.createNewFile();
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void storeIcon(Bookmark bookmark, Bitmap icon) {
|
||||
int id = bookmark.getId();
|
||||
File homeDir = context.getFilesDir();
|
||||
File iconFile = new File(homeDir.toString() + "/" + id + ".png");
|
||||
FileOutputStream out = null;
|
||||
try {
|
||||
out = new FileOutputStream(iconFile.toString());
|
||||
icon.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||
} catch (Exception e) {
|
||||
if(BuildConfig.DEBUG) e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAll() {
|
||||
File homeDir = context.getFilesDir();
|
||||
for(File file : homeDir.listFiles()) {
|
||||
if(file.toString().endsWith(".png") || file.toString().endsWith(".noicon")) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
package org.schabi.ocbookmarks.ui;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.app.Activity;
|
||||
|
||||
import org.schabi.ocbookmarks.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Created by the-scrabi on 25.05.17.
|
||||
*/
|
||||
|
||||
public class TagsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
ArrayList<String> tagList = new ArrayList<>();
|
||||
Activity context;
|
||||
LayoutInflater inflater;
|
||||
boolean addTagMode = false;
|
||||
|
||||
public interface OnTagTapedListener {
|
||||
void onTagTaped(String tag);
|
||||
}
|
||||
private OnTagTapedListener onTagTapedListener = null;
|
||||
public void setOnTagTapedListener(OnTagTapedListener listener) {
|
||||
onTagTapedListener = listener;
|
||||
}
|
||||
|
||||
public interface OnTagEditedListener {
|
||||
void onTagEdited(String oldTag, String newTag);
|
||||
}
|
||||
private OnTagEditedListener onTagEditedListener = null;
|
||||
public void setOnTagEditedListener(OnTagEditedListener listener) {
|
||||
onTagEditedListener = listener;
|
||||
}
|
||||
|
||||
public interface OnTagDeletedListener {
|
||||
void onTagDeleted(String tag);
|
||||
}
|
||||
private OnTagDeletedListener onTagDeletedListener = null;
|
||||
public void setOnTagDeletedListener(OnTagDeletedListener listener) {
|
||||
onTagDeletedListener = listener;
|
||||
}
|
||||
|
||||
public TagsRecyclerViewAdapter(Activity acitivty, boolean addTagMode, ArrayList<String> list) {
|
||||
this.addTagMode = addTagMode;
|
||||
this.context = acitivty;
|
||||
inflater = LayoutInflater.from(context);
|
||||
tagList = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case 0:
|
||||
return new TagHolder(inflater.inflate(R.layout.tag_list_item, parent, false));
|
||||
case 1:
|
||||
return new AddTagHolder(inflater.inflate(R.layout.add_tag_list_item, parent, false));
|
||||
case 2:
|
||||
return new FooderTagHolder(inflater.inflate(R.layout.fooder_tag_list_item, parent, false));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
if(position < tagList.size()) {
|
||||
TagHolder tagHolder = (TagHolder) holder;
|
||||
tagHolder.setTag(position, tagList.get(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if(position < tagList.size()) {
|
||||
return 0;
|
||||
} else {
|
||||
if(addTagMode) {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return tagList.size() + 1;
|
||||
}
|
||||
|
||||
public void addTag(String tagName) {
|
||||
tagList.add(tagName);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
class TagHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener, View.OnLongClickListener{
|
||||
private final TextView textView;
|
||||
private final PopupMenu popup;
|
||||
private final CardView cardView;
|
||||
private String tagName;
|
||||
private int tagId;
|
||||
|
||||
public TagHolder(View view) {
|
||||
super(view);
|
||||
|
||||
textView = (TextView) view.findViewById(R.id.tag_text);
|
||||
cardView = (CardView) view.findViewById(R.id.card_view);
|
||||
|
||||
cardView.setOnClickListener(this);
|
||||
cardView.setOnLongClickListener(this);
|
||||
|
||||
popup = new PopupMenu(context, view);
|
||||
MenuInflater inflater = popup.getMenuInflater();
|
||||
inflater.inflate(R.menu.edit_tag_item_menu, popup.getMenu());
|
||||
|
||||
// try setting force show icons via reflections (android is a peace of shit)
|
||||
Object menuHelper;
|
||||
Class[] argTypes;
|
||||
try {
|
||||
Field fMenuHelper = PopupMenu.class.getDeclaredField("mPopup");
|
||||
fMenuHelper.setAccessible(true);
|
||||
menuHelper = fMenuHelper.get(popup);
|
||||
argTypes = new Class[]{boolean.class};
|
||||
menuHelper.getClass().getDeclaredMethod("setForceShowIcon", argTypes).invoke(menuHelper, true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.edit_menu:
|
||||
showEditDialog();
|
||||
return true;
|
||||
case R.id.delete_menu:
|
||||
if(!addTagMode) {
|
||||
showDeleteDialog();
|
||||
} else {
|
||||
notifyDataSetChanged();
|
||||
onTagDeletedListener.onTagDeleted(tagName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setTag(int id, String tag) {
|
||||
tagName = tag;
|
||||
tagId = id;
|
||||
textView.setText(tagName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(addTagMode) {
|
||||
showEditDialog();
|
||||
} else {
|
||||
if (onTagTapedListener != null) {
|
||||
onTagTapedListener.onTagTaped(tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
popup.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showDeleteDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.sure_to_delete_tag)
|
||||
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if(onTagDeletedListener != null) {
|
||||
for(int i = 0; i < tagList.size(); i++) {
|
||||
if(tagList.get(i).equals(tagName)) {
|
||||
tagList.remove(i);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
onTagDeletedListener.onTagDeleted(tagName);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
private void showEditDialog() {
|
||||
final EditText editText = new EditText(context);
|
||||
editText.setText(tagName);
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.edit_tag)
|
||||
.setView(editText)
|
||||
.setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
||||
//setTagName(editText.getText().toString());
|
||||
// tagList.set(tagId, editText.getText().toString());
|
||||
|
||||
if(onTagEditedListener != null) {
|
||||
onTagEditedListener.onTagEdited(tagName, editText.getText().toString());
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).create();
|
||||
dialog.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
class AddTagHolder extends RecyclerView.ViewHolder
|
||||
implements View.OnClickListener {
|
||||
|
||||
public AddTagHolder(View view) {
|
||||
super(view);
|
||||
CardView cardView = (CardView) view.findViewById(R.id.card_view);
|
||||
cardView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
final EditText editText = new EditText(context);
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.new_tag)
|
||||
.setView(editText)
|
||||
.setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
addTag(editText.getText().toString());
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}).show();
|
||||
dialog.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
class FooderTagHolder extends RecyclerView.ViewHolder {
|
||||
public FooderTagHolder(View view) {
|
||||
super(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_action_visibility.png
Normal file
|
After Width: | Height: | Size: 504 B |
BIN
app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png
Normal file
|
After Width: | Height: | Size: 148 B |
BIN
app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
app/src/main/res/drawable-hdpi/ic_folder_light_blue_700_24dp.png
Normal file
|
After Width: | Height: | Size: 168 B |
BIN
app/src/main/res/drawable-hdpi/ic_globe.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 192 B |
|
After Width: | Height: | Size: 156 B |
BIN
app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
app/src/main/res/drawable-hdpi/ic_share_black_24dp.png
Normal file
|
After Width: | Height: | Size: 398 B |
BIN
app/src/main/res/drawable-hdpi/nextcloud.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_action_visibility.png
Normal file
|
After Width: | Height: | Size: 335 B |
BIN
app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png
Normal file
|
After Width: | Height: | Size: 115 B |
BIN
app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png
Normal file
|
After Width: | Height: | Size: 111 B |
BIN
app/src/main/res/drawable-mdpi/ic_folder_light_blue_700_24dp.png
Normal file
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 128 B |
BIN
app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
app/src/main/res/drawable-mdpi/ic_share_black_24dp.png
Normal file
|
After Width: | Height: | Size: 262 B |
BIN
app/src/main/res/drawable-mdpi/nextcloud.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_action_visibility.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png
Normal file
|
After Width: | Height: | Size: 131 B |
BIN
app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png
Normal file
|
After Width: | Height: | Size: 148 B |
|
After Width: | Height: | Size: 257 B |
|
After Width: | Height: | Size: 168 B |
BIN
app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png
Normal file
|
After Width: | Height: | Size: 483 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_visibility.png
Normal file
|
After Width: | Height: | Size: 934 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png
Normal file
|
After Width: | Height: | Size: 191 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png
Normal file
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 199 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_menu_delete.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/drawable-xxhdpi/nextcloud.png
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png
Normal file
|
After Width: | Height: | Size: 194 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png
Normal file
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 449 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 258 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_mode_edit_black_24dp.png
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png
Normal file
|
After Width: | Height: | Size: 888 B |
5
app/src/main/res/drawable/add_bookmark_background.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#D2575757" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/connect.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
5
app/src/main/res/drawable/ic_add.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_add_white.png
Normal file
|
After Width: | Height: | Size: 102 B |
28
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="1344"
|
||||
android:viewportHeight="1344">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h1344v1344h-1344z"
|
||||
android:strokeLineJoin="round">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="1344"
|
||||
android:endY="1.2959057E-4"
|
||||
android:startX="163.34073"
|
||||
android:startY="1344"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FF0082C9"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FF1CAFFF"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<group android:translateX="16"
|
||||
android:translateY="16">
|
||||
<path
|
||||
android:pathData="m16,2c0.9449,0 3.9911,7.9919 4.7555,8.5752 0.7644,0.5833 8.9427,1.1565 9.2346,2.1003 0.292,0.9438 -6.0036,6.4562 -6.2956,7.4 -0.292,0.9438 2.3984,9.2899 1.6339,9.8732 -0.764,0.583 -8.383,-4.002 -9.328,-4.002 -0.9449,0 -8.5641,4.585 -9.3285,4.0017 -0.7644,-0.584 1.9259,-8.9297 1.6339,-9.8735s-6.5875,-6.4562 -6.2956,-7.4c0.292,-0.9438 8.4702,-1.517 9.2342,-2.1003 0.765,-0.5833 3.811,-8.5752 4.756,-8.5752z"
|
||||
android:fillColor="#fff"/>
|
||||
</group>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_menu_email.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
5
app/src/main/res/drawable/ic_settings.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_tag.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,10L20,8h-4L16,4h-2v4h-4L10,4L8,4v4L4,8v2h4v4L4,14v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4h4zM14,14h-4v-4h4v4z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/shadow.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- res/drawable/myrect.xml -->
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#31454545" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/shape.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/colorPrimaryDark" />
|
||||
<corners android:radius="32dp" />
|
||||
</shape>
|
||||
BIN
app/src/main/res/drawable/star.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable/star_new.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
73
app/src/main/res/layout/activity_login_acitivty.xml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.schabi.ocbookmarks.LoginAcitivty">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/starImageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/star_new" />
|
||||
|
||||
|
||||
<!-- todo: Fix this blue on blue color -->
|
||||
<Button
|
||||
android:id="@+id/ssoButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginTop="48dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:background="@drawable/shape"
|
||||
android:backgroundTint="@color/colorPrimaryDark"
|
||||
android:elevation="8dp"
|
||||
android:enabled="true"
|
||||
android:padding="16dp"
|
||||
android:text="@string/connect_sso"
|
||||
android:textColor="@color/login_error_color"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/starImageView" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/errorContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ssoButton">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@android:drawable/stat_notify_error" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginErrorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="96dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="96dp"
|
||||
android:text="Login Failed"
|
||||
android:textAlignment="center"
|
||||
android:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
27
app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:focusableInTouchMode="true"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<include
|
||||
android:id="@+id/app_bar_main"
|
||||
layout="@layout/app_bar_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nvView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:headerLayout="@layout/fragment_drawer"
|
||||
app:menu="@menu/navigation_menu" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
47
app/src/main/res/layout/add_bookmark_activity.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="176dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="64dp"
|
||||
android:background="@drawable/add_bookmark_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/successView"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center"
|
||||
app:srcCompat="@drawable/star" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressView"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/adding_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:gravity="center"
|
||||
android:text="Adding..." />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
34
app/src/main/res/layout/add_tag_list_item.xml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:id="@+id/add_tag_list_item_layout">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
card_view:cardBackgroundColor="@android:color/white"
|
||||
card_view:cardElevation="4dp"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerInside"
|
||||
card_view:srcCompat="@drawable/ic_add" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
23
app/src/main/res/layout/app_bar_main.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<include layout="@layout/content_main" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
48
app/src/main/res/layout/bookmark_list_item.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:longClickable="true"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/site_icon"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="fitXY"
|
||||
android:gravity="center"
|
||||
android:padding="15dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/item_name_and_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@id/site_icon"
|
||||
android:layout_toEndOf="@id/site_icon"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="Example"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bookmark_url_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:text="https://example.org"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
53
app/src/main/res/layout/bookmark_list_item_folder.xml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:longClickable="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:gravity="center"
|
||||
android:padding="15dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_folder_light_blue_700_24dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_back"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:gravity="center"
|
||||
android:padding="15dp"
|
||||
android:scaleType="fitXY"
|
||||
android:visibility="invisible"
|
||||
app:srcCompat="@drawable/ic_arrow_back_white_24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:layout_toRightOf="@id/icon"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/folder_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:padding="6dp"
|
||||
android:singleLine="true"
|
||||
android:text="Folder"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
44
app/src/main/res/layout/content_main.xml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:showIn="@layout/app_bar_main">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/main_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/container"
|
||||
android:name="org.schabi.ocbookmarks.BookmarkFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout="@layout/fagment_bookmarks" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/mainProgressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="@dimen/fab_margin"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:backgroundTint="@color/colorAccent"
|
||||
app:srcCompat="@drawable/ic_add" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
82
app/src/main/res/layout/edit_bookmark_dialog.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context="org.schabi.ocbookmarks.EditBookmarkDialog">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/colorPrimary"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="5dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlInput"
|
||||
android:inputType="textUri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/bookmark_url_hint" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/titleInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/bookmark_title_hint" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/descriptionInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/bookmark_description_hint" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_tags_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_tags" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tag_recycler_view"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/tag_list_item"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
27
app/src/main/res/layout/fagment_bookmarks.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.schabi.ocbookmarks.MainActivity">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swiperefresh_bookmarks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- <androidx.recyclerview.widget.RecyclerView-->
|
||||
<!-- android:id="@+id/bookmark_recycler_view"-->
|
||||
<!-- android:scrollbars="vertical"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- tools:listitem="@layout/bookmark_list_item"/>-->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
8
app/src/main/res/layout/fooder_tag_list_item.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:id="@+id/fooder_tag_list_item_layout">
|
||||
|
||||
</LinearLayout>
|
||||
57
app/src/main/res/layout/fragment_drawer.xml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/app_drawer_feed_list_background_color">
|
||||
|
||||
<!-- TODO: make header scroll up with listview -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/header_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:padding="10dp"
|
||||
android:background="@drawable/nextcloud"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text='Username'
|
||||
android:id="@+id/userTextView"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_above="@+id/urlTextView"
|
||||
android:layout_toEndOf="@+id/header_logo"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/urlTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text='https://url_to_owncloud.oc'
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_toEndOf="@+id/header_logo"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:id="@+id/header_logo"
|
||||
android:background="@drawable/shadow"
|
||||
android:layout_alignBottom="@+id/urlTextView"
|
||||
android:layout_alignParentStart="true"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/content_desc_tap_to_refresh"/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
23
app/src/main/res/layout/fragment_tags.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.schabi.ocbookmarks.MainActivity">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swiperefresh_tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tag_recycler_view"
|
||||
android:scrollbars="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/tag_list_item"/>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
24
app/src/main/res/layout/item_dir.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_arrow"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_keyboard_arrow_right_black_18dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:drawableLeft="@drawable/ic_folder_light_blue_700_24dp"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="@string/app_name"
|
||||
android:textSize="18sp" />
|
||||
</LinearLayout>
|
||||
16
app/src/main/res/layout/item_file.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_marginLeft="23dp"
|
||||
android:drawableLeft="@drawable/ic_insert_drive_file_light_blue_700_24dp"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="@string/app_name"
|
||||
android:textSize="18sp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
38
app/src/main/res/layout/tag_list_item.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:id="@+id/tag_list_item_layout">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
card_view:cardBackgroundColor="@color/colorPrimary"
|
||||
card_view:cardElevation="4dp"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
android:clickable="true"
|
||||
android:longClickable="true"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tag_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
15
app/src/main/res/menu/edit_bookmark_item_menu.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/share"
|
||||
android:title="@string/share"
|
||||
android:icon="@drawable/ic_share_black_24dp"/>
|
||||
<item
|
||||
android:id="@+id/edit_menu"
|
||||
android:title="@string/edit"
|
||||
android:icon="@drawable/ic_mode_edit_black_24dp"/>
|
||||
<item
|
||||
android:id="@+id/delete_menu"
|
||||
android:title="@string/delete"
|
||||
android:icon="@drawable/ic_delete_black_24dp"/>
|
||||
</menu>
|
||||
8
app/src/main/res/menu/edit_bookmark_menu.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?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/save_menu"
|
||||
android:title="@string/save"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||