Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-23 08:50:54 +01:00
parent f8d4c92d36
commit 0e370921f0
241 changed files with 8373 additions and 2 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

86
app/build.gradle Normal file
View 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
View 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.**

View file

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

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -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) {}
}

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

View 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);
}
}

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
package org.schabi.ocbookmarks.api;
/**
* Created by the-scrabi on 04.06.17.
*/
public class LoginData {
public boolean ssologin;
}

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

View file

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

View file

@ -0,0 +1,7 @@
package org.schabi.ocbookmarks.listener
import org.schabi.ocbookmarks.REST.model.Folder
interface FolderListener {
fun changeFolderCallback(f: Folder)
}

View file

@ -0,0 +1,5 @@
package org.schabi.ocbookmarks.listener;
public interface OnRequestReloadListener {
void requestReload();
}

View file

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

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show more